Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/apidom-ls/src/config/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ enum ApilintCodes {
OPENAPI2_PATH_TEMPLATE = 3040000,
OPENAPI2_PATH_TEMPLATE_VALUE_WELL_FORMED = 3040100,
OPENAPI2_PATH_TEMPLATE_VALUE_VALID,
OPENAPI2_PATH_TEMPLATE_VALUE_EQUIVALENT_NOT_ALLOWED,

OPENAPI2_LICENSE = 3050000,
OPENAPI2_LICENSE_FIELD_NAME_TYPE = 3050100,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import valueWellFormedLint from './value--well-formed.ts';
import valueValidLint from './value--valid.ts';
import valueEquivalentNotAllowedLint from './value--equivalent-not-allowed.ts';

const lints = [valueWellFormedLint, valueValidLint];
const lints = [valueWellFormedLint, valueValidLint, valueEquivalentNotAllowedLint];

export default lints;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { DiagnosticSeverity } from 'vscode-languageserver-types';

import ApilintCodes from '../../../codes.ts';
import { LinterMeta } from '../../../../apidom-language-types.ts';
import { OpenAPI2 } from '../../target-specs.ts';

const valueEquivalentNotAllowedLint: LinterMeta = {
code: ApilintCodes.OPENAPI2_PATH_TEMPLATE_VALUE_EQUIVALENT_NOT_ALLOWED,
source: 'apilint',
message: 'Equivalent paths are not allowed',
severity: DiagnosticSeverity.Error,
linterFunction: 'apilintOpenAPIPathTemplateNoEquivalent',
marker: 'value',
targetSpecs: OpenAPI2,
};

export default valueEquivalentNotAllowedLint;
70 changes: 51 additions & 19 deletions packages/apidom-ls/src/services/validation/linter-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
test as testPathTemplate,
resolve as resolvePathTemplate,
parse as parsePathTemplate,
isIdentical,
} from 'openapi-path-templating';

// eslint-disable-next-line import/no-cycle
Expand Down Expand Up @@ -1099,52 +1100,66 @@ export const standardLinterfunctions: FunctionItem[] = [
}

let oneOfParametersIsReferenceObject = false;
const parameterElements: Element[] = [];
const isParameterElement = (el: Element): boolean => el.element === 'parameter';
const isReferenceElement = (el: Element): boolean => el.element === 'reference';

const pathLevelParameterElements: Element[] = [];
const pathItemParameterElements = pathItemElement.get('parameters');
if (isArrayElement(pathItemParameterElements)) {
pathItemParameterElements.forEach((parameter) => {
if (isReferenceElement(parameter) && !oneOfParametersIsReferenceObject) {
oneOfParametersIsReferenceObject = true;
}
if (isParameterElement(parameter)) {
parameterElements.push(parameter);
}
pathLevelParameterElements.push(parameter);
});
}

const includesTemplateExpression: boolean[] = [];
pathItemElement.forEach((el) => {
if (el.element === 'operation') {
const operationParameterElements = (el as ObjectElement).get('parameters');
const operationParameterElements = isObject(el) && el.get('parameters');
const currentOperationLevelParameterElements: Element[] = [];
if (isArrayElement(operationParameterElements)) {
operationParameterElements.forEach((parameter) => {
if (isReferenceElement(parameter) && !oneOfParametersIsReferenceObject) {
oneOfParametersIsReferenceObject = true;
}
if (isParameterElement(parameter)) {
parameterElements.push(parameter);
currentOperationLevelParameterElements.push(parameter);
}
});
}
}
});

const pathTemplateResolveParams: { [key: string]: 'placeholder' } = {};
const pathTemplateResolveParams: { [key: string]: 'placeholder' } = {};
pathLevelParameterElements
.concat(currentOperationLevelParameterElements)
.forEach((parameter) => {
if (isObject(parameter)) {
const inParam = parameter.get('in');
const nameParam = parameter.get('name');
if (inParam && inParam.content === 'path' && nameParam && nameParam.content) {
pathTemplateResolveParams[nameParam.content] = 'placeholder';
}
}
});

parameterElements.forEach((parameter) => {
if (toValue((parameter as ObjectElement).get('in')) === 'path') {
pathTemplateResolveParams[toValue((parameter as ObjectElement).get('name'))] =
'placeholder';
const pathTemplate = toValue(element);
const resolvedPathTemplate = resolvePathTemplate(
pathTemplate,
pathTemplateResolveParams,
);
includesTemplateExpression.push(
testPathTemplate(resolvedPathTemplate, {
strict: true,
}),
);
}
});

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

