Skip to content

Commit

Permalink
feat(ls): add path template linting rule (#3538)
Browse files Browse the repository at this point in the history
This linting rule that validates if the template expressions defined
within the path template are all present in the appropriate Parameter Objects.

Refs #3517

Co-authored-by: Vladimír Gorej <vladimir.gorej@gmail.com>
  • Loading branch information
kowalczyk-krzysztof and char0n authored Dec 18, 2023
1 parent 5c61dba commit 0055a09
Show file tree
Hide file tree
Showing 7 changed files with 549 additions and 9 deletions.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/apidom-ls/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
59 changes: 57 additions & 2 deletions packages/apidom-ls/src/services/validation/linter-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
toValue,
ArraySlice,
ObjectElement,
isArrayElement,
} from '@swagger-api/apidom-core';
import { CompletionItem } from 'vscode-languageserver-types';
import { test } from 'openapi-path-templating';
import { test, resolve } from 'openapi-path-templating';

// eslint-disable-next-line import/no-cycle
import {
Expand Down Expand Up @@ -1009,8 +1010,62 @@ export const standardLinterfunctions: FunctionItem[] = [
functionName: 'apilintOpenAPIPathTemplateValid',
function: (element: Element) => {
if (isStringElement(element)) {
return true;
const pathItemElement = (element.parent as MemberElement).value as ObjectElement;

if (pathItemElement.length === 0) {
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);
}
});
}

pathItemElement.forEach((el) => {
if (el.element === 'operation') {
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);
}
});
}
}
});

const allowedLocation = ['path', 'query'];
const pathTemplateResolveParams: { [key: string]: 'placeholder' } = {};

parameterElements.forEach((parameter) => {
if (allowedLocation.includes(toValue((parameter as ObjectElement).get('in')))) {
pathTemplateResolveParams[toValue((parameter as ObjectElement).get('name'))] =
'placeholder';
}
});

const pathTemplate = toValue(element);
const resolvedPathTemplate = resolve(pathTemplate, pathTemplateResolveParams);
const includesTemplateExpression = test(resolvedPathTemplate, { strict: true });

return !includesTemplateExpression || oneOfParametersIsReferenceObject;
}

return true;
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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 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
Loading

0 comments on commit 0055a09

Please sign in to comment.