diff --git a/.changeset/itchy-kiwis-notice.md b/.changeset/itchy-kiwis-notice.md new file mode 100644 index 0000000000..1b88ae2501 --- /dev/null +++ b/.changeset/itchy-kiwis-notice.md @@ -0,0 +1,6 @@ +--- +"@redocly/openapi-core": minor +"@redocly/cli": minor +--- + +Implemented basic support for OpenRPC specification. diff --git a/docs/@v2/commands/lint.md b/docs/@v2/commands/lint.md index 85e36acf23..0135402116 100644 --- a/docs/@v2/commands/lint.md +++ b/docs/@v2/commands/lint.md @@ -2,7 +2,7 @@ ## Introduction -Redocly CLI can identify and report on problems found in OpenAPI, AsyncAPI, or Arazzo descriptions. +Redocly CLI can identify and report on problems found in OpenAPI, AsyncAPI, Arazzo, or Open-RPC descriptions. This helps you avoid bugs and make API or Arazzo descriptions more consistent. The `lint` command reports on problems and executes preprocessors and rules. @@ -50,7 +50,7 @@ The `lint` command behaves differently depending on how you pass the API(s) to i redocly lint openapi/openapi.yaml ``` -In this case, `lint` validates the API or Arazzo description(s) passed to the command. +In this case, `lint` validates the API, Arazzo, or Open-RPC description(s) passed to the command. If you have no configuration file defined, the [recommended ruleset](../rules/recommended.md) is used. If you have `extends` or `rules` defined in `redocly.yaml`, those are used when linting. @@ -432,3 +432,10 @@ The `lint` command also validates [The Arazzo](https://spec.openapis.org/arazzo/ You can pass the Arazzo description file to the `lint` command as shown below: `redocly lint arazzo.yaml` + +### Lint Open-RPC description file + +The `lint` command also validates [Open-RPC](https://spec.open-rpc.org/) description files. +You can pass the Open-RPC description file to the `lint` command as shown below: + +`redocly lint open-rpc.json` diff --git a/docs/@v2/guides/index.md b/docs/@v2/guides/index.md index 328254041a..57a420615d 100644 --- a/docs/@v2/guides/index.md +++ b/docs/@v2/guides/index.md @@ -61,6 +61,12 @@ API governance for async and streaming API applications. Check the validity of Arazzo descriptions for workflows. {% /card %} +{% card title="Lint Open-RPC descriptions" + to="./lint-openrpc" + %} +Check the validity of Open-RPC descriptions. +{% /card %} + {% card title="Change the OAuth2 token URL" to="./change-token-url" %} diff --git a/docs/@v2/guides/lint-openrpc.md b/docs/@v2/guides/lint-openrpc.md new file mode 100644 index 0000000000..23b36f03e2 --- /dev/null +++ b/docs/@v2/guides/lint-openrpc.md @@ -0,0 +1,90 @@ +--- +seo: + title: Lint Open-RPC with Redocly CLI + description: Use the Redocly CLI to enforce basic validation, configure rules, or even build custom plugins for Open-RPC. +--- + +# Lint Open-RPC with Redocly CLI + +{% admonition type="info" name="Experimental Open-RPC support" %} +This feature is at an early stage, please use with caution and send us lots of feedback! +{% /admonition %} + +In addition to providing lint functionality for multiple OpenAPI formats, Redocly CLI also has support for Open-RPC. +Redocly CLI supports the following linting approaches with Open-RPC documents: + +- Open-RPC document validation. +- Supported versions: + - [Open-RPC 1.x](https://spec.open-rpc.org/) +- Built-in rules for checking common standards requirements (see the [list of Open-RPC rules](#open-rpc-rules)). +- [Configurable rules](../rules/configurable-rules.md) so that you can build your own rules following common patterns +- [Custom plugins](../custom-plugins/index.md) for advanced users that need additional functionality + +## Lint an existing Open-RPC file + +Redocly CLI takes its settings from a `redocly.yaml` configuration file. +Below is an example of a simple configuration file for validating an Open-RPC file is in the expected format: + +```yaml +rules: + struct: error +``` + +The empty `extends` element instructs Redocly CLI not to use any existing rulesets, but to display an error if the `struct` rule finds any problem. +This rule checks that the document structure matches the Open-RPC specification. + +With this configuration file, and your Open-RPC description file, run the linting command: + +```sh +redocly lint open-rpc.json +``` + +The output describes any structural problems with the document, or reports that it is valid. + +## Open-RPC rules + +To expand the linting checks for an Open-RPC description, start by enabling some of the built-in rules. +The currently supported rules are: + +- `no-unresolved-refs`: Every `$ref` must exist. +- `no-unused-components`: All components must be used. +- `spec-no-duplicated-method-params`: The list of parameters must not include duplicated parameters. +- `spec-no-required-params-after-optional`: Required parameters must be positioned before optional parameters. +- `info-contact`: Contact section is defined under `info`. +- `info-license`: License section is defined under `info`. + +We expect the list to expand over time, so keep checking back - and let us know if you have any requests by [opening an issue on the GitHub repo](https://github.com/Redocly/redocly-cli/issues). + +To use a rule in your own linting setup, add the rule name to the `rules` configuration section, and declare the severity level (either `error`, `warn` or `off`). +Here's an example of a rules block where a missing contact section causes a warning, and a tag without a description triggers an error: + +```yaml +rules: + info-contact: warn + info-license: error +``` + +Pick and mix the available rules until you have the setup that fits your situation. + +## Configurable rule example + +Redocly CLI also offers [configurable rules](../rules/configurable-rules.md) that allow you to set assertions about the API description being linted. This functionality works for Open-RPC too. +The following example shows a configurable rule that displays a warning if the `title` field is not present in the `info` block: + +```yaml +rules: + rule/info-title: + subject: + type: Info + property: title + assertions: + defined: true + severity: warn +``` + +With the extensive configurable rules options available, there are many opportunities to make sure that your Open-RPC spec conforms with expectations. We'd also love to see what you're building - it helps us know how things are going! + +{% admonition type="info" name="Custom plugins" %} +To create your own plugins, see the [custom plugins documentation](../custom-plugins/index.md). + +{% /admonition %} diff --git a/docs/@v2/rules.md b/docs/@v2/rules.md index 3d5d9df108..c8128ab113 100644 --- a/docs/@v2/rules.md +++ b/docs/@v2/rules.md @@ -9,7 +9,7 @@ Redocly uses rules to describe all the different aspects of API behavior that we - **Rulesets** are groups of rules that can be applied to any API. This is a good way to build up a ruleset that you can use locally or with your CI. Multiple rulesets can be used at once, so feel free to make smaller ones and compose the rulesets that fit each API. - **Built-in rules:** for the most common use cases, the rules are already made for you, all you need to do is choose if they should cause an `error`, simply `warn` of a problem, or be turned `off`. [See the built-in rules documentation](./rules/built-in-rules.md) for more information and examples. -- **Configurable rules** allow powerful describing of API standards without needing to write code. Create a configurable rule, choose which parts of the OpenAPI description it applies to, and what the criteria for success are. The linting tool does the rest. With plenty of examples, the [configurable rules](./rules/configurable-rules.md) helps you to describe your API standards easily and well. +- **Configurable rules** allow powerful describing of API standards without needing to write code. Create a configurable rule, choose which parts of the API description it applies to, and what the criteria for success are. The linting tool does the rest. With plenty of examples, the [configurable rules](./rules/configurable-rules.md) helps you to describe your API standards easily and well. - **Custom code rules** if none of the above exactly fits your needs, then a [custom code plugin](./custom-plugins/index.md) is an extensible way to bring some custom JavaScript to build on Redocly's existing features. ## Rulesets diff --git a/docs/@v2/rules/built-in-rules.md b/docs/@v2/rules/built-in-rules.md index 24270b1e30..006434c285 100644 --- a/docs/@v2/rules/built-in-rules.md +++ b/docs/@v2/rules/built-in-rules.md @@ -20,6 +20,7 @@ Redocly CLI can lint multiple API description formats: - [OpenAPI](#openapi-rules) - [AsyncAPI](#asyncapi-rules) - [Arazzo](#arazzo-rules) +- [Open-RPC](#open-rpc-rules) - Overlay Visit each page for details of what the rule does, additional configuration options, and examples of it in use. @@ -148,6 +149,18 @@ The below rules are being migrated to Respect: - [x-security-scheme-name-reference](./respect/x-security-scheme-name-reference.md): when multiple `sourceDescriptions` exist, `workflow.x-security.schemeName` must reference a source description (for example, `$sourceDescriptions.{name}.schemeName`) - [x-security-scheme-required-values](./respect/x-security-scheme-required-values.md) validate that `x-security` have all required `values` described according to the used `scheme`. +## Open-RPC rules + +Use the rules in this section for Open-RPC specific linting. + +- [struct](./common/struct.md): Conform to the declared Open-RPC specification version +- [no-unresolved-refs](./common/no-unresolved-refs.md): Every `$ref` must exist +- [no-unused-components](./oas/no-unused-components.md): All components must be used +- `spec-no-duplicated-method-params`: The list of parameters must not include duplicated parameters +- `spec-no-required-params-after-optional`: Required parameters must be positioned before optional parameters +- [info-contact](./oas/info-contact.md): Contact section is defined under `info` +- [info-license](./oas/info-license.md): License section is defined under `info` + ## Resources - Learn more about [API linting](../api-standards.md), or follow the [guide to configuring a ruleset](../guides/configure-rules.md). diff --git a/packages/cli/src/__tests__/fixtures/config.ts b/packages/cli/src/__tests__/fixtures/config.ts index 24ddac4e23..bf22eaa163 100644 --- a/packages/cli/src/__tests__/fixtures/config.ts +++ b/packages/cli/src/__tests__/fixtures/config.ts @@ -25,6 +25,7 @@ export const configFixture: Config = { async3: {}, arazzo1: {}, overlay1: {}, + openrpc1: {}, }, preprocessors: { oas2: {}, @@ -35,6 +36,7 @@ export const configFixture: Config = { async3: {}, arazzo1: {}, overlay1: {}, + openrpc1: {}, }, plugins: [], doNotResolveExamples: false, @@ -47,6 +49,7 @@ export const configFixture: Config = { async3: {}, arazzo1: {}, overlay1: {}, + openrpc1: {}, }, resolveIgnore: vi.fn(), addProblemToIgnore: vi.fn(), diff --git a/packages/cli/src/__tests__/utils.test.ts b/packages/cli/src/__tests__/utils.test.ts index 8a7eb1b1d6..e0d32e4bb0 100644 --- a/packages/cli/src/__tests__/utils.test.ts +++ b/packages/cli/src/__tests__/utils.test.ts @@ -546,6 +546,7 @@ describe('checkIfRulesetExist', () => { async3: {}, arazzo1: {}, overlay1: {}, + openrpc1: {}, }; expect(() => checkIfRulesetExist(rules)).toThrowError( '⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/' @@ -562,6 +563,7 @@ describe('checkIfRulesetExist', () => { async3: {}, arazzo1: {}, overlay1: {}, + openrpc1: {}, }; checkIfRulesetExist(rules); }); diff --git a/packages/core/src/__tests__/openrpc.test.ts b/packages/core/src/__tests__/openrpc.test.ts new file mode 100644 index 0000000000..cfcbf7b140 --- /dev/null +++ b/packages/core/src/__tests__/openrpc.test.ts @@ -0,0 +1,217 @@ +import { describe, expect, test } from 'vitest'; +import { lintFromString } from '../lint.js'; +import { createConfig } from '../config/index.js'; +import { detectSpec } from '../detect-spec.js'; + +describe('Open-RPC support', () => { + const openRpcDocument = JSON.stringify({ + openrpc: '1.2.6', + info: { + title: 'Petstore', + version: '1.0.0', + }, + methods: [ + { + name: 'listPets', + params: [], + result: { + name: 'pets', + schema: { + type: 'array', + items: { $ref: '#/components/schemas/Pet' }, + }, + }, + }, + ], + components: { + schemas: { + Pet: { + type: 'object', + properties: { + id: { type: 'integer' }, + name: { type: 'string' }, + }, + required: ['id', 'name'], + }, + }, + }, + }); + + test('detects Open-RPC 1.x', () => { + const parsed = JSON.parse(openRpcDocument); + expect(detectSpec(parsed)).toBe('openrpc1'); + }); + + test('lints valid Open-RPC document', async () => { + const config = await createConfig({ + extends: ['recommended'], + rules: { 'info-license': 'off' }, + }); + const result = await lintFromString({ + source: openRpcDocument, + config, + }); + + expect(result).toEqual([]); + }); + + test('lints invalid Open-RPC document (structural error)', async () => { + const invalidDocument = JSON.stringify({ + openrpc: '1.2.6', + info: { + // missing version + title: 'Petstore', + }, + methods: [], + }); + + const config = await createConfig({ + extends: ['recommended'], + rules: { 'info-license': 'off' }, + }); + const result = await lintFromString({ + source: invalidDocument, + config, + }); + + expect(result).toHaveLength(1); + expect(result[0].ruleId).toBe('struct'); + expect(result[0].message).toContain('The field `version` must be present on this level'); + }); + + test('lints invalid Open-RPC document (missing required field)', async () => { + const invalidDocument = JSON.stringify({ + openrpc: '1.2.6', + // missing info + methods: [], + }); + + const config = await createConfig({ + extends: ['recommended'], + rules: { 'info-license': 'off' }, + }); + const result = await lintFromString({ + source: invalidDocument, + config, + }); + + expect(result.length).toBeGreaterThan(0); + expect(result[0].ruleId).toBe('struct'); + expect(result[0].message).toContain('The field `info` must be present on this level'); + }); + + test('reports unused components', async () => { + const documentWithUnused = JSON.stringify({ + openrpc: '1.2.6', + info: { + title: 'Petstore', + version: '1.0.0', + }, + methods: [ + { + name: 'listPets', + params: [], + result: { + name: 'pets', + schema: { + type: 'array', + items: { $ref: '#/components/schemas/Pet' }, + }, + }, + }, + ], + components: { + schemas: { + Pet: { + type: 'object', + properties: { + id: { type: 'integer' }, + name: { type: 'string' }, + }, + required: ['id', 'name'], + }, + UnusedPet: { + type: 'object', + properties: { + id: { type: 'integer' }, + name: { type: 'string' }, + }, + required: ['id', 'name'], + }, + }, + }, + }); + + const config = await createConfig({ + rules: { 'no-unused-components': 'error' }, + }); + const result = await lintFromString({ + source: documentWithUnused, + config, + }); + + expect(result).toHaveLength(1); + expect(result[0].ruleId).toBe('no-unused-components'); + expect(result[0].message).toContain('Component: "UnusedPet" is never used.'); + }); + + test('reports duplicated method parameters', async () => { + const documentWithDuplicates = JSON.stringify({ + openrpc: '1.2.6', + info: { title: 'Petstore', version: '1.0.0' }, + methods: [ + { + name: 'listPets', + params: [ + { name: 'limit', schema: { type: 'integer' } }, + { name: 'limit', schema: { type: 'integer' } }, + ], + result: { name: 'pets', schema: { type: 'array' } }, + }, + ], + }); + + const config = await createConfig({ + rules: { 'spec-no-duplicated-method-params': 'error' }, + }); + const result = await lintFromString({ + source: documentWithDuplicates, + config, + }); + + expect(result).toHaveLength(1); + expect(result[0].ruleId).toBe('spec-no-duplicated-method-params'); + expect(result[0].message).toContain("Duplicate parameter name 'limit' found."); + }); + + test('reports required parameters after optional ones', async () => { + const documentWithBadOrder = JSON.stringify({ + openrpc: '1.2.6', + info: { title: 'Petstore', version: '1.0.0' }, + methods: [ + { + name: 'listPets', + params: [ + { name: 'optionalParam', required: false, schema: { type: 'integer' } }, + { name: 'requiredParam', required: true, schema: { type: 'integer' } }, + ], + result: { name: 'pets', schema: { type: 'array' } }, + }, + ], + }); + + const config = await createConfig({ + rules: { 'spec-no-required-params-after-optional': 'error' }, + }); + const result = await lintFromString({ + source: documentWithBadOrder, + config, + }); + + expect(result).toHaveLength(1); + expect(result[0].ruleId).toBe('spec-no-required-params-after-optional'); + expect(result[0].message).toContain( + "Required parameter 'requiredParam' must be positioned before optional parameters." + ); + }); +}); diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index bcd16978d4..f993d8999b 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -79,6 +79,25 @@ export function mapTypeToComponent(typeName: string, version: SpecMajorVersion) default: return null; } + case 'openrpc1': + switch (typeName) { + case 'ContentDescriptor': + return 'contentDescriptors'; + case 'Schema': + return 'schemas'; + case 'Example': + return 'examples'; + case 'Link': + return 'links'; + case 'ErrorObject': + return 'errors'; + case 'ExamplePairing': + return 'examplePairingObjects'; + case 'Tag': + return 'tags'; + default: + return null; + } } } @@ -163,6 +182,8 @@ export function makeBundleVisitor( components = root.components = root.components || {}; } else if (version === 'arazzo1') { components = root.components = root.components || {}; + } else if (version === 'openrpc1') { + components = root.components = root.components || {}; } }, }, @@ -205,7 +226,12 @@ export function makeBundleVisitor( components[componentType] = components[componentType] || {}; const name = getComponentName(target, componentType, ctx); components[componentType][name] = target.node; - if (version === 'oas3' || version === 'async2' || version === 'async3') { + if ( + version === 'oas3' || + version === 'async2' || + version === 'async3' || + version === 'openrpc1' + ) { return `#/components/${componentType}/${name}`; } else { return `#/${componentType}/${name}`; diff --git a/packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap b/packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap index 61ef0ec1de..ba1acb7d6d 100644 --- a/packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap +++ b/packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap @@ -286,6 +286,15 @@ exports[`resolveConfig > should ignore minimal from the root and read local file "tag-description": "warn", "tags-alphabetical": "off", }, + "openrpc1Decorators": {}, + "openrpc1Preprocessors": {}, + "openrpc1Rules": { + "info-contact": "off", + "info-license": "warn", + "no-unused-components": "warn", + "spec-no-duplicated-method-params": "error", + "spec-no-required-params-after-optional": "error", + }, "overlay1Decorators": {}, "overlay1Preprocessors": {}, "overlay1Rules": { @@ -657,6 +666,15 @@ exports[`resolveConfig > should resolve extends with local file config which con "tag-description": "warn", "tags-alphabetical": "off", }, + "openrpc1Decorators": {}, + "openrpc1Preprocessors": {}, + "openrpc1Rules": { + "info-contact": "off", + "info-license": "warn", + "no-unused-components": "warn", + "spec-no-duplicated-method-params": "error", + "spec-no-required-params-after-optional": "error", + }, "overlay1Decorators": {}, "overlay1Preprocessors": {}, "overlay1Rules": { diff --git a/packages/core/src/config/__tests__/__snapshots__/config.test.ts.snap b/packages/core/src/config/__tests__/__snapshots__/config.test.ts.snap index 5957427ec5..c744acb40c 100644 --- a/packages/core/src/config/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/core/src/config/__tests__/__snapshots__/config.test.ts.snap @@ -14,6 +14,7 @@ Config { "oas3_0": {}, "oas3_1": {}, "oas3_2": {}, + "openrpc1": {}, "overlay1": {}, }, "doNotResolveExamples": false, @@ -39,6 +40,7 @@ Config { "oas3_0": {}, "oas3_1": {}, "oas3_2": {}, + "openrpc1": {}, "overlay1": {}, }, "resolve": { @@ -78,6 +80,10 @@ Config { "no-empty-servers": "error", "operation-summary": "error", }, + "openrpc1": { + "no-empty-servers": "error", + "operation-summary": "error", + }, "overlay1": { "no-empty-servers": "error", "operation-summary": "error", diff --git a/packages/core/src/config/__tests__/__snapshots__/load.test.ts.snap b/packages/core/src/config/__tests__/__snapshots__/load.test.ts.snap index 573e01e946..cb8265aec4 100644 --- a/packages/core/src/config/__tests__/__snapshots__/load.test.ts.snap +++ b/packages/core/src/config/__tests__/__snapshots__/load.test.ts.snap @@ -14,6 +14,7 @@ Config { "oas3_0": {}, "oas3_1": {}, "oas3_2": {}, + "openrpc1": {}, "overlay1": {}, }, "doNotResolveExamples": false, @@ -28,6 +29,7 @@ Config { "oas3_0": {}, "oas3_1": {}, "oas3_2": {}, + "openrpc1": {}, "overlay1": {}, }, "resolve": { @@ -61,6 +63,9 @@ Config { "oas3_2Decorators": {}, "oas3_2Preprocessors": {}, "oas3_2Rules": {}, + "openrpc1Decorators": {}, + "openrpc1Preprocessors": {}, + "openrpc1Rules": {}, "overlay1Decorators": {}, "overlay1Preprocessors": {}, "overlay1Rules": {}, @@ -217,6 +222,22 @@ Config { "no-empty-servers": "error", "operation-summary": "error", }, + "openrpc1": { + "assertions": [ + { + "assertionId": "rule/test", + "assertions": { + "defined": true, + }, + "subject": { + "property": "x-test", + "type": "Operation", + }, + }, + ], + "no-empty-servers": "error", + "operation-summary": "error", + }, "overlay1": { "assertions": [ { diff --git a/packages/core/src/config/__tests__/config.test.ts b/packages/core/src/config/__tests__/config.test.ts index c3ada9d8a3..5aa93cca3e 100644 --- a/packages/core/src/config/__tests__/config.test.ts +++ b/packages/core/src/config/__tests__/config.test.ts @@ -56,6 +56,7 @@ describe('Config.forAlias', () => { "oas3_0": {}, "oas3_1": {}, "oas3_2": {}, + "openrpc1": {}, "overlay1": {}, }, "doNotResolveExamples": false, @@ -96,6 +97,7 @@ describe('Config.forAlias', () => { "oas3_0": {}, "oas3_1": {}, "oas3_2": {}, + "openrpc1": {}, "overlay1": {}, }, "resolve": { @@ -127,6 +129,9 @@ describe('Config.forAlias', () => { "oas3_2Decorators": {}, "oas3_2Preprocessors": {}, "oas3_2Rules": {}, + "openrpc1Decorators": {}, + "openrpc1Preprocessors": {}, + "openrpc1Rules": {}, "overlay1Decorators": {}, "overlay1Preprocessors": {}, "overlay1Rules": {}, @@ -173,6 +178,10 @@ describe('Config.forAlias', () => { "no-empty-servers": "error", "operation-summary": "warn", }, + "openrpc1": { + "no-empty-servers": "error", + "operation-summary": "warn", + }, "overlay1": { "no-empty-servers": "error", "operation-summary": "warn", diff --git a/packages/core/src/config/__tests__/load.test.ts b/packages/core/src/config/__tests__/load.test.ts index c492dd109f..a6a39183ac 100644 --- a/packages/core/src/config/__tests__/load.test.ts +++ b/packages/core/src/config/__tests__/load.test.ts @@ -416,6 +416,15 @@ describe('loadConfig', () => { "tag-description": "warn", "tags-alphabetical": "off", }, + "openrpc1Decorators": {}, + "openrpc1Preprocessors": {}, + "openrpc1Rules": { + "info-contact": "off", + "info-license": "off", + "no-unused-components": "off", + "spec-no-duplicated-method-params": "error", + "spec-no-required-params-after-optional": "error", + }, "overlay1Decorators": {}, "overlay1Preprocessors": {}, "overlay1Rules": { @@ -716,6 +725,15 @@ describe('loadConfig', () => { "tag-description": "warn", "tags-alphabetical": "off", }, + "openrpc1Decorators": {}, + "openrpc1Preprocessors": {}, + "openrpc1Rules": { + "info-contact": "off", + "info-license": "warn", + "no-unused-components": "warn", + "spec-no-duplicated-method-params": "error", + "spec-no-required-params-after-optional": "error", + }, "overlay1Decorators": {}, "overlay1Preprocessors": {}, "overlay1Rules": { @@ -1029,6 +1047,15 @@ describe('loadConfig', () => { "tag-description": "warn", "tags-alphabetical": "off", }, + "openrpc1Decorators": {}, + "openrpc1Preprocessors": {}, + "openrpc1Rules": { + "info-contact": "off", + "info-license": "off", + "no-unused-components": "off", + "spec-no-duplicated-method-params": "error", + "spec-no-required-params-after-optional": "error", + }, "overlay1Decorators": {}, "overlay1Preprocessors": {}, "overlay1Rules": { diff --git a/packages/core/src/config/all.ts b/packages/core/src/config/all.ts index b1bc873ed8..838441be1a 100644 --- a/packages/core/src/config/all.ts +++ b/packages/core/src/config/all.ts @@ -298,6 +298,13 @@ const all: RawGovernanceConfig<'built-in'> = { overlay1Rules: { 'info-contact': 'error', }, + openrpc1Rules: { + 'info-contact': 'error', + 'info-license': 'error', + 'no-unused-components': 'error', + 'spec-no-duplicated-method-params': 'error', + 'spec-no-required-params-after-optional': 'error', + }, }; export default all; diff --git a/packages/core/src/config/builtIn.ts b/packages/core/src/config/builtIn.ts index 863e5f0833..d08f26cb9d 100644 --- a/packages/core/src/config/builtIn.ts +++ b/packages/core/src/config/builtIn.ts @@ -21,12 +21,17 @@ import { rules as overlay1Rules, preprocessors as overlay1Preprocessors, } from '../rules/overlay1/index.js'; +import { + rules as openrpc1Rules, + preprocessors as openrpc1Preprocessors, +} from '../rules/openrpc/index.js'; import { decorators as oas3Decorators } from '../decorators/oas3/index.js'; import { decorators as oas2Decorators } from '../decorators/oas2/index.js'; import { decorators as async2Decorators } from '../decorators/async2/index.js'; import { decorators as async3Decorators } from '../decorators/async3/index.js'; import { decorators as arazzo1Decorators } from '../decorators/arazzo/index.js'; import { decorators as overlay1Decorators } from '../decorators/overlay1/index.js'; +import { decorators as openrpc1Decorators } from '../decorators/openrpc/index.js'; import { type Plugin, type RawGovernanceConfig } from './types.js'; export const builtInConfigs: Record = { @@ -46,6 +51,7 @@ export const defaultPlugin: Plugin<'built-in'> = { async3: async3Rules, arazzo1: arazzo1Rules, overlay1: overlay1Rules, + openrpc1: openrpc1Rules, }, preprocessors: { oas3: oas3Preprocessors, @@ -54,6 +60,7 @@ export const defaultPlugin: Plugin<'built-in'> = { async3: async3Preprocessors, arazzo1: arazzoPreprocessors, overlay1: overlay1Preprocessors, + openrpc1: openrpc1Preprocessors, }, decorators: { oas3: oas3Decorators, @@ -62,6 +69,7 @@ export const defaultPlugin: Plugin<'built-in'> = { async3: async3Decorators, arazzo1: arazzo1Decorators, overlay1: overlay1Decorators, + openrpc1: openrpc1Decorators, }, configs: builtInConfigs, }; diff --git a/packages/core/src/config/config-resolvers.ts b/packages/core/src/config/config-resolvers.ts index fc720b1b66..b2c7a35830 100644 --- a/packages/core/src/config/config-resolvers.ts +++ b/packages/core/src/config/config-resolvers.ts @@ -329,10 +329,11 @@ export async function resolvePlugins( !pluginInstance.rules.async2 && !pluginInstance.rules.async3 && !pluginInstance.rules.arazzo1 && - !pluginInstance.rules.overlay1 + !pluginInstance.rules.overlay1 && + !pluginInstance.rules.openrpc1 ) { throw new Error( - `Plugin rules must have \`oas3\`, \`oas2\`, \`async2\`, \`async3\`, \`arazzo\`, or \`overlay1\` rules "${p}.` + `Plugin rules must have \`oas3\`, \`oas2\`, \`async2\`, \`async3\`, \`arazzo\`, \`overlay1\`, or \`openrpc1\` rules "${p}.` ); } plugin.rules = {}; @@ -354,6 +355,9 @@ export async function resolvePlugins( if (pluginInstance.rules.overlay1) { plugin.rules.overlay1 = prefixRules(pluginInstance.rules.overlay1, id); } + if (pluginInstance.rules.openrpc1) { + plugin.rules.openrpc1 = prefixRules(pluginInstance.rules.openrpc1, id); + } } if (pluginInstance.preprocessors) { if ( @@ -362,10 +366,11 @@ export async function resolvePlugins( !pluginInstance.preprocessors.async2 && !pluginInstance.preprocessors.async3 && !pluginInstance.preprocessors.arazzo1 && - !pluginInstance.preprocessors.overlay1 + !pluginInstance.preprocessors.overlay1 && + !pluginInstance.preprocessors.openrpc1 ) { throw new Error( - `Plugin \`preprocessors\` must have \`oas3\`, \`oas2\`, \`async2\`, \`async3\`, \`arazzo1\`, or \`overlay1\` preprocessors "${p}.` + `Plugin \`preprocessors\` must have \`oas3\`, \`oas2\`, \`async2\`, \`async3\`, \`arazzo1\`, \`overlay1\`, or \`openrpc1\` preprocessors "${p}.` ); } plugin.preprocessors = {}; @@ -393,6 +398,12 @@ export async function resolvePlugins( id ); } + if (pluginInstance.preprocessors.openrpc1) { + plugin.preprocessors.openrpc1 = prefixRules( + pluginInstance.preprocessors.openrpc1, + id + ); + } } if (pluginInstance.decorators) { @@ -402,10 +413,11 @@ export async function resolvePlugins( !pluginInstance.decorators.async2 && !pluginInstance.decorators.async3 && !pluginInstance.decorators.arazzo1 && - !pluginInstance.decorators.overlay1 + !pluginInstance.decorators.overlay1 && + !pluginInstance.decorators.openrpc1 ) { throw new Error( - `Plugin \`decorators\` must have \`oas3\`, \`oas2\`, \`async2\`, \`async3\`, \`arazzo1\`, or \`overlay1\` decorators "${p}.` + `Plugin \`decorators\` must have \`oas3\`, \`oas2\`, \`async2\`, \`async3\`, \`arazzo1\`, \`overlay1\`, or \`openrpc1\` decorators "${p}.` ); } plugin.decorators = {}; @@ -427,6 +439,9 @@ export async function resolvePlugins( if (pluginInstance.decorators.overlay1) { plugin.decorators.overlay1 = prefixRules(pluginInstance.decorators.overlay1, id); } + if (pluginInstance.decorators.openrpc1) { + plugin.decorators.openrpc1 = prefixRules(pluginInstance.decorators.openrpc1, id); + } } if (pluginInstance.assertions) { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index a93a238ce2..b99170eb2a 100755 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -20,6 +20,7 @@ import type { Async3RuleSet, Arazzo1RuleSet, Overlay1RuleSet, + OpenRpc1RuleSet, SpecVersion, SpecMajorVersion, } from '../oas-types.js'; @@ -95,6 +96,7 @@ export class Config { async3: group({ ...resolvedConfig.rules, ...resolvedConfig.async3Rules }), arazzo1: group({ ...resolvedConfig.rules, ...resolvedConfig.arazzo1Rules }), overlay1: group({ ...resolvedConfig.rules, ...resolvedConfig.overlay1Rules }), + openrpc1: group({ ...resolvedConfig.rules, ...resolvedConfig.openrpc1Rules }), }; this.preprocessors = { @@ -127,6 +129,10 @@ export class Config { ...resolvedConfig.preprocessors, ...resolvedConfig.overlay1Preprocessors, }, + openrpc1: { + ...resolvedConfig.preprocessors, + ...resolvedConfig.openrpc1Preprocessors, + }, }; this.decorators = { @@ -141,6 +147,10 @@ export class Config { ...resolvedConfig.decorators, ...resolvedConfig.overlay1Decorators, }, + openrpc1: { + ...resolvedConfig.decorators, + ...resolvedConfig.openrpc1Decorators, + }, }; this.resolveIgnore(getIgnoreFilePath(opts.configPath)); @@ -264,6 +274,10 @@ export class Config { if (!plugin.typeExtension.overlay1) continue; extendedTypes = plugin.typeExtension.overlay1(extendedTypes, version); break; + case 'openrpc1': + if (!plugin.typeExtension.openrpc1) continue; + extendedTypes = plugin.typeExtension.openrpc1(extendedTypes, version); + break; default: throw new Error('Not implemented'); } @@ -397,6 +411,17 @@ export class Config { (p) => p.decorators?.overlay1 && overlay1Rules.push(p.decorators.overlay1) ); return overlay1Rules; + case 'openrpc1': + // eslint-disable-next-line no-case-declarations + const openrpc1Rules: OpenRpc1RuleSet[] = []; + this.plugins.forEach( + (p) => p.preprocessors?.openrpc1 && openrpc1Rules.push(p.preprocessors.openrpc1) + ); + this.plugins.forEach((p) => p.rules?.openrpc1 && openrpc1Rules.push(p.rules.openrpc1)); + this.plugins.forEach( + (p) => p.decorators?.openrpc1 && openrpc1Rules.push(p.decorators.openrpc1) + ); + return openrpc1Rules; } } diff --git a/packages/core/src/config/minimal.ts b/packages/core/src/config/minimal.ts index 9b0687369e..2fae120edd 100644 --- a/packages/core/src/config/minimal.ts +++ b/packages/core/src/config/minimal.ts @@ -277,6 +277,13 @@ const minimal: RawGovernanceConfig<'built-in'> = { overlay1Rules: { 'info-contact': 'off', }, + openrpc1Rules: { + 'info-contact': 'off', + 'info-license': 'off', + 'no-unused-components': 'off', + 'spec-no-duplicated-method-params': 'error', + 'spec-no-required-params-after-optional': 'error', + }, }; export default minimal; diff --git a/packages/core/src/config/recommended-strict.ts b/packages/core/src/config/recommended-strict.ts index 1ad61f28ee..6a9b268766 100644 --- a/packages/core/src/config/recommended-strict.ts +++ b/packages/core/src/config/recommended-strict.ts @@ -277,6 +277,13 @@ const recommendedStrict: RawGovernanceConfig<'built-in'> = { overlay1Rules: { 'info-contact': 'off', }, + openrpc1Rules: { + 'info-contact': 'off', + 'info-license': 'error', + 'no-unused-components': 'error', + 'spec-no-duplicated-method-params': 'error', + 'spec-no-required-params-after-optional': 'error', + }, }; export default recommendedStrict; diff --git a/packages/core/src/config/recommended.ts b/packages/core/src/config/recommended.ts index cc2119c7a4..4696000436 100644 --- a/packages/core/src/config/recommended.ts +++ b/packages/core/src/config/recommended.ts @@ -277,6 +277,13 @@ const recommended: RawGovernanceConfig<'built-in'> = { overlay1Rules: { 'info-contact': 'off', }, + openrpc1Rules: { + 'info-license': 'warn', + 'no-unused-components': 'warn', + 'info-contact': 'off', + 'spec-no-duplicated-method-params': 'error', + 'spec-no-required-params-after-optional': 'error', + }, }; export default recommended; diff --git a/packages/core/src/config/rules.ts b/packages/core/src/config/rules.ts index f091e0e248..f0224bc393 100644 --- a/packages/core/src/config/rules.ts +++ b/packages/core/src/config/rules.ts @@ -7,6 +7,7 @@ import type { Oas2RuleSet, Oas3RuleSet, Overlay1RuleSet, + OpenRpc1RuleSet, SpecVersion, } from '../oas-types.js'; import type { Config } from './config.js'; @@ -26,6 +27,7 @@ export function initRules( | Async3RuleSet | Arazzo1RuleSet | Overlay1RuleSet + | OpenRpc1RuleSet )[], config: Config, type: 'rules' | 'preprocessors' | 'decorators', diff --git a/packages/core/src/config/spec.ts b/packages/core/src/config/spec.ts index c7df6bef00..db869c6dce 100644 --- a/packages/core/src/config/spec.ts +++ b/packages/core/src/config/spec.ts @@ -277,6 +277,13 @@ const spec: RawGovernanceConfig<'built-in'> = { overlay1Rules: { 'info-contact': 'off', }, + openrpc1Rules: { + 'info-contact': 'off', + 'info-license': 'off', + 'no-unused-components': 'off', + 'spec-no-duplicated-method-params': 'error', + 'spec-no-required-params-after-optional': 'error', + }, }; export default spec; diff --git a/packages/core/src/config/types.ts b/packages/core/src/config/types.ts index 42569f7816..685228a050 100644 --- a/packages/core/src/config/types.ts +++ b/packages/core/src/config/types.ts @@ -23,6 +23,9 @@ import type { Overlay1PreprocessorsSet, Overlay1DecoratorsSet, Overlay1RuleSet, + OpenRpc1RuleSet, + OpenRpc1PreprocessorsSet, + OpenRpc1DecoratorsSet, } from '../oas-types.js'; import type { NodeType } from '../types/index.js'; import type { SkipFunctionContext } from '../visitors.js'; @@ -34,6 +37,7 @@ import type { BuiltInAsync3RuleId, BuiltInArazzo1RuleId, BuiltInOverlay1RuleId, + BuiltInOpenRpc1RuleId, BuiltInCommonRuleId, } from '../types/redocly-yaml.js'; @@ -89,6 +93,7 @@ export type RawGovernanceConfig = async3Rules?: RuleMap; arazzo1Rules?: RuleMap; overlay1Rules?: RuleMap; + openrpc1Rules?: RuleMap; preprocessors?: Record; oas2Preprocessors?: Record; @@ -99,6 +104,7 @@ export type RawGovernanceConfig = async3Preprocessors?: Record; arazzo1Preprocessors?: Record; overlay1Preprocessors?: Record; + openrpc1Preprocessors?: Record; decorators?: Record; oas2Decorators?: Record; @@ -109,6 +115,7 @@ export type RawGovernanceConfig = async3Decorators?: Record; arazzo1Decorators?: Record; overlay1Decorators?: Record; + openrpc1Decorators?: Record; }; export type ResolvedGovernanceConfig = Omit; @@ -120,6 +127,7 @@ export type PreprocessorsConfig = { async3?: Async3PreprocessorsSet; arazzo1?: Arazzo1PreprocessorsSet; overlay1?: Overlay1PreprocessorsSet; + openrpc1?: OpenRpc1PreprocessorsSet; }; export type DecoratorsConfig = { @@ -129,6 +137,7 @@ export type DecoratorsConfig = { async3?: Async3DecoratorsSet; arazzo1?: Arazzo1DecoratorsSet; overlay1?: Overlay1DecoratorsSet; + openrpc1?: OpenRpc1DecoratorsSet; }; export type TypesExtensionFn = ( @@ -145,6 +154,7 @@ export type RulesConfig = { async3?: Async3RuleSet; arazzo1?: Arazzo1RuleSet; overlay1?: Overlay1RuleSet; + openrpc1?: OpenRpc1RuleSet; }; export type CustomRulesConfig = RulesConfig; diff --git a/packages/core/src/config/utils.ts b/packages/core/src/config/utils.ts index f220dccc04..31851bf3ec 100644 --- a/packages/core/src/config/utils.ts +++ b/packages/core/src/config/utils.ts @@ -9,6 +9,7 @@ import type { Async2RuleSet, Arazzo1RuleSet, Overlay1RuleSet, + OpenRpc1RuleSet, } from '../oas-types.js'; export function parsePresetName(presetName: string): { pluginId: string; configName: string } { @@ -28,6 +29,7 @@ export function prefixRules< | Async2RuleSet | Arazzo1RuleSet | Overlay1RuleSet + | OpenRpc1RuleSet >(rules: T, prefix: string) { if (!prefix) return rules; @@ -50,6 +52,7 @@ export function mergeExtends(rulesConfList: ResolvedGovernanceConfig[]) { async3Rules: {}, arazzo1Rules: {}, overlay1Rules: {}, + openrpc1Rules: {}, preprocessors: {}, oas2Preprocessors: {}, @@ -60,6 +63,7 @@ export function mergeExtends(rulesConfList: ResolvedGovernanceConfig[]) { async3Preprocessors: {}, arazzo1Preprocessors: {}, overlay1Preprocessors: {}, + openrpc1Preprocessors: {}, decorators: {}, oas2Decorators: {}, @@ -70,6 +74,7 @@ export function mergeExtends(rulesConfList: ResolvedGovernanceConfig[]) { async3Decorators: {}, arazzo1Decorators: {}, overlay1Decorators: {}, + openrpc1Decorators: {}, }; for (const rulesConf of rulesConfList) { @@ -96,6 +101,8 @@ export function mergeExtends(rulesConfList: ResolvedGovernanceConfig[]) { assignOnlyExistingConfig(result.arazzo1Rules, rulesConf.rules); assignConfig(result.overlay1Rules, rulesConf.overlay1Rules); assignOnlyExistingConfig(result.overlay1Rules, rulesConf.rules); + assignConfig(result.openrpc1Rules, rulesConf.openrpc1Rules); + assignOnlyExistingConfig(result.openrpc1Rules, rulesConf.rules); assignConfig(result.preprocessors, rulesConf.preprocessors); assignConfig(result.oas2Preprocessors, rulesConf.oas2Preprocessors); @@ -114,6 +121,8 @@ export function mergeExtends(rulesConfList: ResolvedGovernanceConfig[]) { assignOnlyExistingConfig(result.arazzo1Preprocessors, rulesConf.preprocessors); assignConfig(result.overlay1Preprocessors, rulesConf.overlay1Preprocessors); assignOnlyExistingConfig(result.overlay1Preprocessors, rulesConf.preprocessors); + assignConfig(result.openrpc1Preprocessors, rulesConf.openrpc1Preprocessors); + assignOnlyExistingConfig(result.openrpc1Preprocessors, rulesConf.preprocessors); assignConfig(result.decorators, rulesConf.decorators); assignConfig(result.oas2Decorators, rulesConf.oas2Decorators); @@ -132,6 +141,8 @@ export function mergeExtends(rulesConfList: ResolvedGovernanceConfig[]) { assignOnlyExistingConfig(result.arazzo1Decorators, rulesConf.decorators); assignConfig(result.overlay1Decorators, rulesConf.overlay1Decorators); assignOnlyExistingConfig(result.overlay1Decorators, rulesConf.decorators); + assignConfig(result.openrpc1Decorators, rulesConf.openrpc1Decorators); + assignOnlyExistingConfig(result.openrpc1Decorators, rulesConf.decorators); } return result; diff --git a/packages/core/src/decorators/openrpc/index.ts b/packages/core/src/decorators/openrpc/index.ts new file mode 100644 index 0000000000..6ebd5e11e7 --- /dev/null +++ b/packages/core/src/decorators/openrpc/index.ts @@ -0,0 +1 @@ +export const decorators = {}; diff --git a/packages/core/src/detect-spec.ts b/packages/core/src/detect-spec.ts index f63b989f1a..5293288786 100644 --- a/packages/core/src/detect-spec.ts +++ b/packages/core/src/detect-spec.ts @@ -12,6 +12,7 @@ export const specVersions = [ 'async3', 'arazzo1', 'overlay1', + 'openrpc1', ] as const; export function getMajorSpecVersion(version: SpecVersion): SpecMajorVersion { @@ -25,6 +26,8 @@ export function getMajorSpecVersion(version: SpecVersion): SpecMajorVersion { return 'arazzo1'; } else if (version === 'overlay1') { return 'overlay1'; + } else if (version === 'openrpc1') { + return 'openrpc1'; } else { return 'oas3'; } @@ -35,6 +38,10 @@ export function detectSpec(root: unknown): SpecVersion { throw new Error(`Document must be JSON object, got ${typeof root}`); } + if (typeof root.openrpc === 'string' && root.openrpc.startsWith('1.')) { + return 'openrpc1'; + } + if (root.openapi && typeof root.openapi !== 'string') { throw new Error(`Invalid OpenAPI version: should be a string but got "${typeof root.openapi}"`); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2f02034320..1e7ba15dea 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -19,6 +19,7 @@ export { AsyncApi2Types } from './types/asyncapi2.js'; export { AsyncApi3Types } from './types/asyncapi3.js'; export { Arazzo1Types } from './types/arazzo.js'; export { Overlay1Types } from './types/overlay.js'; +export { OpenRpcTypes } from './types/openrpc.js'; export { ConfigTypes, createConfigTypes } from './types/redocly-yaml.js'; export { createEntityTypes } from './types/entity-yaml.js'; export { normalizeTypes, type NormalizedNodeType, type NodeType } from './types/index.js'; @@ -68,24 +69,28 @@ export { type Async3Visitor, type Arazzo1Visitor, type Overlay1Visitor, + type OpenRpc1Visitor, type Oas3Rule, type Oas2Rule, type Async2Rule, type Async3Rule, type Arazzo1Rule, type Overlay1Rule, + type OpenRpc1Rule, type Oas3Decorator, type Oas2Decorator, type Async2Decorator, type Async3Decorator, type Arazzo1Decorator, type Overlay1Decorator, + type OpenRpc1Decorator, type Oas3Preprocessor, type Oas2Preprocessor, type Async2Preprocessor, type Async3Preprocessor, type Arazzo1Preprocessor, type Overlay1Preprocessor, + type OpenRpc1Preprocessor, } from './visitors.js'; export { WalkContext, @@ -149,6 +154,7 @@ export type { export type { Oas2Definition } from './typings/swagger.js'; export type { Async3Definition } from './typings/asyncapi3.js'; export type { Async2Definition } from './typings/asyncapi.js'; +export type { OpenRpc1Definition } from './typings/openrpc.js'; export type { ArazzoDefinition, ExtendedOperation, diff --git a/packages/core/src/lint.ts b/packages/core/src/lint.ts index 47ef7f9434..524fdd80d8 100755 --- a/packages/core/src/lint.ts +++ b/packages/core/src/lint.ts @@ -27,6 +27,7 @@ import type { Oas2Visitor, Oas3Visitor, Overlay1Visitor, + OpenRpc1Visitor, RuleInstanceConfig, } from './visitors.js'; import type { CollectFn } from './utils/types.js'; @@ -167,6 +168,8 @@ export async function lintConfig(opts: { | Arazzo1Visitor[] | Overlay1Visitor | Overlay1Visitor[] + | OpenRpc1Visitor + | OpenRpc1Visitor[] >; })[] = [ { diff --git a/packages/core/src/oas-types.ts b/packages/core/src/oas-types.ts index 8ffc94ca9b..48c84d7493 100644 --- a/packages/core/src/oas-types.ts +++ b/packages/core/src/oas-types.ts @@ -6,6 +6,7 @@ import { AsyncApi2Types } from './types/asyncapi2.js'; import { AsyncApi3Types } from './types/asyncapi3.js'; import { Arazzo1Types } from './types/arazzo.js'; import { Overlay1Types } from './types/overlay.js'; +import { OpenRpcTypes } from './types/openrpc.js'; import type { BuiltInAsync2RuleId, @@ -15,6 +16,7 @@ import type { BuiltInOAS3RuleId, BuiltInOverlay1RuleId, BuiltInCommonRuleId, + BuiltInOpenRpc1RuleId, } from './types/redocly-yaml.js'; import type { Oas3Rule, @@ -29,6 +31,8 @@ import type { Arazzo1Rule, Overlay1Preprocessor, Overlay1Rule, + OpenRpc1Preprocessor, + OpenRpc1Rule, } from './visitors.js'; export const specVersions = [ @@ -40,10 +44,18 @@ export const specVersions = [ 'async3', 'arazzo1', 'overlay1', + 'openrpc1', ] as const; export type SpecVersion = typeof specVersions[number]; -export type SpecMajorVersion = 'oas2' | 'oas3' | 'async2' | 'async3' | 'arazzo1' | 'overlay1'; +export type SpecMajorVersion = + | 'oas2' + | 'oas3' + | 'async2' + | 'async3' + | 'arazzo1' + | 'overlay1' + | 'openrpc1'; const typesMap = { oas2: Oas2Types, @@ -54,6 +66,7 @@ const typesMap = { async3: AsyncApi3Types, arazzo1: Arazzo1Types, overlay1: Overlay1Types, + openrpc1: OpenRpcTypes, }; export type RuleMap = Record< @@ -91,12 +104,20 @@ export type Overlay1RuleSet = RuleMap< Overlay1Rule, T >; + +export type OpenRpc1RuleSet = RuleMap< + BuiltInOpenRpc1RuleId | BuiltInCommonRuleId | 'assertions', + OpenRpc1Rule, + T +>; + export type Oas3PreprocessorsSet = Record; export type Oas2PreprocessorsSet = Record; export type Async2PreprocessorsSet = Record; export type Async3PreprocessorsSet = Record; export type Arazzo1PreprocessorsSet = Record; export type Overlay1PreprocessorsSet = Record; +export type OpenRpc1PreprocessorsSet = Record; export type Oas3DecoratorsSet = Record; export type Oas2DecoratorsSet = Record; @@ -104,6 +125,7 @@ export type Async2DecoratorsSet = Record; export type Async3DecoratorsSet = Record; export type Arazzo1DecoratorsSet = Record; export type Overlay1DecoratorsSet = Record; +export type OpenRpc1DecoratorsSet = Record; export function getTypes(spec: SpecVersion) { return typesMap[spec]; diff --git a/packages/core/src/rules/common/struct.ts b/packages/core/src/rules/common/struct.ts index c674877e25..9e6372eb77 100644 --- a/packages/core/src/rules/common/struct.ts +++ b/packages/core/src/rules/common/struct.ts @@ -12,6 +12,7 @@ import type { Async3Rule, Arazzo1Rule, Overlay1Rule, + OpenRpc1Rule, } from '../../visitors.js'; export const Struct: @@ -20,7 +21,8 @@ export const Struct: | Async2Rule | Async3Rule | Arazzo1Rule - | Overlay1Rule = () => { + | Overlay1Rule + | OpenRpc1Rule = () => { return { any( node: any, diff --git a/packages/core/src/rules/openrpc/index.ts b/packages/core/src/rules/openrpc/index.ts new file mode 100644 index 0000000000..e01505ff01 --- /dev/null +++ b/packages/core/src/rules/openrpc/index.ts @@ -0,0 +1,24 @@ +import { Struct } from '../common/struct.js'; +import { NoUnresolvedRefs } from '../common/no-unresolved-refs.js'; +import { Assertions } from '../common/assertions/index.js'; +import { NoUnusedComponents } from './no-unused-components.js'; +import { NoDuplicatedMethodParams } from './spec-no-duplicated-method-params.js'; +import { NoRequiredParamsAfterOptional } from './spec-no-required-params-after-optional.js'; +import { InfoLicense } from '../common/info-license.js'; +import { InfoContact } from '../common/info-contact.js'; + +import type { OpenRpc1RuleSet } from '../../oas-types.js'; +import type { OpenRpc1Rule } from '../../visitors.js'; + +export const rules: OpenRpc1RuleSet<'built-in'> = { + struct: Struct as OpenRpc1Rule, + 'no-unresolved-refs': NoUnresolvedRefs as OpenRpc1Rule, + assertions: Assertions as OpenRpc1Rule, + 'info-license': InfoLicense as OpenRpc1Rule, + 'no-unused-components': NoUnusedComponents, + 'info-contact': InfoContact as OpenRpc1Rule, + 'spec-no-duplicated-method-params': NoDuplicatedMethodParams, + 'spec-no-required-params-after-optional': NoRequiredParamsAfterOptional, +}; + +export const preprocessors = {}; diff --git a/packages/core/src/rules/openrpc/no-unused-components.ts b/packages/core/src/rules/openrpc/no-unused-components.ts new file mode 100644 index 0000000000..129f431736 --- /dev/null +++ b/packages/core/src/rules/openrpc/no-unused-components.ts @@ -0,0 +1,87 @@ +import type { OpenRpc1Rule } from '../../visitors.js'; +import type { Location } from '../../ref-utils.js'; + +export const NoUnusedComponents: OpenRpc1Rule = () => { + const components = new Map(); + + function registerComponent(location: Location, name: string): void { + components.set(location.absolutePointer, { + used: components.get(location.absolutePointer)?.used || false, + location, + name, + }); + } + + return { + ref(ref, { type, resolve, key, location }) { + if ( + [ + 'Schema', + 'ContentDescriptor', + 'Example', + 'Link', + 'ErrorObject', + 'ExamplePairing', + 'Tag', + ].includes(type.name) + ) { + const resolvedRef = resolve(ref); + if (!resolvedRef.location) return; + components.set(resolvedRef.location.absolutePointer, { + used: true, + name: key.toString(), + location, + }); + } + }, + Root: { + leave(_, { report }) { + components.forEach((usageInfo) => { + if (!usageInfo.used) { + report({ + message: `Component: "${usageInfo.name}" is never used.`, + location: usageInfo.location.key(), + }); + } + }); + }, + }, + NamedSchemas: { + Schema(schema, { location, key }) { + if (!schema.allOf) { + registerComponent(location, key.toString()); + } + }, + }, + NamedContentDescriptors: { + ContentDescriptor(_node, { location, key }) { + registerComponent(location, key.toString()); + }, + }, + NamedExamples: { + Example(_node, { location, key }) { + registerComponent(location, key.toString()); + }, + }, + NamedLinks: { + Link(_node, { location, key }) { + registerComponent(location, key.toString()); + }, + }, + NamedErrors: { + ErrorObject(_node, { location, key }) { + registerComponent(location, key.toString()); + }, + }, + NamedExamplePairingObjects: { + ExamplePairing(_node, { location, key }) { + registerComponent(location, key.toString()); + }, + }, + NamedTags: { + Tag(_node, { location, key }) { + registerComponent(location, key.toString()); + }, + }, + }; +}; diff --git a/packages/core/src/rules/openrpc/spec-no-duplicated-method-params.ts b/packages/core/src/rules/openrpc/spec-no-duplicated-method-params.ts new file mode 100644 index 0000000000..a8025ff362 --- /dev/null +++ b/packages/core/src/rules/openrpc/spec-no-duplicated-method-params.ts @@ -0,0 +1,30 @@ +import type { OpenRpc1Rule } from '../../visitors.js'; + +export const NoDuplicatedMethodParams: OpenRpc1Rule = () => { + return { + Method: { + leave(method, { report, resolve, location }) { + if (!method.params || !Array.isArray(method.params)) return; + + const seenParams = new Set(); + + method.params.forEach((paramOrRef, index) => { + const resolved = resolve(paramOrRef); + if (!resolved.node) return; + + const param = resolved.node; + const paramName = param.name; + if (!paramName) return; + + if (seenParams.has(paramName)) { + report({ + message: `Parameter names must be unique. Duplicate parameter name '${param.name}' found.`, + location: location.child([index, 'name']), + }); + } + seenParams.add(paramName); + }); + }, + }, + }; +}; diff --git a/packages/core/src/rules/openrpc/spec-no-required-params-after-optional.ts b/packages/core/src/rules/openrpc/spec-no-required-params-after-optional.ts new file mode 100644 index 0000000000..9b204fc308 --- /dev/null +++ b/packages/core/src/rules/openrpc/spec-no-required-params-after-optional.ts @@ -0,0 +1,33 @@ +import type { OpenRpc1Rule } from '../../visitors.js'; + +export const NoRequiredParamsAfterOptional: OpenRpc1Rule = () => { + return { + Method: { + leave(method, { report, resolve, location }) { + if (!method.params || !Array.isArray(method.params)) return; + + let foundOptional = false; + + method.params.forEach((paramOrRef, index) => { + const resolved = resolve(paramOrRef); + if (!resolved.node) return; + + const param = resolved.node; + const paramName = param.name; + if (!paramName) return; + + const isRequired = param.required === true; + + if (!isRequired) { + foundOptional = true; + } else if (foundOptional) { + report({ + message: `Required parameter '${param.name}' must be positioned before optional parameters.`, + location: location.child([index, 'name']), + }); + } + }); + }, + }, + }; +}; diff --git a/packages/core/src/types/asyncapi2.ts b/packages/core/src/types/asyncapi2.ts index f096ed462d..835fc4ae36 100644 --- a/packages/core/src/types/asyncapi2.ts +++ b/packages/core/src/types/asyncapi2.ts @@ -1,5 +1,11 @@ import { listOf, mapOf } from './index.js'; -import { isMappingRef } from '../ref-utils.js'; +import { + Dependencies, + Discriminator, + DiscriminatorMapping, + Schema, + SchemaProperties, +} from './json-schema-draft7.shared.js'; import type { NodeType } from './index.js'; @@ -334,102 +340,6 @@ export const MessageExample: NodeType = { }, }; -export const Schema: NodeType = { - properties: { - $id: { type: 'string' }, - $schema: { type: 'string' }, - definitions: 'NamedSchemas', - externalDocs: 'ExternalDocs', - discriminator: 'Discriminator', - myArbitraryKeyword: { type: 'boolean' }, - title: { type: 'string' }, - multipleOf: { type: 'number', minimum: 0 }, - maximum: { type: 'number' }, - minimum: { type: 'number' }, - exclusiveMaximum: { type: 'number' }, - exclusiveMinimum: { type: 'number' }, - maxLength: { type: 'integer', minimum: 0 }, - minLength: { type: 'integer', minimum: 0 }, - pattern: { type: 'string' }, - maxItems: { type: 'integer', minimum: 0 }, - minItems: { type: 'integer', minimum: 0 }, - uniqueItems: { type: 'boolean' }, - maxProperties: { type: 'integer', minimum: 0 }, - minProperties: { type: 'integer', minimum: 0 }, - required: { type: 'array', items: { type: 'string' } }, - enum: { type: 'array' }, - type: (value: any) => { - return Array.isArray(value) - ? { - type: 'array', - items: { enum: ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'] }, - } - : { - enum: ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'], - }; - }, - allOf: listOf('Schema'), - anyOf: listOf('Schema'), - oneOf: listOf('Schema'), - not: 'Schema', - if: 'Schema', - then: 'Schema', - else: 'Schema', - contains: 'Schema', - patternProperties: { type: 'object' }, - propertyNames: 'Schema', - properties: 'SchemaProperties', - items: (value: any) => { - return Array.isArray(value) ? listOf('Schema') : 'Schema'; - }, - additionalProperties: (value: any) => { - return typeof value === 'boolean' ? { type: 'boolean' } : 'Schema'; - }, - description: { type: 'string' }, - format: { type: 'string' }, - contentEncoding: { type: 'string' }, - contentMediaType: { type: 'string' }, - default: null, - readOnly: { type: 'boolean' }, - writeOnly: { type: 'boolean' }, - examples: { type: 'array' }, - example: { isExample: true }, - deprecated: { type: 'boolean' }, - const: null, - $comment: { type: 'string' }, - additionalItems: (value: any) => { - return typeof value === 'boolean' ? { type: 'boolean' } : 'Schema'; - }, - dependencies: 'Dependencies', - }, -}; - -export const SchemaProperties: NodeType = { - properties: {}, - additionalProperties: (value: any) => { - return typeof value === 'boolean' ? { type: 'boolean' } : 'Schema'; - }, -}; - -export const DiscriminatorMapping: NodeType = { - properties: {}, - additionalProperties: (value: any) => { - if (isMappingRef(value)) { - return { type: 'string', directResolveAs: 'Schema' }; - } else { - return { type: 'string' }; - } - }, -}; - -export const Discriminator: NodeType = { - properties: { - propertyName: { type: 'string' }, - mapping: 'DiscriminatorMapping', - }, - required: ['propertyName'], -}; - const Components: NodeType = { properties: { messages: 'NamedMessages', @@ -557,13 +467,6 @@ const SecurityScheme: NodeType = { extensionsPrefix: 'x-', }; -export const Dependencies: NodeType = { - properties: {}, - additionalProperties: (value: any) => { - return Array.isArray(value) ? { type: 'array', items: { type: 'string' } } : 'Schema'; - }, -}; - // --- Per-protocol node types // http diff --git a/packages/core/src/types/asyncapi3.ts b/packages/core/src/types/asyncapi3.ts index ac25cbd06d..b52d688aff 100644 --- a/packages/core/src/types/asyncapi3.ts +++ b/packages/core/src/types/asyncapi3.ts @@ -1,11 +1,6 @@ import { listOf, mapOf } from './index.js'; import { AsyncApi2Bindings, - Schema, - Dependencies, - Discriminator, - DiscriminatorMapping, - SchemaProperties, CorrelationId, Tag, ServerMap, @@ -16,6 +11,13 @@ import { License, MessageExample, } from './asyncapi2.js'; +import { + Dependencies, + Discriminator, + DiscriminatorMapping, + Schema, + SchemaProperties, +} from './json-schema-draft7.shared.js'; import type { NodeType } from './index.js'; @@ -341,7 +343,6 @@ export const AsyncApi3Types: Record = { ServerMap, ExternalDocs, Root, - Channel, Parameter, Info, diff --git a/packages/core/src/types/json-schema-draft7.shared.ts b/packages/core/src/types/json-schema-draft7.shared.ts new file mode 100644 index 0000000000..ad7bd02e05 --- /dev/null +++ b/packages/core/src/types/json-schema-draft7.shared.ts @@ -0,0 +1,104 @@ +import { isMappingRef } from '../ref-utils.js'; +import { listOf, type NodeType } from './index.js'; + +export const Schema: NodeType = { + properties: { + $id: { type: 'string' }, + $schema: { type: 'string' }, + definitions: 'NamedSchemas', + externalDocs: 'ExternalDocs', + discriminator: 'Discriminator', + title: { type: 'string' }, + multipleOf: { type: 'number', minimum: 0 }, + maximum: { type: 'number' }, + minimum: { type: 'number' }, + exclusiveMaximum: { type: 'number' }, + exclusiveMinimum: { type: 'number' }, + maxLength: { type: 'integer', minimum: 0 }, + minLength: { type: 'integer', minimum: 0 }, + pattern: { type: 'string' }, + maxItems: { type: 'integer', minimum: 0 }, + minItems: { type: 'integer', minimum: 0 }, + uniqueItems: { type: 'boolean' }, + maxProperties: { type: 'integer', minimum: 0 }, + minProperties: { type: 'integer', minimum: 0 }, + required: { type: 'array', items: { type: 'string' } }, + enum: { type: 'array' }, + type: (value: any) => { + return Array.isArray(value) + ? { + type: 'array', + items: { enum: ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'] }, + } + : { + enum: ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'], + }; + }, + allOf: listOf('Schema'), + anyOf: listOf('Schema'), + oneOf: listOf('Schema'), + not: 'Schema', + if: 'Schema', + then: 'Schema', + else: 'Schema', + contains: 'Schema', + patternProperties: { type: 'object' }, + propertyNames: 'Schema', + properties: 'SchemaProperties', + items: (value: any) => { + return Array.isArray(value) ? listOf('Schema') : 'Schema'; + }, + additionalProperties: (value: any) => { + return typeof value === 'boolean' ? { type: 'boolean' } : 'Schema'; + }, + description: { type: 'string' }, + format: { type: 'string' }, + contentEncoding: { type: 'string' }, + contentMediaType: { type: 'string' }, + default: null, + readOnly: { type: 'boolean' }, + writeOnly: { type: 'boolean' }, + examples: { type: 'array' }, + example: { isExample: true }, + deprecated: { type: 'boolean' }, + const: null, + $comment: { type: 'string' }, + additionalItems: (value: any) => { + return typeof value === 'boolean' ? { type: 'boolean' } : 'Schema'; + }, + dependencies: 'Dependencies', + }, +}; + +export const SchemaProperties: NodeType = { + properties: {}, + additionalProperties: (value: any) => { + return typeof value === 'boolean' ? { type: 'boolean' } : 'Schema'; + }, +}; + +export const Dependencies: NodeType = { + properties: {}, + additionalProperties: (value: any) => { + return Array.isArray(value) ? { type: 'array', items: { type: 'string' } } : 'Schema'; + }, +}; + +export const DiscriminatorMapping: NodeType = { + properties: {}, + additionalProperties: (value: any) => { + if (isMappingRef(value)) { + return { type: 'string', directResolveAs: 'Schema' }; + } else { + return { type: 'string' }; + } + }, +}; + +export const Discriminator: NodeType = { + properties: { + propertyName: { type: 'string' }, + mapping: 'DiscriminatorMapping', + }, + required: ['propertyName'], +}; diff --git a/packages/core/src/types/oas3.ts b/packages/core/src/types/oas3.ts index 38eb7cca73..d68c200f12 100755 --- a/packages/core/src/types/oas3.ts +++ b/packages/core/src/types/oas3.ts @@ -309,6 +309,7 @@ const Link: NodeType = { extensionsPrefix: 'x-', }; +// draft-00 const Schema: NodeType = { properties: { externalDocs: 'ExternalDocs', diff --git a/packages/core/src/types/oas3_1.ts b/packages/core/src/types/oas3_1.ts index 3b96235b71..159e5a4c60 100755 --- a/packages/core/src/types/oas3_1.ts +++ b/packages/core/src/types/oas3_1.ts @@ -84,6 +84,7 @@ const Operation: NodeType = { extensionsPrefix: 'x-', }; +// draft-2020-12 const Schema: NodeType = { properties: { $id: { type: 'string' }, diff --git a/packages/core/src/types/oas3_2.ts b/packages/core/src/types/oas3_2.ts index 571a86b1cc..c0bd1cf1c5 100755 --- a/packages/core/src/types/oas3_2.ts +++ b/packages/core/src/types/oas3_2.ts @@ -211,6 +211,7 @@ const Xml: NodeType = { extensionsPrefix: 'x-', }; +// based on draft-2020-12 const Schema: NodeType = { ...Oas3_1Types.Schema, properties: { diff --git a/packages/core/src/types/openrpc.ts b/packages/core/src/types/openrpc.ts new file mode 100644 index 0000000000..2b6dc3a9b0 --- /dev/null +++ b/packages/core/src/types/openrpc.ts @@ -0,0 +1,227 @@ +import { listOf, mapOf, type NodeType } from './index.js'; +import { + Schema, + SchemaProperties, + Dependencies, + DiscriminatorMapping, + Discriminator, +} from './json-schema-draft7.shared.js'; + +const Root: NodeType = { + properties: { + openrpc: { type: 'string' }, + info: 'Info', + servers: 'ServerList', + methods: 'MethodList', + components: 'Components', + externalDocs: 'ExternalDocs', + }, + required: ['openrpc', 'info', 'methods'], + extensionsPrefix: 'x-', +}; + +const Info: NodeType = { + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + termsOfService: { type: 'string' }, + contact: 'Contact', + license: 'License', + version: { type: 'string' }, + }, + required: ['title', 'version'], + extensionsPrefix: 'x-', +}; + +const Contact: NodeType = { + properties: { + name: { type: 'string' }, + url: { type: 'string' }, + email: { type: 'string' }, + }, + extensionsPrefix: 'x-', +}; + +const License: NodeType = { + properties: { + name: { type: 'string' }, + url: { type: 'string' }, + }, + required: ['name'], + extensionsPrefix: 'x-', +}; + +const Server: NodeType = { + properties: { + url: { type: 'string' }, + name: { type: 'string' }, + description: { type: 'string' }, + summary: { type: 'string' }, + variables: 'ServerVariablesMap', + }, + required: ['url'], + extensionsPrefix: 'x-', +}; + +const ServerVariable: NodeType = { + properties: { + enum: { + type: 'array', + items: { type: 'string' }, + }, + default: { type: 'string' }, + description: { type: 'string' }, + }, + required: ['default'], + extensionsPrefix: 'x-', +}; + +const Method: NodeType = { + properties: { + name: { type: 'string' }, + tags: 'TagList', + summary: { type: 'string' }, + description: { type: 'string' }, + externalDocs: 'ExternalDocs', + params: 'ContentDescriptorList', + result: 'ContentDescriptor', + deprecated: { type: 'boolean' }, + servers: 'ServerList', + errors: 'ErrorList', + links: 'LinkList', + paramStructure: { enum: ['by-name', 'by-position', 'either'] }, + examples: 'ExamplePairingList', + }, + required: ['name', 'params'], + extensionsPrefix: 'x-', +}; + +const ContentDescriptor: NodeType = { + properties: { + name: { type: 'string' }, + summary: { type: 'string' }, + description: { type: 'string' }, + required: { type: 'boolean' }, + schema: 'Schema', + deprecated: { type: 'boolean' }, + }, + required: ['name', 'schema'], + extensionsPrefix: 'x-', +}; + +const ExamplePairing: NodeType = { + properties: { + name: { type: 'string' }, + description: { type: 'string' }, + summary: { type: 'string' }, + params: 'ExampleList', + result: 'Example', + }, + extensionsPrefix: 'x-', +}; + +const Example: NodeType = { + properties: { + name: { type: 'string' }, + summary: { type: 'string' }, + description: { type: 'string' }, + value: { resolvable: false }, + externalValue: { type: 'string' }, + }, + extensionsPrefix: 'x-', +}; + +const Link: NodeType = { + properties: { + name: { type: 'string' }, + description: { type: 'string' }, + summary: { type: 'string' }, + method: { type: 'string' }, + params: { type: 'object' }, // Map[string, Any | Runtime Expression] + server: 'Server', + }, + required: ['name'], + extensionsPrefix: 'x-', +}; + +const ErrorObject: NodeType = { + properties: { + code: { type: 'integer' }, + message: { type: 'string' }, + data: { resolvable: false }, + }, + required: ['code', 'message'], + extensionsPrefix: 'x-', +}; + +const Components: NodeType = { + properties: { + contentDescriptors: 'NamedContentDescriptors', + schemas: 'NamedSchemas', + examples: 'NamedExamples', + links: 'NamedLinks', + errors: 'NamedErrors', + examplePairingObjects: 'NamedExamplePairingObjects', + tags: 'NamedTags', + }, + extensionsPrefix: 'x-', +}; + +const Tag: NodeType = { + properties: { + name: { type: 'string' }, + summary: { type: 'string' }, + description: { type: 'string' }, + externalDocs: 'ExternalDocs', + }, + required: ['name'], + extensionsPrefix: 'x-', +}; + +const ExternalDocs: NodeType = { + properties: { + description: { type: 'string' }, + url: { type: 'string' }, + }, + required: ['url'], + extensionsPrefix: 'x-', +}; + +export const OpenRpcTypes = { + Root, + Info, + Contact, + License, + Server, + ServerList: listOf('Server'), + ServerVariable, + ServerVariablesMap: mapOf('ServerVariable'), + Method, + MethodList: listOf('Method'), + ContentDescriptor, + ContentDescriptorList: listOf('ContentDescriptor'), + ExamplePairing, + ExamplePairingList: listOf('ExamplePairing'), + Example, + ExampleList: listOf('Example'), + Link, + LinkList: listOf('Link'), + ErrorObject, + ErrorList: listOf('ErrorObject'), + Components, + Tag, + TagList: listOf('Tag'), + ExternalDocs, + Schema, + SchemaProperties, + Dependencies, + DiscriminatorMapping, + Discriminator, + NamedContentDescriptors: mapOf('ContentDescriptor'), + NamedSchemas: mapOf('Schema'), + NamedExamples: mapOf('Example'), + NamedLinks: mapOf('Link'), + NamedErrors: mapOf('ErrorObject'), + NamedExamplePairingObjects: mapOf('ExamplePairing'), + NamedTags: mapOf('Tag'), +} as const; diff --git a/packages/core/src/types/redocly-yaml.ts b/packages/core/src/types/redocly-yaml.ts index 2ab6fb4ab4..e538e4604e 100644 --- a/packages/core/src/types/redocly-yaml.ts +++ b/packages/core/src/types/redocly-yaml.ts @@ -184,6 +184,16 @@ const builtInOverlay1Rules = ['info-contact'] as const; export type BuiltInOverlay1RuleId = typeof builtInOverlay1Rules[number]; +const builtInOpenRpc1Rules = [ + 'info-contact', + 'info-license', + 'no-unused-components', + 'spec-no-duplicated-method-params', + 'spec-no-required-params-after-optional', +] as const; + +export type BuiltInOpenRpc1RuleId = typeof builtInOpenRpc1Rules[number]; + const builtInCommonRules = ['struct', 'no-unresolved-refs'] as const; export type BuiltInCommonRuleId = typeof builtInCommonRules[number]; @@ -195,6 +205,7 @@ const builtInRules = [ ...builtInAsync3Rules, ...builtInArazzo1Rules, ...builtInOverlay1Rules, + ...builtInOpenRpc1Rules, ...builtInCommonRules, ] as const; @@ -229,6 +240,7 @@ const configGovernanceProperties: Record< async3Rules: 'Rules', arazzo1Rules: 'Rules', overlay1Rules: 'Rules', + openrpc1Rules: 'Rules', preprocessors: { type: 'object' }, oas2Preprocessors: { type: 'object' }, oas3_0Preprocessors: { type: 'object' }, @@ -238,6 +250,7 @@ const configGovernanceProperties: Record< async3Preprocessors: { type: 'object' }, arazzo1Preprocessors: { type: 'object' }, overlay1Preprocessors: { type: 'object' }, + openrpc1Preprocessors: { type: 'object' }, decorators: { type: 'object' }, oas2Decorators: { type: 'object' }, oas3_0Decorators: { type: 'object' }, @@ -247,6 +260,7 @@ const configGovernanceProperties: Record< async3Decorators: { type: 'object' }, arazzo1Decorators: { type: 'object' }, overlay1Decorators: { type: 'object' }, + openrpc1Decorators: { type: 'object' }, }; const ConfigGovernance: NodeType = { diff --git a/packages/core/src/typings/openrpc.ts b/packages/core/src/typings/openrpc.ts new file mode 100644 index 0000000000..5005a11b30 --- /dev/null +++ b/packages/core/src/typings/openrpc.ts @@ -0,0 +1,124 @@ +export interface OpenRpc1Definition { + openrpc: string; + info: OpenRpc1Info; + servers?: OpenRpc1Server[]; + methods: (OpenRpc1Method | OpenRpc1Reference)[]; + components?: OpenRpc1Components; + externalDocs?: OpenRpc1ExternalDocs; +} + +export interface OpenRpc1Info { + title: string; + description?: string; + termsOfService?: string; + contact?: OpenRpc1Contact; + license?: OpenRpc1License; + version: string; +} + +export interface OpenRpc1Contact { + name?: string; + url?: string; + email?: string; +} + +export interface OpenRpc1License { + name: string; + url?: string; +} + +export interface OpenRpc1Server { + url: string; + name?: string; + description?: string; + summary?: string; + variables?: Record; +} + +export interface OpenRpc1ServerVariable { + enum?: string[]; + default: string; + description?: string; +} + +export interface OpenRpc1Method { + name: string; + tags?: (OpenRpc1Tag | OpenRpc1Reference)[]; + summary?: string; + description?: string; + externalDocs?: OpenRpc1ExternalDocs; + params: (OpenRpc1ContentDescriptor | OpenRpc1Reference)[]; + result?: OpenRpc1ContentDescriptor | OpenRpc1Reference; + deprecated?: boolean; + servers?: OpenRpc1Server[]; + errors?: (OpenRpc1Error | OpenRpc1Reference)[]; + links?: (OpenRpc1Link | OpenRpc1Reference)[]; + paramStructure?: 'by-name' | 'by-position' | 'either'; + examples?: (OpenRpc1ExamplePairing | OpenRpc1Reference)[]; +} + +export interface OpenRpc1ContentDescriptor { + name: string; + summary?: string; + description?: string; + required?: boolean; + schema: any; // Schema + deprecated?: boolean; +} + +export interface OpenRpc1ExamplePairing { + name?: string; + description?: string; + summary?: string; + params: (OpenRpc1Example | OpenRpc1Reference)[]; + result?: OpenRpc1Example | OpenRpc1Reference; +} + +export interface OpenRpc1Example { + name?: string; + summary?: string; + description?: string; + value?: any; + externalValue?: string; +} + +export interface OpenRpc1Link { + name: string; + description?: string; + summary?: string; + method?: string; + params?: any; + server?: OpenRpc1Server; +} + +export interface OpenRpc1Error { + code: number; + message: string; + data?: any; +} + +export interface OpenRpc1Components { + contentDescriptors?: Record; + schemas?: Record; + examples?: Record; + links?: Record; + errors?: Record; + examplePairingObjects?: Record; + tags?: Record; +} + +export interface OpenRpc1Tag { + name: string; + summary?: string; + description?: string; + externalDocs?: OpenRpc1ExternalDocs; +} + +export interface OpenRpc1ExternalDocs { + description?: string; + url: string; +} + +export interface OpenRpc1Reference { + $ref: string; +} diff --git a/packages/core/src/visitors.ts b/packages/core/src/visitors.ts index 56b4f1a834..3ccdd0feb8 100644 --- a/packages/core/src/visitors.ts +++ b/packages/core/src/visitors.ts @@ -75,6 +75,23 @@ import type { Workflow, } from './typings/arazzo.js'; import type { Overlay1Definition } from './typings/overlay.js'; +import type { + OpenRpc1Definition, + OpenRpc1Info, + OpenRpc1Server, + OpenRpc1ServerVariable, + OpenRpc1Method, + OpenRpc1ContentDescriptor, + OpenRpc1ExamplePairing, + OpenRpc1Example, + OpenRpc1Link, + OpenRpc1Error, + OpenRpc1Components, + OpenRpc1Tag, + OpenRpc1ExternalDocs, + OpenRpc1Contact, + OpenRpc1License, +} from './typings/openrpc.js'; export type SkipFunctionContext = Pick< UserContext, @@ -272,6 +289,33 @@ type Overlay1FlatVisitor = { Root?: VisitFunctionOrObject; }; +type OpenRpc1FlatVisitor = { + Root?: VisitFunctionOrObject; + Info?: VisitFunctionOrObject; + Contact?: VisitFunctionOrObject; + License?: VisitFunctionOrObject; + Server?: VisitFunctionOrObject; + ServerVariable?: VisitFunctionOrObject; + Method?: VisitFunctionOrObject; + ContentDescriptor?: VisitFunctionOrObject; + ExamplePairing?: VisitFunctionOrObject; + Example?: VisitFunctionOrObject; + Link?: VisitFunctionOrObject; + ErrorObject?: VisitFunctionOrObject; + Components?: VisitFunctionOrObject; + Tag?: VisitFunctionOrObject; + ExternalDocs?: VisitFunctionOrObject; + Schema?: VisitFunctionOrObject; // generic schema + SchemaProperties?: VisitFunctionOrObject>; + NamedContentDescriptors?: VisitFunctionOrObject>; + NamedSchemas?: VisitFunctionOrObject>; + NamedExamples?: VisitFunctionOrObject>; + NamedLinks?: VisitFunctionOrObject>; + NamedErrors?: VisitFunctionOrObject>; + NamedExamplePairingObjects?: VisitFunctionOrObject>; + NamedTags?: VisitFunctionOrObject>; +}; + const legacyTypesMap = { Root: 'DefinitionRoot', ServerVariablesMap: 'ServerVariableMap', @@ -328,6 +372,13 @@ type Overlay1NestedVisitor = { : Overlay1FlatVisitor[T] & NestedVisitor; }; +type OpenRpc1NestedVisitor = { + // eslint-disable-next-line @typescript-eslint/ban-types + [T in keyof OpenRpc1FlatVisitor]: OpenRpc1FlatVisitor[T] extends Function + ? OpenRpc1FlatVisitor[T] + : OpenRpc1FlatVisitor[T] & NestedVisitor; +}; + export type Oas3Visitor = BaseVisitor & Oas3NestedVisitor & Record | NestedVisitObject>; @@ -352,6 +403,10 @@ export type Overlay1Visitor = BaseVisitor & Overlay1NestedVisitor & Record | NestedVisitObject>; +export type OpenRpc1Visitor = BaseVisitor & + OpenRpc1NestedVisitor & + Record | NestedVisitObject>; + export type CatalogEntityVisitor = BaseVisitor & Record>; export type NestedVisitor = Exclude; @@ -379,6 +434,7 @@ export type Async2Rule = (options: Record) => Async2Visitor | Async export type Async3Rule = (options: Record) => Async3Visitor | Async3Visitor[]; export type Arazzo1Rule = (options: Record) => Arazzo1Visitor | Arazzo1Visitor[]; export type Overlay1Rule = (options: Record) => Overlay1Visitor | Overlay1Visitor[]; +export type OpenRpc1Rule = (options: Record) => OpenRpc1Visitor | OpenRpc1Visitor[]; export type CatalogEntityRule = ( options: Record ) => CatalogEntityVisitor | CatalogEntityVisitor[]; @@ -388,12 +444,14 @@ export type Async2Preprocessor = (options: Record) => Async2Visitor export type Async3Preprocessor = (options: Record) => Async3Visitor; export type Arazzo1Preprocessor = (options: Record) => Arazzo1Visitor; export type Overlay1Preprocessor = (options: Record) => Overlay1Visitor; +export type OpenRpc1Preprocessor = (options: Record) => OpenRpc1Visitor; export type Oas3Decorator = (options: Record) => Oas3Visitor; export type Oas2Decorator = (options: Record) => Oas2Visitor; export type Async2Decorator = (options: Record) => Async2Visitor; export type Async3Decorator = (options: Record) => Async3Visitor; export type Arazzo1Decorator = (options: Record) => Arazzo1Visitor; export type Overlay1Decorator = (options: Record) => Overlay1Visitor; +export type OpenRpc1Decorator = (options: Record) => OpenRpc1Visitor; // alias for the latest version supported // every time we update it - consider semver diff --git a/tests/e2e/check-config/wrong-config-type-extensions-in-assertions/snapshot.txt b/tests/e2e/check-config/wrong-config-type-extensions-in-assertions/snapshot.txt index 35ef60f437..9791d9c6ad 100644 --- a/tests/e2e/check-config/wrong-config-type-extensions-in-assertions/snapshot.txt +++ b/tests/e2e/check-config/wrong-config-type-extensions-in-assertions/snapshot.txt @@ -1,6 +1,6 @@ [1] redocly.yaml:10:13 at #/rules/rule~1metadata-lifecycle/subject/type -`type` can be one of the following only: "any", "Root", "Tag", "TagList", "TagGroups", "TagGroup", "ExternalDocs", "Example", "ExamplesMap", "EnumDescriptions", "SecurityRequirement", "SecurityRequirementList", "Info", "Contact", "License", "Logo", "Paths", "PathItem", "Parameter", "ParameterItems", "ParameterList", "Operation", "Examples", "Header", "Responses", "Response", "Schema", "Xml", "SchemaProperties", "NamedSchemas", "NamedResponses", "NamedParameters", "NamedSecuritySchemes", "SecurityScheme", "XCodeSample", "XCodeSampleList", "XServerList", "XServer", "Server", "ServerList", "ServerVariable", "ServerVariablesMap", "Callback", "CallbacksMap", "RequestBody", "MediaTypesMap", "MediaType", "Encoding", "EncodingMap", "HeadersMap", "Link", "DiscriminatorMapping", "Discriminator", "Components", "LinksMap", "NamedExamples", "NamedRequestBodies", "NamedHeaders", "NamedLinks", "NamedCallbacks", "ImplicitFlow", "PasswordFlow", "ClientCredentials", "AuthorizationCode", "OAuth2Flows", "XUsePkce", "WebhooksMap", "XMetaData", "PatternProperties", "NamedPathItems", "DependentRequired", "DeviceAuthorization", "HttpServerBinding", "HttpChannelBinding", "HttpMessageBinding", "HttpOperationBinding", "WsServerBinding", "WsChannelBinding", "WsMessageBinding", "WsOperationBinding", "KafkaServerBinding", "KafkaTopicConfiguration", "KafkaChannelBinding", "KafkaMessageBinding", "KafkaOperationBinding", "AnypointmqServerBinding", "AnypointmqChannelBinding", "AnypointmqMessageBinding", "AnypointmqOperationBinding", "AmqpServerBinding", "AmqpChannelBinding", "AmqpMessageBinding", "AmqpOperationBinding", "Amqp1ServerBinding", "Amqp1ChannelBinding", "Amqp1MessageBinding", "Amqp1OperationBinding", "MqttServerBindingLastWill", "MqttServerBinding", "MqttChannelBinding", "MqttMessageBinding", "MqttOperationBinding", "Mqtt5ServerBinding", "Mqtt5ChannelBinding", "Mqtt5MessageBinding", "Mqtt5OperationBinding", "NatsServerBinding", "NatsChannelBinding", "NatsMessageBinding", "NatsOperationBinding", "JmsServerBinding", "JmsChannelBinding", "JmsMessageBinding", "JmsOperationBinding", "SolaceServerBinding", "SolaceChannelBinding", "SolaceMessageBinding", "SolaceDestination", "SolaceOperationBinding", "StompServerBinding", "StompChannelBinding", "StompMessageBinding", "StompOperationBinding", "RedisServerBinding", "RedisChannelBinding", "RedisMessageBinding", "RedisOperationBinding", "MercureServerBinding", "MercureChannelBinding", "MercureMessageBinding", "MercureOperationBinding", "ServerBindings", "ChannelBindings", "MessageBindings", "OperationBindings", "ServerMap", "ChannelMap", "Channel", "ParametersMap", "MessageExample", "NamedMessages", "NamedMessageTraits", "NamedOperationTraits", "NamedCorrelationIds", "SecuritySchemeFlows", "Message", "OperationTrait", "OperationTraitList", "MessageTrait", "MessageTraitList", "MessageExampleList", "CorrelationId", "Dependencies", "OperationReply", "OperationReplyAddress", "NamedTags", "NamedExternalDocs", "NamedChannels", "NamedOperations", "NamedOperationReplies", "NamedOperationRelyAddresses", "SecuritySchemeList", "MessageList", "SourceDescriptions", "OpenAPISourceDescription", "ArazzoSourceDescription", "Parameters", "ReusableObject", "Workflows", "Workflow", "Steps", "Step", "Replacement", "ExtendedOperation", "ExtendedSecurityList", "ExtendedSecurity", "Outputs", "CriterionObject", "XPathCriterion", "JSONPathCriterion", "SuccessActionObject", "OnSuccessActionList", "FailureActionObject", "OnFailureActionList", "NamedInputs", "NamedSuccessActions", "NamedFailureActions", "Actions", "Action", "SpecExtension". +`type` can be one of the following only: "any", "Root", "Tag", "TagList", "TagGroups", "TagGroup", "ExternalDocs", "Example", "ExamplesMap", "EnumDescriptions", "SecurityRequirement", "SecurityRequirementList", "Info", "Contact", "License", "Logo", "Paths", "PathItem", "Parameter", "ParameterItems", "ParameterList", "Operation", "Examples", "Header", "Responses", "Response", "Schema", "Xml", "SchemaProperties", "NamedSchemas", "NamedResponses", "NamedParameters", "NamedSecuritySchemes", "SecurityScheme", "XCodeSample", "XCodeSampleList", "XServerList", "XServer", "Server", "ServerList", "ServerVariable", "ServerVariablesMap", "Callback", "CallbacksMap", "RequestBody", "MediaTypesMap", "MediaType", "Encoding", "EncodingMap", "HeadersMap", "Link", "DiscriminatorMapping", "Discriminator", "Components", "LinksMap", "NamedExamples", "NamedRequestBodies", "NamedHeaders", "NamedLinks", "NamedCallbacks", "ImplicitFlow", "PasswordFlow", "ClientCredentials", "AuthorizationCode", "OAuth2Flows", "XUsePkce", "WebhooksMap", "XMetaData", "PatternProperties", "NamedPathItems", "DependentRequired", "DeviceAuthorization", "HttpServerBinding", "HttpChannelBinding", "HttpMessageBinding", "HttpOperationBinding", "WsServerBinding", "WsChannelBinding", "WsMessageBinding", "WsOperationBinding", "KafkaServerBinding", "KafkaTopicConfiguration", "KafkaChannelBinding", "KafkaMessageBinding", "KafkaOperationBinding", "AnypointmqServerBinding", "AnypointmqChannelBinding", "AnypointmqMessageBinding", "AnypointmqOperationBinding", "AmqpServerBinding", "AmqpChannelBinding", "AmqpMessageBinding", "AmqpOperationBinding", "Amqp1ServerBinding", "Amqp1ChannelBinding", "Amqp1MessageBinding", "Amqp1OperationBinding", "MqttServerBindingLastWill", "MqttServerBinding", "MqttChannelBinding", "MqttMessageBinding", "MqttOperationBinding", "Mqtt5ServerBinding", "Mqtt5ChannelBinding", "Mqtt5MessageBinding", "Mqtt5OperationBinding", "NatsServerBinding", "NatsChannelBinding", "NatsMessageBinding", "NatsOperationBinding", "JmsServerBinding", "JmsChannelBinding", "JmsMessageBinding", "JmsOperationBinding", "SolaceServerBinding", "SolaceChannelBinding", "SolaceMessageBinding", "SolaceDestination", "SolaceOperationBinding", "StompServerBinding", "StompChannelBinding", "StompMessageBinding", "StompOperationBinding", "RedisServerBinding", "RedisChannelBinding", "RedisMessageBinding", "RedisOperationBinding", "MercureServerBinding", "MercureChannelBinding", "MercureMessageBinding", "MercureOperationBinding", "ServerBindings", "ChannelBindings", "MessageBindings", "OperationBindings", "ServerMap", "ChannelMap", "Channel", "ParametersMap", "MessageExample", "NamedMessages", "NamedMessageTraits", "NamedOperationTraits", "NamedCorrelationIds", "SecuritySchemeFlows", "Message", "OperationTrait", "OperationTraitList", "MessageTrait", "MessageTraitList", "MessageExampleList", "CorrelationId", "Dependencies", "OperationReply", "OperationReplyAddress", "NamedTags", "NamedExternalDocs", "NamedChannels", "NamedOperations", "NamedOperationReplies", "NamedOperationRelyAddresses", "SecuritySchemeList", "MessageList", "SourceDescriptions", "OpenAPISourceDescription", "ArazzoSourceDescription", "Parameters", "ReusableObject", "Workflows", "Workflow", "Steps", "Step", "Replacement", "ExtendedOperation", "ExtendedSecurityList", "ExtendedSecurity", "Outputs", "CriterionObject", "XPathCriterion", "JSONPathCriterion", "SuccessActionObject", "OnSuccessActionList", "FailureActionObject", "OnFailureActionList", "NamedInputs", "NamedSuccessActions", "NamedFailureActions", "Actions", "Action", "Method", "MethodList", "ContentDescriptor", "ContentDescriptorList", "ExamplePairing", "ExamplePairingList", "ExampleList", "LinkList", "ErrorObject", "ErrorList", "NamedContentDescriptors", "NamedErrors", "NamedExamplePairingObjects", "SpecExtension". 8 | rule/metadata-lifecycle: 9 | subject: diff --git a/tests/e2e/lint-config/invalid-config-assertation-config-type/snapshot.txt b/tests/e2e/lint-config/invalid-config-assertation-config-type/snapshot.txt index c8cc0ca233..d77a812ce6 100644 --- a/tests/e2e/lint-config/invalid-config-assertation-config-type/snapshot.txt +++ b/tests/e2e/lint-config/invalid-config-assertation-config-type/snapshot.txt @@ -1,6 +1,6 @@ [1] redocly.yaml:9:17 at #/rules/rule~1path-item-mutually-required/where/0/subject/type -`type` can be one of the following only: "any", "Root", "Tag", "TagList", "TagGroups", "TagGroup", "ExternalDocs", "Example", "ExamplesMap", "EnumDescriptions", "SecurityRequirement", "SecurityRequirementList", "Info", "Contact", "License", "Logo", "Paths", "PathItem", "Parameter", "ParameterItems", "ParameterList", "Operation", "Examples", "Header", "Responses", "Response", "Schema", "Xml", "SchemaProperties", "NamedSchemas", "NamedResponses", "NamedParameters", "NamedSecuritySchemes", "SecurityScheme", "XCodeSample", "XCodeSampleList", "XServerList", "XServer", "Server", "ServerList", "ServerVariable", "ServerVariablesMap", "Callback", "CallbacksMap", "RequestBody", "MediaTypesMap", "MediaType", "Encoding", "EncodingMap", "HeadersMap", "Link", "DiscriminatorMapping", "Discriminator", "Components", "LinksMap", "NamedExamples", "NamedRequestBodies", "NamedHeaders", "NamedLinks", "NamedCallbacks", "ImplicitFlow", "PasswordFlow", "ClientCredentials", "AuthorizationCode", "OAuth2Flows", "XUsePkce", "WebhooksMap", "PatternProperties", "NamedPathItems", "DependentRequired", "DeviceAuthorization", "HttpServerBinding", "HttpChannelBinding", "HttpMessageBinding", "HttpOperationBinding", "WsServerBinding", "WsChannelBinding", "WsMessageBinding", "WsOperationBinding", "KafkaServerBinding", "KafkaTopicConfiguration", "KafkaChannelBinding", "KafkaMessageBinding", "KafkaOperationBinding", "AnypointmqServerBinding", "AnypointmqChannelBinding", "AnypointmqMessageBinding", "AnypointmqOperationBinding", "AmqpServerBinding", "AmqpChannelBinding", "AmqpMessageBinding", "AmqpOperationBinding", "Amqp1ServerBinding", "Amqp1ChannelBinding", "Amqp1MessageBinding", "Amqp1OperationBinding", "MqttServerBindingLastWill", "MqttServerBinding", "MqttChannelBinding", "MqttMessageBinding", "MqttOperationBinding", "Mqtt5ServerBinding", "Mqtt5ChannelBinding", "Mqtt5MessageBinding", "Mqtt5OperationBinding", "NatsServerBinding", "NatsChannelBinding", "NatsMessageBinding", "NatsOperationBinding", "JmsServerBinding", "JmsChannelBinding", "JmsMessageBinding", "JmsOperationBinding", "SolaceServerBinding", "SolaceChannelBinding", "SolaceMessageBinding", "SolaceDestination", "SolaceOperationBinding", "StompServerBinding", "StompChannelBinding", "StompMessageBinding", "StompOperationBinding", "RedisServerBinding", "RedisChannelBinding", "RedisMessageBinding", "RedisOperationBinding", "MercureServerBinding", "MercureChannelBinding", "MercureMessageBinding", "MercureOperationBinding", "ServerBindings", "ChannelBindings", "MessageBindings", "OperationBindings", "ServerMap", "ChannelMap", "Channel", "ParametersMap", "MessageExample", "NamedMessages", "NamedMessageTraits", "NamedOperationTraits", "NamedCorrelationIds", "SecuritySchemeFlows", "Message", "OperationTrait", "OperationTraitList", "MessageTrait", "MessageTraitList", "MessageExampleList", "CorrelationId", "Dependencies", "OperationReply", "OperationReplyAddress", "NamedTags", "NamedExternalDocs", "NamedChannels", "NamedOperations", "NamedOperationReplies", "NamedOperationRelyAddresses", "SecuritySchemeList", "MessageList", "SourceDescriptions", "OpenAPISourceDescription", "ArazzoSourceDescription", "Parameters", "ReusableObject", "Workflows", "Workflow", "Steps", "Step", "Replacement", "ExtendedOperation", "ExtendedSecurityList", "ExtendedSecurity", "Outputs", "CriterionObject", "XPathCriterion", "JSONPathCriterion", "SuccessActionObject", "OnSuccessActionList", "FailureActionObject", "OnFailureActionList", "NamedInputs", "NamedSuccessActions", "NamedFailureActions", "Actions", "Action", "SpecExtension". +`type` can be one of the following only: "any", "Root", "Tag", "TagList", "TagGroups", "TagGroup", "ExternalDocs", "Example", "ExamplesMap", "EnumDescriptions", "SecurityRequirement", "SecurityRequirementList", "Info", "Contact", "License", "Logo", "Paths", "PathItem", "Parameter", "ParameterItems", "ParameterList", "Operation", "Examples", "Header", "Responses", "Response", "Schema", "Xml", "SchemaProperties", "NamedSchemas", "NamedResponses", "NamedParameters", "NamedSecuritySchemes", "SecurityScheme", "XCodeSample", "XCodeSampleList", "XServerList", "XServer", "Server", "ServerList", "ServerVariable", "ServerVariablesMap", "Callback", "CallbacksMap", "RequestBody", "MediaTypesMap", "MediaType", "Encoding", "EncodingMap", "HeadersMap", "Link", "DiscriminatorMapping", "Discriminator", "Components", "LinksMap", "NamedExamples", "NamedRequestBodies", "NamedHeaders", "NamedLinks", "NamedCallbacks", "ImplicitFlow", "PasswordFlow", "ClientCredentials", "AuthorizationCode", "OAuth2Flows", "XUsePkce", "WebhooksMap", "PatternProperties", "NamedPathItems", "DependentRequired", "DeviceAuthorization", "HttpServerBinding", "HttpChannelBinding", "HttpMessageBinding", "HttpOperationBinding", "WsServerBinding", "WsChannelBinding", "WsMessageBinding", "WsOperationBinding", "KafkaServerBinding", "KafkaTopicConfiguration", "KafkaChannelBinding", "KafkaMessageBinding", "KafkaOperationBinding", "AnypointmqServerBinding", "AnypointmqChannelBinding", "AnypointmqMessageBinding", "AnypointmqOperationBinding", "AmqpServerBinding", "AmqpChannelBinding", "AmqpMessageBinding", "AmqpOperationBinding", "Amqp1ServerBinding", "Amqp1ChannelBinding", "Amqp1MessageBinding", "Amqp1OperationBinding", "MqttServerBindingLastWill", "MqttServerBinding", "MqttChannelBinding", "MqttMessageBinding", "MqttOperationBinding", "Mqtt5ServerBinding", "Mqtt5ChannelBinding", "Mqtt5MessageBinding", "Mqtt5OperationBinding", "NatsServerBinding", "NatsChannelBinding", "NatsMessageBinding", "NatsOperationBinding", "JmsServerBinding", "JmsChannelBinding", "JmsMessageBinding", "JmsOperationBinding", "SolaceServerBinding", "SolaceChannelBinding", "SolaceMessageBinding", "SolaceDestination", "SolaceOperationBinding", "StompServerBinding", "StompChannelBinding", "StompMessageBinding", "StompOperationBinding", "RedisServerBinding", "RedisChannelBinding", "RedisMessageBinding", "RedisOperationBinding", "MercureServerBinding", "MercureChannelBinding", "MercureMessageBinding", "MercureOperationBinding", "ServerBindings", "ChannelBindings", "MessageBindings", "OperationBindings", "ServerMap", "ChannelMap", "Channel", "ParametersMap", "MessageExample", "NamedMessages", "NamedMessageTraits", "NamedOperationTraits", "NamedCorrelationIds", "SecuritySchemeFlows", "Message", "OperationTrait", "OperationTraitList", "MessageTrait", "MessageTraitList", "MessageExampleList", "CorrelationId", "Dependencies", "OperationReply", "OperationReplyAddress", "NamedTags", "NamedExternalDocs", "NamedChannels", "NamedOperations", "NamedOperationReplies", "NamedOperationRelyAddresses", "SecuritySchemeList", "MessageList", "SourceDescriptions", "OpenAPISourceDescription", "ArazzoSourceDescription", "Parameters", "ReusableObject", "Workflows", "Workflow", "Steps", "Step", "Replacement", "ExtendedOperation", "ExtendedSecurityList", "ExtendedSecurity", "Outputs", "CriterionObject", "XPathCriterion", "JSONPathCriterion", "SuccessActionObject", "OnSuccessActionList", "FailureActionObject", "OnFailureActionList", "NamedInputs", "NamedSuccessActions", "NamedFailureActions", "Actions", "Action", "Method", "MethodList", "ContentDescriptor", "ContentDescriptorList", "ExamplePairing", "ExamplePairingList", "ExampleList", "LinkList", "ErrorObject", "ErrorList", "NamedContentDescriptors", "NamedErrors", "NamedExamplePairingObjects", "SpecExtension". 7 | where: 8 | - subject: diff --git a/tests/e2e/lint/openrpc-invalid/openrpc.json b/tests/e2e/lint/openrpc-invalid/openrpc.json new file mode 100644 index 0000000000..f52d3bbe45 --- /dev/null +++ b/tests/e2e/lint/openrpc-invalid/openrpc.json @@ -0,0 +1,7 @@ +{ + "openrpc": "1.2.6", + "info": { + "title": "Petstore" + }, + "methods": [] +} diff --git a/tests/e2e/lint/openrpc-invalid/redocly.yaml b/tests/e2e/lint/openrpc-invalid/redocly.yaml new file mode 100644 index 0000000000..7adc852802 --- /dev/null +++ b/tests/e2e/lint/openrpc-invalid/redocly.yaml @@ -0,0 +1,6 @@ +apis: + main: + root: ./openrpc.json + +extends: + - recommended diff --git a/tests/e2e/lint/openrpc-invalid/snapshot.txt b/tests/e2e/lint/openrpc-invalid/snapshot.txt new file mode 100644 index 0000000000..e9b665018c --- /dev/null +++ b/tests/e2e/lint/openrpc-invalid/snapshot.txt @@ -0,0 +1,35 @@ +[1] openrpc.json:3:3 at #/info + +The field `version` must be present on this level. + +1 | { +2 | "openrpc": "1.2.6", +3 | "info": { + | ^^^^^^ +4 | "title": "Petstore" +5 | }, + +Error was generated by the struct rule. + + +[2] openrpc.json:3:3 at #/info + +Info object should contain `license` field. + +1 | { +2 | "openrpc": "1.2.6", +3 | "info": { + | ^^^^^^ +4 | "title": "Petstore" +5 | }, + +Warning was generated by the info-license rule. + + + +validating openrpc.json using lint rules for api 'main'... +openrpc.json: validated in ms + +❌ Validation failed with 1 error and 1 warning. +run `redocly lint --generate-ignore-file` to add all problems to the ignore file. + diff --git a/tests/e2e/lint/openrpc-valid/openrpc.json b/tests/e2e/lint/openrpc-valid/openrpc.json new file mode 100644 index 0000000000..7f9756534a --- /dev/null +++ b/tests/e2e/lint/openrpc-valid/openrpc.json @@ -0,0 +1,32 @@ +{ + "openrpc": "1.2.6", + "info": { + "title": "Petstore", + "version": "1.0.0" + }, + "methods": [ + { + "name": "listPets", + "params": [], + "result": { + "name": "pets", + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/Pet" } + } + } + } + ], + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" } + }, + "required": ["id", "name"] + } + } + } +} diff --git a/tests/e2e/lint/openrpc-valid/redocly.yaml b/tests/e2e/lint/openrpc-valid/redocly.yaml new file mode 100644 index 0000000000..7adc852802 --- /dev/null +++ b/tests/e2e/lint/openrpc-valid/redocly.yaml @@ -0,0 +1,6 @@ +apis: + main: + root: ./openrpc.json + +extends: + - recommended diff --git a/tests/e2e/lint/openrpc-valid/snapshot.txt b/tests/e2e/lint/openrpc-valid/snapshot.txt new file mode 100644 index 0000000000..6626a7e352 --- /dev/null +++ b/tests/e2e/lint/openrpc-valid/snapshot.txt @@ -0,0 +1,21 @@ +[1] openrpc.json:3:3 at #/info + +Info object should contain `license` field. + +1 | { +2 | "openrpc": "1.2.6", +3 | "info": { + | ^^^^^^ +4 | "title": "Petstore", +5 | "version": "" + +Warning was generated by the info-license rule. + + + +validating openrpc.json using lint rules for api 'main'... +openrpc.json: validated in ms + +Woohoo! Your API description is valid. 🎉 +You have 1 warning. +