From b681a8b7bd865a8c1b97b7ffe7eebabcbdd3f0b3 Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Thu, 14 Dec 2023 09:30:50 +0100 Subject: [PATCH 01/11] feat(apidom-ls): path template match parameter object lint --- .../services/validation/linter-functions.ts | 34 +++++++++++-- .../oas/path-template-all-defined.yaml | 50 +++++++++++++++++++ packages/apidom-ls/test/validate.ts | 42 ++++++++++++++++ 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined.yaml diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index c595ff234a..79a227e37d 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -10,7 +10,7 @@ import { ObjectElement, } from '@swagger-api/apidom-core'; import { CompletionItem } from 'vscode-languageserver-types'; -import { test } from 'openapi-path-templating'; +import { test, parse } from 'openapi-path-templating'; // eslint-disable-next-line import/no-cycle import { @@ -1009,9 +1009,37 @@ export const standardLinterfunctions: FunctionItem[] = [ functionName: 'apilintOpenAPIPathTemplateValid', function: (element: Element) => { if (isStringElement(element)) { - return true; + const parseResult = parse(toValue(element)); + const parts: string[] = []; + parseResult.ast.translate(parts); + + const templateExpressions = parts + .filter((part) => part[0] === 'template-expression') + .map((part) => part[1].slice(1, -1)); + + if (templateExpressions.length === 0) { + return true; + } + + const httpVerbsWithParameters: { + [key: string]: { + parameters: { + name: string; + }[]; + }; + } = element.parent.toValue().value; + + const allExpressionsHaveMatchingParameter = Object.values(httpVerbsWithParameters).every( + ({ parameters }) => { + return templateExpressions.every((expression) => + parameters.find(({ name }) => name === expression), + ); + }, + ); + return allExpressionsHaveMatchingParameter; } - return true; + + return false; }, }, ]; diff --git a/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined.yaml b/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined.yaml new file mode 100644 index 0000000000..295e6d3648 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined.yaml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: Foo + version: 0.1.0 +paths: + /foo/bar/{baz}/test/{foo_id}/baz/{bar_id}: + delete: + summary: Delete foo bar baz + operationId: >- + deleteFooBarBaz + parameters: + - name: foo_id + in: path + required: true + schema: + type: string + format: uuid + title: Foo Id + - name: bar_id + in: path + required: true + schema: + type: string + format: uuid + title: Bar Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + /test/{foo_id}/{bar_id}: + get: + summary: Get test foo bar + operationId: >- + getTestFooBar + parameters: + - name: foo_id + in: path + required: true + schema: + type: string + format: uuid + title: Foo Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} diff --git a/packages/apidom-ls/test/validate.ts b/packages/apidom-ls/test/validate.ts index 5446403be6..e0fc8d7fcc 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3376,6 +3376,48 @@ describe('apidom-ls-validate', function () { assert.deepEqual(result, expected as Diagnostic[]); + languageService.terminate(); + }); + it('oas / yaml - every path template should be defined', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join(__dirname, 'fixtures', 'validation', 'oas', 'path-template-all-defined.yaml'), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/path-template-all-defined.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 5, character: 2 }, end: { line: 5, character: 43 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + { + range: { start: { line: 31, character: 2 }, end: { line: 31, character: 25 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + languageService.terminate(); }); }); From c27219350a396cd1285673a52a091b764f829832 Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Thu, 14 Dec 2023 13:53:49 +0100 Subject: [PATCH 02/11] feat(apidom-ls): remove unnecessary check --- .../apidom-ls/src/services/validation/linter-functions.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index 79a227e37d..9376e3cf36 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -1017,10 +1017,6 @@ export const standardLinterfunctions: FunctionItem[] = [ .filter((part) => part[0] === 'template-expression') .map((part) => part[1].slice(1, -1)); - if (templateExpressions.length === 0) { - return true; - } - const httpVerbsWithParameters: { [key: string]: { parameters: { From 171fe978cb25268f0794ec54b611e5f5a859cde1 Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Fri, 15 Dec 2023 09:36:27 +0100 Subject: [PATCH 03/11] feat(apidom-ls): more semantic implementation --- .../services/validation/linter-functions.ts | 56 ++++++++++++------- packages/apidom-ls/test/validate.ts | 3 +- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index 9376e3cf36..764da1c02a 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -8,9 +8,15 @@ import { toValue, ArraySlice, ObjectElement, + isArrayElement, } from '@swagger-api/apidom-core'; import { CompletionItem } from 'vscode-languageserver-types'; -import { test, parse } from 'openapi-path-templating'; +import { test, resolve } from 'openapi-path-templating'; +import { + OperationElement, + ParameterElement, + isParameterElement, +} from '@swagger-api/apidom-ns-openapi-3-0'; // eslint-disable-next-line import/no-cycle import { @@ -1009,30 +1015,40 @@ export const standardLinterfunctions: FunctionItem[] = [ functionName: 'apilintOpenAPIPathTemplateValid', function: (element: Element) => { if (isStringElement(element)) { - const parseResult = parse(toValue(element)); - const parts: string[] = []; - parseResult.ast.translate(parts); + const parameterElements: ParameterElement[] = []; - const templateExpressions = parts - .filter((part) => part[0] === 'template-expression') - .map((part) => part[1].slice(1, -1)); + const pathItemElement = (element.parent as MemberElement).value as ObjectElement; - const httpVerbsWithParameters: { - [key: string]: { - parameters: { - name: string; - }[]; - }; - } = element.parent.toValue().value; + if (pathItemElement.length === 0) { + return true; + } + + pathItemElement.forEach((operation) => { + const parameters = (operation as OperationElement).get('parameters'); + if (isArrayElement(parameters)) { + parameters.forEach((parameter) => { + if (isParameterElement(parameter)) { + const allowedLocation = ['path', 'query']; + if (allowedLocation.includes(toValue(parameter.in))) { + parameterElements.push(parameter); + } + } + }); + } + }); - const allExpressionsHaveMatchingParameter = Object.values(httpVerbsWithParameters).every( - ({ parameters }) => { - return templateExpressions.every((expression) => - parameters.find(({ name }) => name === expression), - ); + const pathTemplateResolveParams = parameterElements.reduce( + (params: { [key: string]: string }, parameterElement) => { + params[toValue(parameterElement.name) as string] = 'placeholder'; // eslint-disable-line no-param-reassign + return params; }, + {}, ); - return allExpressionsHaveMatchingParameter; + + const pathTemplate = toValue(element); + const resolvedPathTemplate = resolve(pathTemplate, pathTemplateResolveParams); + + return !test(resolvedPathTemplate, { strict: true }); } return false; diff --git a/packages/apidom-ls/test/validate.ts b/packages/apidom-ls/test/validate.ts index e0fc8d7fcc..2f59e04be7 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3378,7 +3378,8 @@ describe('apidom-ls-validate', function () { languageService.terminate(); }); - it('oas / yaml - every path template should be defined', async function () { + // eslint-disable-next-line + it.only('oas / yaml - every path template should be defined', async function () { const validationContext: ValidationContext = { comments: DiagnosticSeverity.Error, maxNumberOfProblems: 100, From 5dcdf03093930be156f5a705ca0ea6fa950891e6 Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Fri, 15 Dec 2023 11:01:25 +0100 Subject: [PATCH 04/11] feat(apidom-ls): add reference test case --- .../services/validation/linter-functions.ts | 20 +++---------- .../oas/path-template-all-defined.yaml | 30 +++++++++++++++---- packages/apidom-ls/test/validate.ts | 11 +++++-- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index 764da1c02a..6091c068b4 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -12,11 +12,7 @@ import { } from '@swagger-api/apidom-core'; import { CompletionItem } from 'vscode-languageserver-types'; import { test, resolve } from 'openapi-path-templating'; -import { - OperationElement, - ParameterElement, - isParameterElement, -} from '@swagger-api/apidom-ns-openapi-3-0'; +import { OperationElement, isParameterElement } from '@swagger-api/apidom-ns-openapi-3-0'; // eslint-disable-next-line import/no-cycle import { @@ -1015,14 +1011,14 @@ export const standardLinterfunctions: FunctionItem[] = [ functionName: 'apilintOpenAPIPathTemplateValid', function: (element: Element) => { if (isStringElement(element)) { - const parameterElements: ParameterElement[] = []; - const pathItemElement = (element.parent as MemberElement).value as ObjectElement; if (pathItemElement.length === 0) { return true; } + const pathTemplateResolveParams: { [key: string]: string } = {}; + pathItemElement.forEach((operation) => { const parameters = (operation as OperationElement).get('parameters'); if (isArrayElement(parameters)) { @@ -1030,21 +1026,13 @@ export const standardLinterfunctions: FunctionItem[] = [ if (isParameterElement(parameter)) { const allowedLocation = ['path', 'query']; if (allowedLocation.includes(toValue(parameter.in))) { - parameterElements.push(parameter); + pathTemplateResolveParams[toValue(parameter.name) as string] = 'placeholder'; } } }); } }); - const pathTemplateResolveParams = parameterElements.reduce( - (params: { [key: string]: string }, parameterElement) => { - params[toValue(parameterElement.name) as string] = 'placeholder'; // eslint-disable-line no-param-reassign - return params; - }, - {}, - ); - const pathTemplate = toValue(element); const resolvedPathTemplate = resolve(pathTemplate, pathTemplateResolveParams); diff --git a/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined.yaml b/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined.yaml index 295e6d3648..7b01e0454d 100644 --- a/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined.yaml +++ b/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined.yaml @@ -2,12 +2,21 @@ openapi: 3.0.0 info: title: Foo version: 0.1.0 +components: + parameters: + test_id: + name: test_id + in: path + required: true + schema: + type: string + format: uuid + title: Test Id paths: /foo/bar/{baz}/test/{foo_id}/baz/{bar_id}: delete: - summary: Delete foo bar baz - operationId: >- - deleteFooBarBaz + summary: Delete foo bar test baz + operationId: deleteFooBarTestBaz parameters: - name: foo_id in: path @@ -32,8 +41,7 @@ paths: /test/{foo_id}/{bar_id}: get: summary: Get test foo bar - operationId: >- - getTestFooBar + operationId: getTestFooBar parameters: - name: foo_id in: path @@ -48,3 +56,15 @@ paths: content: application/json: schema: {} + /reference/{test_id}/{baz_id}: + get: + summary: Get test baz + operationId: getReferenceTestBaz + parameters: + - $ref: '#/components/parameters/test_id' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} diff --git a/packages/apidom-ls/test/validate.ts b/packages/apidom-ls/test/validate.ts index 2f59e04be7..5ef6ddce19 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3403,14 +3403,21 @@ describe('apidom-ls-validate', function () { const result = await languageService.doValidation(doc, validationContext); const expected: Diagnostic[] = [ { - range: { start: { line: 5, character: 2 }, end: { line: 5, character: 43 } }, + range: { start: { line: 15, character: 2 }, end: { line: 15, character: 43 } }, message: 'path template expressions is not matched with Parameter Object(s)', severity: 1, code: 3040101, source: 'apilint', }, { - range: { start: { line: 31, character: 2 }, end: { line: 31, character: 25 } }, + range: { start: { line: 40, character: 2 }, end: { line: 40, character: 25 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + { + range: { start: { line: 58, character: 2 }, end: { line: 58, character: 31 } }, message: 'path template expressions is not matched with Parameter Object(s)', severity: 1, code: 3040101, From 9d61642ea48d335d182481d293d4c07ac005ca2e Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Fri, 15 Dec 2023 12:27:17 +0100 Subject: [PATCH 05/11] feat(apidom-ls): change implementation and add more fixtures --- .../services/validation/linter-functions.ts | 46 ++++-- .../oas/path-template-all-defined-2-0.yaml | 104 ++++++++++++ ...aml => path-template-all-defined-3-0.yaml} | 35 ++++ .../oas/path-template-all-defined-3-1.yaml | 105 ++++++++++++ packages/apidom-ls/test/validate.ts | 150 +++++++++++++++++- 5 files changed, 424 insertions(+), 16 deletions(-) create mode 100644 packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined-2-0.yaml rename packages/apidom-ls/test/fixtures/validation/oas/{path-template-all-defined.yaml => path-template-all-defined-3-0.yaml} (64%) create mode 100644 packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined-3-1.yaml diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index 6091c068b4..aa16500aba 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -12,7 +12,6 @@ import { } from '@swagger-api/apidom-core'; import { CompletionItem } from 'vscode-languageserver-types'; import { test, resolve } from 'openapi-path-templating'; -import { OperationElement, isParameterElement } from '@swagger-api/apidom-ns-openapi-3-0'; // eslint-disable-next-line import/no-cycle import { @@ -1017,19 +1016,42 @@ export const standardLinterfunctions: FunctionItem[] = [ return true; } - const pathTemplateResolveParams: { [key: string]: string } = {}; + const parameterElements: Element[] = []; + + const isParameterPolymorphicCheck = (el: Element): boolean => el.element === 'parameter'; - pathItemElement.forEach((operation) => { - const parameters = (operation as OperationElement).get('parameters'); - if (isArrayElement(parameters)) { - parameters.forEach((parameter) => { - if (isParameterElement(parameter)) { - const allowedLocation = ['path', 'query']; - if (allowedLocation.includes(toValue(parameter.in))) { - pathTemplateResolveParams[toValue(parameter.name) as string] = 'placeholder'; + pathItemElement.forEach((el) => { + if (el.element === 'parameters') { + const parametersObject = pathItemElement.get('parameters'); + if (isArrayElement(parametersObject)) { + parametersObject.forEach((parameter) => { + if (isParameterPolymorphicCheck(parameter)) { + parameterElements.push(parameter); } - } - }); + }); + } + } + if (el.element === 'operation') { + // @ts-ignore + const parameters = el.get('parameters'); + if (isArrayElement(parameters)) { + parameters.forEach((parameter) => { + if (isParameterPolymorphicCheck(parameter)) { + parameterElements.push(parameter); + } + }); + } + } + }); + + const allowedLocation = ['path', 'query']; + const pathTemplateResolveParams: { [key: string]: string } = {}; + + parameterElements.forEach((parameter) => { + // @ts-ignore + if (allowedLocation.includes(toValue(parameter.in))) { + // @ts-ignore + pathTemplateResolveParams[toValue(parameter.name) as string] = 'placeholder'; } }); diff --git a/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined-2-0.yaml b/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined-2-0.yaml new file mode 100644 index 0000000000..45ddea6388 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined-2-0.yaml @@ -0,0 +1,104 @@ +swagger: '2.0' +info: + title: Foo + version: 0.1.0 +parameters: + test_id: + name: test_id + in: path + required: true + schema: + type: string + format: uuid + title: Test Id +paths: + /foo/bar/{baz}/test/{foo_id}/baz/{bar_id}: + delete: + summary: Delete foo bar test baz + operationId: deleteFooBarTestBaz + parameters: + - name: foo_id + in: path + required: true + schema: + type: string + format: uuid + title: Foo Id + - name: bar_id + in: path + required: true + schema: + type: string + format: uuid + title: Bar Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + /test/{foo_id}/{bar_id}: + get: + summary: Get test foo bar + operationId: getTestFooBar + parameters: + - name: foo_id + in: path + required: true + schema: + type: string + format: uuid + title: Foo Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + /reference/{test_id}/{baz_id}: + get: + summary: Get test baz + operationId: getReferenceTestBaz + parameters: + - $ref: '#/parameters/test_id' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + /just_parameters_object/{x_id}/{y_id}: + parameters: + - name: x_id + in: path + required: true + schema: + type: string + format: uuid + title: X Id + /both_parameters_and_operations_object/{a_id}/{b_id}/{c_id}: + get: + summary: Get both parameters and operations object a b c + operationId: getBothParametersAndOperationsObjectABC + parameters: + - name: b_id + in: path + required: true + schema: + type: string + format: uuid + title: B Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + parameters: + - name: a_id + in: path + required: true + schema: + type: string + format: uuid + title: A Id diff --git a/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined.yaml b/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined-3-0.yaml similarity index 64% rename from packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined.yaml rename to packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined-3-0.yaml index 7b01e0454d..cb1fc8c37f 100644 --- a/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined.yaml +++ b/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined-3-0.yaml @@ -68,3 +68,38 @@ paths: content: application/json: schema: {} + /just_parameters_object/{x_id}/{y_id}: + parameters: + - name: x_id + in: path + required: true + schema: + type: string + format: uuid + title: X Id + /both_parameters_and_operations_object/{a_id}/{b_id}/{c_id}: + get: + summary: Get both parameters and operations object a b c + operationId: getBothParametersAndOperationsObjectABC + parameters: + - name: b_id + in: path + required: true + schema: + type: string + format: uuid + title: B Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + parameters: + - name: a_id + in: path + required: true + schema: + type: string + format: uuid + title: A Id diff --git a/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined-3-1.yaml b/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined-3-1.yaml new file mode 100644 index 0000000000..be7d8d82b1 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/path-template-all-defined-3-1.yaml @@ -0,0 +1,105 @@ +openapi: 3.1.0 +info: + title: Foo + version: 0.1.0 +components: + parameters: + test_id: + name: test_id + in: path + required: true + schema: + type: string + format: uuid + title: Test Id +paths: + /foo/bar/{baz}/test/{foo_id}/baz/{bar_id}: + delete: + summary: Delete foo bar test baz + operationId: deleteFooBarTestBaz + parameters: + - name: foo_id + in: path + required: true + schema: + type: string + format: uuid + title: Foo Id + - name: bar_id + in: path + required: true + schema: + type: string + format: uuid + title: Bar Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + /test/{foo_id}/{bar_id}: + get: + summary: Get test foo bar + operationId: getTestFooBar + parameters: + - name: foo_id + in: path + required: true + schema: + type: string + format: uuid + title: Foo Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + /reference/{test_id}/{baz_id}: + get: + summary: Get test baz + operationId: getReferenceTestBaz + parameters: + - $ref: '#/components/parameters/test_id' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + /just_parameters_object/{x_id}/{y_id}: + parameters: + - name: x_id + in: path + required: true + schema: + type: string + format: uuid + title: X Id + /both_parameters_and_operations_object/{a_id}/{b_id}/{c_id}: + get: + summary: Get both parameters and operations object a b c + operationId: getBothParametersAndOperationsObjectABC + parameters: + - name: b_id + in: path + required: true + schema: + type: string + format: uuid + title: B Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + parameters: + - name: a_id + in: path + required: true + schema: + type: string + format: uuid + title: A Id diff --git a/packages/apidom-ls/test/validate.ts b/packages/apidom-ls/test/validate.ts index 5ef6ddce19..acebf4337d 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3378,8 +3378,136 @@ describe('apidom-ls-validate', function () { languageService.terminate(); }); - // eslint-disable-next-line - it.only('oas / yaml - every path template should be defined', async function () { + + it('oas 2.0 / yaml - every path template should be defined', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join(__dirname, 'fixtures', 'validation', 'oas', 'path-template-all-defined-2-0.yaml'), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/path-template-all-defined-2-0.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 14, character: 2 }, end: { line: 14, character: 43 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + { + range: { start: { line: 39, character: 2 }, end: { line: 39, character: 25 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + { + range: { start: { line: 57, character: 2 }, end: { line: 57, character: 31 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + { + range: { start: { line: 69, character: 2 }, end: { line: 69, character: 39 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + { + range: { start: { line: 78, character: 2 }, end: { line: 78, character: 61 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.0 / yaml - every path template should be defined', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join(__dirname, 'fixtures', 'validation', 'oas', 'path-template-all-defined-3-0.yaml'), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/path-template-all-defined-3-0.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 15, character: 2 }, end: { line: 15, character: 43 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + { + range: { start: { line: 40, character: 2 }, end: { line: 40, character: 25 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + { + range: { start: { line: 58, character: 2 }, end: { line: 58, character: 31 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + { + range: { start: { line: 70, character: 2 }, end: { line: 70, character: 39 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + { + range: { start: { line: 79, character: 2 }, end: { line: 79, character: 61 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.1 / yaml - every path template should be defined', async function () { const validationContext: ValidationContext = { comments: DiagnosticSeverity.Error, maxNumberOfProblems: 100, @@ -3388,11 +3516,11 @@ describe('apidom-ls-validate', function () { const spec = fs .readFileSync( - path.join(__dirname, 'fixtures', 'validation', 'oas', 'path-template-all-defined.yaml'), + path.join(__dirname, 'fixtures', 'validation', 'oas', 'path-template-all-defined-3-1.yaml'), ) .toString(); const doc: TextDocument = TextDocument.create( - 'foo://bar/path-template-all-defined.yaml', + 'foo://bar/path-template-all-defined-3-1.yaml', 'yaml', 0, spec, @@ -3423,6 +3551,20 @@ describe('apidom-ls-validate', function () { code: 3040101, source: 'apilint', }, + { + range: { start: { line: 70, character: 2 }, end: { line: 70, character: 39 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, + { + range: { start: { line: 79, character: 2 }, end: { line: 79, character: 61 } }, + message: 'path template expressions is not matched with Parameter Object(s)', + severity: 1, + code: 3040101, + source: 'apilint', + }, ]; assert.deepEqual(result, expected as Diagnostic[]); From 169f0d69c6ef872b19e53eead3f70af1aba68add Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Fri, 15 Dec 2023 13:49:50 +0100 Subject: [PATCH 06/11] feat(apidom-ls): make variable names explicit --- .../services/validation/linter-functions.ts | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index aa16500aba..18cc760f55 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -1017,26 +1017,24 @@ export const standardLinterfunctions: FunctionItem[] = [ } const parameterElements: Element[] = []; - - const isParameterPolymorphicCheck = (el: Element): boolean => el.element === 'parameter'; + const isParameterElement = (el: Element): boolean => el.element === 'parameter'; pathItemElement.forEach((el) => { if (el.element === 'parameters') { - const parametersObject = pathItemElement.get('parameters'); - if (isArrayElement(parametersObject)) { - parametersObject.forEach((parameter) => { - if (isParameterPolymorphicCheck(parameter)) { + const pathItemParameterElements = pathItemElement.get('parameters'); + if (isArrayElement(pathItemParameterElements)) { + pathItemParameterElements.forEach((parameter) => { + if (isParameterElement(parameter)) { parameterElements.push(parameter); } }); } } if (el.element === 'operation') { - // @ts-ignore - const parameters = el.get('parameters'); - if (isArrayElement(parameters)) { - parameters.forEach((parameter) => { - if (isParameterPolymorphicCheck(parameter)) { + const operationParameterElements = (el as ObjectElement).get('parameters'); + if (isArrayElement(operationParameterElements)) { + operationParameterElements.forEach((parameter) => { + if (isParameterElement(parameter)) { parameterElements.push(parameter); } }); @@ -1045,13 +1043,12 @@ export const standardLinterfunctions: FunctionItem[] = [ }); const allowedLocation = ['path', 'query']; - const pathTemplateResolveParams: { [key: string]: string } = {}; + const pathTemplateResolveParams: { [key: string]: 'placeholder' } = {}; parameterElements.forEach((parameter) => { - // @ts-ignore - if (allowedLocation.includes(toValue(parameter.in))) { - // @ts-ignore - pathTemplateResolveParams[toValue(parameter.name) as string] = 'placeholder'; + if (allowedLocation.includes(toValue((parameter as ObjectElement).get('in')))) { + pathTemplateResolveParams[toValue((parameter as ObjectElement).get('name'))] = + 'placeholder'; } }); From c65449e64b9d43ef18dd17e3d97191cedf479773 Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Fri, 15 Dec 2023 14:21:41 +0100 Subject: [PATCH 07/11] feat(apidom-ls): put pathItemParameterElements outside loop --- .../services/validation/linter-functions.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index 18cc760f55..3a123ae33f 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -1019,17 +1019,16 @@ export const standardLinterfunctions: FunctionItem[] = [ const parameterElements: Element[] = []; const isParameterElement = (el: Element): boolean => el.element === 'parameter'; - pathItemElement.forEach((el) => { - if (el.element === 'parameters') { - const pathItemParameterElements = pathItemElement.get('parameters'); - if (isArrayElement(pathItemParameterElements)) { - pathItemParameterElements.forEach((parameter) => { - if (isParameterElement(parameter)) { - parameterElements.push(parameter); - } - }); + const pathItemParameterElements = pathItemElement.get('parameters'); + if (isArrayElement(pathItemParameterElements)) { + pathItemParameterElements.forEach((parameter) => { + if (isParameterElement(parameter)) { + parameterElements.push(parameter); } - } + }); + } + + pathItemElement.forEach((el) => { if (el.element === 'operation') { const operationParameterElements = (el as ObjectElement).get('parameters'); if (isArrayElement(operationParameterElements)) { From 88b045596120cc0ce127106189c1b4f8867013d8 Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Mon, 18 Dec 2023 12:05:00 +0100 Subject: [PATCH 08/11] feat(apidom-ls): bump openapi-path-templating to 1.3.0 --- package-lock.json | 12 ++++++------ packages/apidom-ls/package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 825a49ede7..a1aa5c5f29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29696,11 +29696,12 @@ } }, "node_modules/openapi-path-templating": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/openapi-path-templating/-/openapi-path-templating-1.2.0.tgz", - "integrity": "sha512-m1f9ws9Zn/1CjaBPxECzLmtaTyFwO79eL53pgHUmjCBJQShtVo3vhM0yGOyPF3dxsQ0FYrX+zfflkvaw4+PlNg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/openapi-path-templating/-/openapi-path-templating-1.3.0.tgz", + "integrity": "sha512-nXA8Y5jvZc8mSxDNgy8ov4BQayshc2G6ZDThMOtghbQS5iOUq1lukOTVzvnFMcyl7fithDgvyuA+V8O0W/CASw==", "dependencies": { - "apg-lite": "^1.0.2" + "apg-lite": "^1.0.2", + "prettier": "^3.1.1" }, "engines": { "node": ">=14.16" @@ -31979,7 +31980,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", - "dev": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -38283,7 +38283,7 @@ "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.88.0", "@swagger-api/apidom-reference": "^0.88.0", "@types/ramda": "~0.29.6", - "openapi-path-templating": "^1.2.0", + "openapi-path-templating": "^1.3.0", "ramda": "~0.29.1", "ramda-adjunct": "^4.1.1", "vscode-languageserver-protocol": "^3.17.2", diff --git a/packages/apidom-ls/package.json b/packages/apidom-ls/package.json index a767dbc101..e25a2aa30f 100644 --- a/packages/apidom-ls/package.json +++ b/packages/apidom-ls/package.json @@ -116,7 +116,7 @@ "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.88.0", "@swagger-api/apidom-reference": "^0.88.0", "@types/ramda": "~0.29.6", - "openapi-path-templating": "^1.2.0", + "openapi-path-templating": "^1.3.0", "ramda": "~0.29.1", "ramda-adjunct": "^4.1.1", "vscode-languageserver-protocol": "^3.17.2", From d0d3b11f30b41dbd32a34e6b699379a4a5a4a72e Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Mon, 18 Dec 2023 12:47:38 +0100 Subject: [PATCH 09/11] feat(apidom-ls): reference object handling --- .../services/validation/linter-functions.ts | 15 ++++++++++++- packages/apidom-ls/test/validate.ts | 21 ------------------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index 3a123ae33f..44be5fe361 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -1016,12 +1016,17 @@ export const standardLinterfunctions: FunctionItem[] = [ return true; } + let oneOfParametersIsReferenceObject = false; const parameterElements: Element[] = []; const isParameterElement = (el: Element): boolean => el.element === 'parameter'; + const isReferenceElement = (el: Element): boolean => el.element === 'reference'; const pathItemParameterElements = pathItemElement.get('parameters'); if (isArrayElement(pathItemParameterElements)) { pathItemParameterElements.forEach((parameter) => { + if (isReferenceElement(parameter) && !oneOfParametersIsReferenceObject) { + oneOfParametersIsReferenceObject = true; + } if (isParameterElement(parameter)) { parameterElements.push(parameter); } @@ -1033,6 +1038,9 @@ export const standardLinterfunctions: FunctionItem[] = [ const operationParameterElements = (el as ObjectElement).get('parameters'); if (isArrayElement(operationParameterElements)) { operationParameterElements.forEach((parameter) => { + if (isReferenceElement(parameter) && !oneOfParametersIsReferenceObject) { + oneOfParametersIsReferenceObject = true; + } if (isParameterElement(parameter)) { parameterElements.push(parameter); } @@ -1054,7 +1062,12 @@ export const standardLinterfunctions: FunctionItem[] = [ const pathTemplate = toValue(element); const resolvedPathTemplate = resolve(pathTemplate, pathTemplateResolveParams); - return !test(resolvedPathTemplate, { strict: true }); + const resolveResult = !test(resolvedPathTemplate, { strict: true }); + + if (!resolveResult && oneOfParametersIsReferenceObject) { + return true; + } + return resolveResult; } return false; diff --git a/packages/apidom-ls/test/validate.ts b/packages/apidom-ls/test/validate.ts index acebf4337d..8e4aa66588 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3416,13 +3416,6 @@ describe('apidom-ls-validate', function () { code: 3040101, source: 'apilint', }, - { - range: { start: { line: 57, character: 2 }, end: { line: 57, character: 31 } }, - message: 'path template expressions is not matched with Parameter Object(s)', - severity: 1, - code: 3040101, - source: 'apilint', - }, { range: { start: { line: 69, character: 2 }, end: { line: 69, character: 39 } }, message: 'path template expressions is not matched with Parameter Object(s)', @@ -3480,13 +3473,6 @@ describe('apidom-ls-validate', function () { code: 3040101, source: 'apilint', }, - { - range: { start: { line: 58, character: 2 }, end: { line: 58, character: 31 } }, - message: 'path template expressions is not matched with Parameter Object(s)', - severity: 1, - code: 3040101, - source: 'apilint', - }, { range: { start: { line: 70, character: 2 }, end: { line: 70, character: 39 } }, message: 'path template expressions is not matched with Parameter Object(s)', @@ -3544,13 +3530,6 @@ describe('apidom-ls-validate', function () { code: 3040101, source: 'apilint', }, - { - range: { start: { line: 58, character: 2 }, end: { line: 58, character: 31 } }, - message: 'path template expressions is not matched with Parameter Object(s)', - severity: 1, - code: 3040101, - source: 'apilint', - }, { range: { start: { line: 70, character: 2 }, end: { line: 70, character: 39 } }, message: 'path template expressions is not matched with Parameter Object(s)', From 22ae82af6713c269bf397c1cfb675653a4fe8dff Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Mon, 18 Dec 2023 13:08:09 +0100 Subject: [PATCH 10/11] feat(apidom-ls): fix wrong return --- packages/apidom-ls/src/services/validation/linter-functions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index 44be5fe361..02a2cdd6ec 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -1070,7 +1070,7 @@ export const standardLinterfunctions: FunctionItem[] = [ return resolveResult; } - return false; + return true; }, }, ]; From ee6020a44ccab679a9057de614ae6af60e98e497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Mon, 18 Dec 2023 13:38:37 +0100 Subject: [PATCH 11/11] Update linter-functions.ts --- .../apidom-ls/src/services/validation/linter-functions.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index 02a2cdd6ec..b453b8ac6f 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -1061,13 +1061,9 @@ export const standardLinterfunctions: FunctionItem[] = [ const pathTemplate = toValue(element); const resolvedPathTemplate = resolve(pathTemplate, pathTemplateResolveParams); + const includesTemplateExpression = test(resolvedPathTemplate, { strict: true }); - const resolveResult = !test(resolvedPathTemplate, { strict: true }); - - if (!resolveResult && oneOfParametersIsReferenceObject) { - return true; - } - return resolveResult; + return !includesTemplateExpression || oneOfParametersIsReferenceObject; } return true;