return !includesTemplateExpression || oneOfParametersIsReferenceObject;
return (
(includesTemplateExpression.length > 0
? includesTemplateExpression.every((bool) => !bool)
: false) || oneOfParametersIsReferenceObject
);
}

return true;
Expand Down Expand Up @@ -1219,4 +1234,21 @@ export const standardLinterfunctions: FunctionItem[] = [
return true;
},
},
{
functionName: 'apilintOpenAPIPathTemplateNoEquivalent',
function: (element: Element): boolean => {
const isFirstOccurrence = (currentKey: string, allKeys: unknown[]) => {
const firstIndex = allKeys.findIndex(
(e) => typeof e === 'string' && isIdentical(e, currentKey),
);

return allKeys[firstIndex] === currentKey;
};
const paths = element.parent?.parent;

return isStringElement(element) && isObject(paths)
? isFirstOccurrence(element.toValue(), paths.keys())
: true;
},
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,20 @@ paths:
required: true
type: string
format: uuid
/users/{id}:
get:
summary: Get user by ID
parameters:
- name: id
in: path
required: true
type: string
responses:
'200':
description: OK

delete:
summary: No parameters here (specified in get only)
responses:
'204':
description: No Content
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,21 @@ paths:
type: string
format: uuid
title: A Id
/users/{id}:
get:
summary: Get user by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: OK

delete:
summary: No parameters here (specified in get only)
responses:
'204':
description: No Content
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,21 @@ paths:
type: string
format: uuid
title: A Id
/users/{id}:
get:
summary: Get user by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: OK

delete:
summary: No parameters here (specified in get only)
responses:
'204':
description: No Content
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
swagger: '2.0'
info:
title: Test API
version: 1.0.0
paths:
/items/{id}:
get:
summary: Get item by ID
parameters:
- name: id
in: path
required: true
type: string
responses:
200:
description: OK
/items/{itemId}:
get:
summary: Get item by itemId
parameters:
- name: itemId
in: path
required: true
type: string
responses:
200:
description: OK

57 changes: 57 additions & 0 deletions packages/apidom-ls/test/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3814,6 +3814,13 @@ describe('apidom-ls-validate', function () {
code: 3040101,
source: 'apilint',
},
{
range: { start: { line: 78, character: 2 }, end: { line: 78, character: 13 } },
message: 'path template expressions is not matched with Parameter Object(s)',
severity: 1,
code: 3040101,
source: 'apilint',
},
];
assert.deepEqual(result, expected as Diagnostic[]);

Expand Down Expand Up @@ -3871,6 +3878,13 @@ describe('apidom-ls-validate', function () {
code: 3040101,
source: 'apilint',
},
{
range: { start: { line: 105, character: 2 }, end: { line: 105, character: 13 } },
message: 'path template expressions is not matched with Parameter Object(s)',
severity: 1,
code: 3040101,
source: 'apilint',
},
];
assert.deepEqual(result, expected as Diagnostic[]);

Expand Down Expand Up @@ -3928,6 +3942,13 @@ describe('apidom-ls-validate', function () {
code: 3040101,
source: 'apilint',
},
{
range: { start: { line: 105, character: 2 }, end: { line: 105, character: 13 } },
message: 'path template expressions is not matched with Parameter Object(s)',
severity: 1,
code: 3040101,
source: 'apilint',
},
];
assert.deepEqual(result, expected as Diagnostic[]);

Expand Down Expand Up @@ -4541,4 +4562,40 @@ describe('apidom-ls-validate', function () {

languageService.terminate();
});

it('oas 2.0 should not allow equivalent paths', async function () {
const spec = fs
.readFileSync(
path.join(
__dirname,
'fixtures',
'validation',
'oas',
'path-template-equivalent-not-allowed.yaml',
),
)
.toString();
const doc: TextDocument = TextDocument.create(
'path-template-equivalent-not-allowed.yaml',
'yaml',
0,
spec,
);

const languageService: LanguageService = getLanguageService(contextNoSchema);

const result = await languageService.doValidation(doc);
const expected: Diagnostic[] = [
{
message: 'Equivalent paths are not allowed',
severity: 1,
code: 3040102,
source: 'apilint',
range: { start: { line: 16, character: 2 }, end: { line: 16, character: 17 } },
},
];
assert.deepEqual(result, expected);

languageService.terminate();
});
});