From 5388145bcbe74a716a5247af6e3e50c1b2c11de0 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Tue, 28 Nov 2023 13:07:22 +0100 Subject: [PATCH] feat(ls): add initial support for OpenAPI 2.0 Resf #3103 --- package-lock.json | 3 ++ packages/apidom-ls/package.json | 3 ++ packages/apidom-ls/src/config/config.ts | 12 +++--- .../apidom-ls/src/config/openapi/config.ts | 2 + .../src/config/openapi/swagger/completion.ts | 5 +++ .../config/openapi/swagger/documentation.ts | 5 +++ .../openapi/swagger/lint/allowed-fields.ts | 36 ++++++++++++++++++ .../src/config/openapi/swagger/lint/index.ts | 5 +++ .../src/config/openapi/swagger/meta.ts | 12 ++++++ packages/apidom-ls/src/parser-factory.ts | 21 +++++++++++ packages/apidom-ls/src/utils/utils.ts | 27 ++++++++++++++ .../fixtures/sample-api-openapi-yaml-2-0.yaml | 2 + packages/apidom-ls/test/validate.ts | 37 +++++++++++++++++++ 13 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 packages/apidom-ls/src/config/openapi/swagger/completion.ts create mode 100644 packages/apidom-ls/src/config/openapi/swagger/documentation.ts create mode 100644 packages/apidom-ls/src/config/openapi/swagger/lint/allowed-fields.ts create mode 100644 packages/apidom-ls/src/config/openapi/swagger/lint/index.ts create mode 100644 packages/apidom-ls/src/config/openapi/swagger/meta.ts create mode 100644 packages/apidom-ls/test/fixtures/sample-api-openapi-yaml-2-0.yaml diff --git a/package-lock.json b/package-lock.json index 53066aee7f..5e0db1fe9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33926,6 +33926,7 @@ "@swagger-api/apidom-json-pointer": "^0.84.0", "@swagger-api/apidom-ns-api-design-systems": "^0.84.0", "@swagger-api/apidom-ns-asyncapi-2": "^0.84.0", + "@swagger-api/apidom-ns-openapi-2": "^0.84.0", "@swagger-api/apidom-ns-openapi-3-0": "^0.84.0", "@swagger-api/apidom-ns-openapi-3-1": "^0.84.0", "@swagger-api/apidom-parser": "^0.84.0", @@ -33934,8 +33935,10 @@ "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^0.84.0", "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^0.84.0", "@swagger-api/apidom-parser-adapter-json": "^0.84.0", + "@swagger-api/apidom-parser-adapter-openapi-json-2": "^0.84.0", "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^0.84.0", "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^0.84.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^0.84.0", "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^0.84.0", "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^0.84.0", "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.84.0", diff --git a/packages/apidom-ls/package.json b/packages/apidom-ls/package.json index 0d531ce2d6..91b5cee7fc 100644 --- a/packages/apidom-ls/package.json +++ b/packages/apidom-ls/package.json @@ -98,6 +98,7 @@ "@swagger-api/apidom-json-pointer": "^0.84.0", "@swagger-api/apidom-ns-api-design-systems": "^0.84.0", "@swagger-api/apidom-ns-asyncapi-2": "^0.84.0", + "@swagger-api/apidom-ns-openapi-2": "^0.84.0", "@swagger-api/apidom-ns-openapi-3-0": "^0.84.0", "@swagger-api/apidom-ns-openapi-3-1": "^0.84.0", "@swagger-api/apidom-parser": "^0.84.0", @@ -106,6 +107,8 @@ "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^0.84.0", "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^0.84.0", "@swagger-api/apidom-parser-adapter-json": "^0.84.0", + "@swagger-api/apidom-parser-adapter-openapi-json-2": "^0.84.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^0.84.0", "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^0.84.0", "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^0.84.0", "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^0.84.0", diff --git a/packages/apidom-ls/src/config/config.ts b/packages/apidom-ls/src/config/config.ts index 1382b94bf6..5729dc3a5a 100644 --- a/packages/apidom-ls/src/config/config.ts +++ b/packages/apidom-ls/src/config/config.ts @@ -1,6 +1,6 @@ -import configAsyncapi from './asyncapi/config'; -import configOpenapi from './openapi/config'; -import configAds from './ads/config'; +import configAsyncAPI from './asyncapi/config'; +import configOpenAPI from './openapi/config'; +import configADS from './ads/config'; import { Metadata } from '../apidom-language-types'; import symbols from './symbols'; import tokens from './tokens'; @@ -9,9 +9,9 @@ import tokens from './tokens'; export function config(): Metadata { return { metadataMaps: { - openapi: configOpenapi, - asyncapi: configAsyncapi, - ads: configAds, + openapi: configOpenAPI, + asyncapi: configAsyncAPI, + ads: configADS, }, linterFunctions: {}, symbols, diff --git a/packages/apidom-ls/src/config/openapi/config.ts b/packages/apidom-ls/src/config/openapi/config.ts index df61e58421..332b99b42e 100644 --- a/packages/apidom-ls/src/config/openapi/config.ts +++ b/packages/apidom-ls/src/config/openapi/config.ts @@ -30,6 +30,7 @@ import securityRequirementMeta from './security-requirement/meta'; import securitySchemeMeta from './security-scheme/meta'; import serverMeta from './server/meta'; import serverVariableMeta from './server-variable/meta'; +import swaggerMeta from './swagger/meta'; import tagMeta from './tag/meta'; import xmlMeta from './xml/meta'; import schemaMeta from '../common/schema/meta'; @@ -78,6 +79,7 @@ export default { securityScheme: securitySchemeMeta, server: serverMeta, serverVariable: serverVariableMeta, + swagger: swaggerMeta, tag: tagMeta, xml: xmlMeta, schema: schemaMeta, diff --git a/packages/apidom-ls/src/config/openapi/swagger/completion.ts b/packages/apidom-ls/src/config/openapi/swagger/completion.ts new file mode 100644 index 0000000000..6426e942fd --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/swagger/completion.ts @@ -0,0 +1,5 @@ +import { ApidomCompletionItem } from '../../../apidom-language-types'; + +const completion: ApidomCompletionItem[] = []; + +export default completion; diff --git a/packages/apidom-ls/src/config/openapi/swagger/documentation.ts b/packages/apidom-ls/src/config/openapi/swagger/documentation.ts new file mode 100644 index 0000000000..554b878282 --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/swagger/documentation.ts @@ -0,0 +1,5 @@ +import { DocumentationMeta } from '../../../apidom-language-types'; + +const documentation: DocumentationMeta[] = []; + +export default documentation; diff --git a/packages/apidom-ls/src/config/openapi/swagger/lint/allowed-fields.ts b/packages/apidom-ls/src/config/openapi/swagger/lint/allowed-fields.ts new file mode 100644 index 0000000000..c5a7869ac7 --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/swagger/lint/allowed-fields.ts @@ -0,0 +1,36 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes'; +import { LinterMeta } from '../../../../apidom-language-types'; + +const allowedFieldsLint: LinterMeta = { + code: ApilintCodes.NOT_ALLOWED_FIELDS, + source: 'apilint', + message: 'Object includes not allowed fields', + severity: DiagnosticSeverity.Error, + linterFunction: 'allowedFields', + linterParams: [ + [ + 'swagger', + 'info', + 'host', + 'basePath', + 'schemes', + 'consumes', + 'produces', + 'paths', + 'definitions', + 'parameters', + 'responses', + 'securityDefinitions', + 'security', + 'tags', + 'externalDocs', + ], + 'x-', + ], + marker: 'key', + targetSpecs: [{ namespace: 'openapi', version: '2.0' }], +}; + +export default allowedFieldsLint; diff --git a/packages/apidom-ls/src/config/openapi/swagger/lint/index.ts b/packages/apidom-ls/src/config/openapi/swagger/lint/index.ts new file mode 100644 index 0000000000..e5a90a6b9d --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/swagger/lint/index.ts @@ -0,0 +1,5 @@ +import allowedFieldsLint from './allowed-fields'; + +const lints = [allowedFieldsLint]; + +export default lints; diff --git a/packages/apidom-ls/src/config/openapi/swagger/meta.ts b/packages/apidom-ls/src/config/openapi/swagger/meta.ts new file mode 100644 index 0000000000..381bda653d --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/swagger/meta.ts @@ -0,0 +1,12 @@ +import lint from './lint'; +import completion from './completion'; +import documentation from './documentation'; +import { FormatMeta } from '../../../apidom-language-types'; + +const meta: FormatMeta = { + lint, + completion, + documentation, +}; + +export default meta; diff --git a/packages/apidom-ls/src/parser-factory.ts b/packages/apidom-ls/src/parser-factory.ts index da01b24b97..5fa726f336 100644 --- a/packages/apidom-ls/src/parser-factory.ts +++ b/packages/apidom-ls/src/parser-factory.ts @@ -1,3 +1,5 @@ +import * as openapi2AdapterJson from '@swagger-api/apidom-parser-adapter-openapi-json-2'; +import * as openapi2AdapterYaml from '@swagger-api/apidom-parser-adapter-openapi-yaml-2'; import * as openapi3_0AdapterJson from '@swagger-api/apidom-parser-adapter-openapi-json-3-0'; import * as openapi3_0AdapterYaml from '@swagger-api/apidom-parser-adapter-openapi-yaml-3-0'; import * as openapi3_1AdapterJson from '@swagger-api/apidom-parser-adapter-openapi-json-3-1'; @@ -9,6 +11,7 @@ import * as adsAdapterYaml from '@swagger-api/apidom-parser-adapter-api-design-s import * as adapterJson from '@swagger-api/apidom-parser-adapter-json'; import * as adapterYaml from '@swagger-api/apidom-parser-adapter-yaml-1-2'; import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementAsyncAPI2 } from '@swagger-api/apidom-ns-asyncapi-2'; +import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementOpenAPI2 } from '@swagger-api/apidom-ns-openapi-2'; import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementOpenAPI3_0 } from '@swagger-api/apidom-ns-openapi-3-0'; import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementOpenAPI3_1 } from '@swagger-api/apidom-ns-openapi-3-1'; import { TextDocument } from 'vscode-languageserver-textdocument'; @@ -45,6 +48,24 @@ export async function parse( options.refractorOpts = { plugins: [refractorPluginReplaceEmptyElementAsyncAPI2()] }; } result = await asyncapi2AdapterYaml.parse(text, options); + } else if ( + contentLanguage.namespace === 'openapi' && + contentLanguage.version === '2.0' && + contentLanguage.format === 'JSON' + ) { + result = await openapi2AdapterJson.parse(text, { sourceMap: true }); + } else if ( + contentLanguage.namespace === 'openapi' && + contentLanguage.version === '2.0' && + contentLanguage.format === 'YAML' + ) { + const options: Record = { + sourceMap: true, + }; + if (registerPlugins) { + options.refractorOpts = { plugins: [refractorPluginReplaceEmptyElementOpenAPI2()] }; + } + result = await openapi2AdapterYaml.parse(text, options); } else if ( contentLanguage.namespace === 'openapi' && contentLanguage.version?.startsWith('3.0') && diff --git a/packages/apidom-ls/src/utils/utils.ts b/packages/apidom-ls/src/utils/utils.ts index 7e32340b4c..af159d1ef6 100644 --- a/packages/apidom-ls/src/utils/utils.ts +++ b/packages/apidom-ls/src/utils/utils.ts @@ -1,3 +1,5 @@ +import * as openapi2AdapterJson from '@swagger-api/apidom-parser-adapter-openapi-json-2'; +import * as openapi2AdapterYaml from '@swagger-api/apidom-parser-adapter-openapi-yaml-2'; import * as openapi3_0AdapterJson from '@swagger-api/apidom-parser-adapter-openapi-json-3-0'; import * as openapi3_0AdapterYaml from '@swagger-api/apidom-parser-adapter-openapi-yaml-3-0'; import * as openapi3_1AdapterJson from '@swagger-api/apidom-parser-adapter-openapi-json-3-1'; @@ -807,6 +809,31 @@ export async function findNamespace( mediaType: `application/vnd.aai.asyncapi+yaml;version=${version}`, }; } + if (await openapi2AdapterJson.detect(text)) { + const versionMatch = text.match(openapi2AdapterJson.detectionRegExp); + const version = versionMatch?.groups?.version_json ? versionMatch?.groups?.version_json : '2.0'; + + return { + namespace: 'openapi', + version, + format: 'JSON', + mediaType: `application/vnd.oai.openapi+json;version=${version}`, + }; + } + if (await openapi2AdapterYaml.detect(text)) { + const versionMatch = text.match(openapi2AdapterYaml.detectionRegExp); + const version = versionMatch?.groups?.version_yaml + ? versionMatch?.groups?.version_yaml + : versionMatch?.groups?.version_json + ? versionMatch?.groups?.version_json + : '2.0'; + return { + namespace: 'openapi', + version, + format: 'YAML', + mediaType: `application/vnd.oai.openapi+yaml;version=${version}`, + }; + } if (await openapi3_0AdapterJson.detect(text)) { const versionMatch = text.match(openapi3_0AdapterJson.detectionRegExp); const version = versionMatch?.groups?.version_json diff --git a/packages/apidom-ls/test/fixtures/sample-api-openapi-yaml-2-0.yaml b/packages/apidom-ls/test/fixtures/sample-api-openapi-yaml-2-0.yaml new file mode 100644 index 0000000000..a289b1f5f4 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/sample-api-openapi-yaml-2-0.yaml @@ -0,0 +1,2 @@ +swagger: "2.0" +unknown: {} diff --git a/packages/apidom-ls/test/validate.ts b/packages/apidom-ls/test/validate.ts index f50aab9fce..17f061c10d 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3307,4 +3307,41 @@ describe('apidom-ls-validate', function () { languageService.terminate(); }); + + it('oas 2.0 / yaml', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync(path.join(__dirname, 'fixtures', 'sample-api-openapi-yaml-2-0.yaml')) + .toString(); + const doc: TextDocument = TextDocument.create('foo://bar/openapi-2-0.yaml', 'yaml', 0, spec); + const languageService: LanguageService = getLanguageService(contextNoSchema); + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + code: 15000, + message: 'Object includes not allowed fields', + range: { + end: { + character: 5, + line: 0, + }, + start: { + character: 0, + line: 0, + }, + }, + severity: 1, + source: 'apilint', + }, + ]; + + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); });