Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(apidom-ls): lint parameter defined in path template #3571

Merged
merged 13 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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 @@ -734,6 +734,7 @@ enum ApilintCodes {
OPENAPI2_PARAMETER_FIELD_UNIQUE_ITEMS_TYPE = 3101700,
OPENAPI2_PARAMETER_FIELD_ENUM_TYPE = 3101800,
OPENAPI2_PARAMETER_FIELD_MULTIPLE_OF_TYPE = 3101900,
OPENAPI2_PARAMETER_FIELD_IS_DEFINED_WITHIN_PATH_TEMPLATE = 3102000,
kowalczyk-krzysztof marked this conversation as resolved.
Show resolved Hide resolved

OPENAPI3_0 = 5000000,

Expand Down
2 changes: 2 additions & 0 deletions packages/apidom-ls/src/config/openapi/parameter/lint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import minLengthTypeLint from './min-length--type';
import uniqueItemsTypeLint from './unique-items--type';
import enumTypeLint from './enum--type';
import multipleOfTypeLint from './multiple-of--type';
import isDefinedWithinPathTemplate from './is-defined-within-path-template';

const lints = [
nameTypeLint,
Expand Down Expand Up @@ -84,6 +85,7 @@ const lints = [
allowedFields2_0Lint,
allowedFields3_0Lint,
allowedFields3_1Lint,
isDefinedWithinPathTemplate,
];

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

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

const isDefinedWithinPathTemplate: LinterMeta = {
kowalczyk-krzysztof marked this conversation as resolved.
Show resolved Hide resolved
code: ApilintCodes.OPENAPI2_PARAMETER_FIELD_IS_DEFINED_WITHIN_PATH_TEMPLATE,
source: 'apilint',
message: 'parameter is not defined within path template',
severity: DiagnosticSeverity.Error,
linterFunction: 'apilintOpenAPIParameterFieldIsDefinedWithinPathTemplate',
marker: 'value',
targetSpecs: [...OpenAPI2, ...OpenAPI3],
conditions: [
kowalczyk-krzysztof marked this conversation as resolved.
Show resolved Hide resolved
{
function: 'apilintOpenAPIPathTemplateWellFormed',
params: [true],
},
],
};

export default isDefinedWithinPathTemplate;
51 changes: 50 additions & 1 deletion packages/apidom-ls/src/services/validation/linter-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import {
ArraySlice,
ObjectElement,
isArrayElement,
includesClasses,
} from '@swagger-api/apidom-core';
import { CompletionItem } from 'vscode-languageserver-types';
import { test, resolve } from 'openapi-path-templating';
import { test, resolve, parse } from 'openapi-path-templating';

// eslint-disable-next-line import/no-cycle
import {
Expand Down Expand Up @@ -1068,4 +1069,52 @@ export const standardLinterfunctions: FunctionItem[] = [
return true;
},
},
{
functionName: 'apilintOpenAPIParameterFieldIsDefinedWithinPathTemplate',
kowalczyk-krzysztof marked this conversation as resolved.
Show resolved Hide resolved
function: (element: Element) => {
if (element.element === 'parameter') {
const parameterName = toValue((element as ObjectElement).get('name'));
const parameterLocation = toValue((element as ObjectElement).get('in'));
char0n marked this conversation as resolved.
Show resolved Hide resolved
const isInPathItemElement =
isArrayElement(element.parent) &&
includesClasses(['path-item-parameters'], element.parent);
const isChildOfOperationElement = element.parent.parent.parent.element === 'operation';
char0n marked this conversation as resolved.
Show resolved Hide resolved
kowalczyk-krzysztof marked this conversation as resolved.
Show resolved Hide resolved

const getAST = (pathTemplate: string) => {
const parseResult = parse(pathTemplate);
const parts: [string, string][] = [];
parseResult.ast.translate(parts);
return parts;
};

const pathTemplateASTIncludesParameter = (ast: [string, string][]) =>
kowalczyk-krzysztof marked this conversation as resolved.
Show resolved Hide resolved
ast.findIndex(
([name, value]) => name === 'template-expression-param-name' && value === parameterName,
) > -1;

const getPathTemplate = (): string => {
if (isInPathItemElement) {
return toValue(element.parent.parent.parent.meta.get('path'));
}
if (isChildOfOperationElement) {
return toValue(
(element.parent.parent.parent.parent.parent.parent as MemberElement).key,
);
}
return '';
};

if (parameterLocation !== 'path') {
return true;
}

if (isInPathItemElement || isChildOfOperationElement) {
return pathTemplateASTIncludesParameter(getAST(getPathTemplate()));
}

return true;
}
return true;
},
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
swagger: '2.0'
info:
title: Foo
version: 0.1.0
paths:
/foo/{bar_id}:
delete:
summary: Delete foo bar id
operationId: deleteFooBar
parameters:
- name: test_id
in: path
required: true
type: string
schema:
type: string
format: uuid
title: Test Id
responses:
'200':
description: Successful Response
content:
application/json:
schema: {}
parameters:
- name: foo_id
in: path
required: true
type: string
schema:
type: string
format: uuid
title: Foo Id
- name: bar_id
in: path
required: true
type: string
schema:
type: string
format: uuid
title: Foo Id
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
openapi: 3.0.0
info:
title: Foo
version: 0.1.0
paths:
/foo/{bar_id}:
delete:
summary: Delete foo bar id
operationId: deleteFooBar
parameters:
- name: test_id
in: path
required: true
schema:
type: string
format: uuid
title: Test Id
responses:
'200':
description: Successful Response
content:
application/json:
schema: {}
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
openapi: 3.1.0
info:
title: Foo
version: 0.1.0
paths:
/foo/{bar_id}:
delete:
summary: Delete foo bar id
operationId: deleteFooBar
parameters:
- name: test_id
in: path
required: true
schema:
type: string
format: uuid
title: Test Id
responses:
'200':
description: Successful Response
content:
application/json:
schema: {}
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
156 changes: 156 additions & 0 deletions packages/apidom-ls/test/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3549,4 +3549,160 @@ describe('apidom-ls-validate', function () {

languageService.terminate();
});

it('oas 2.0 / yaml - parameter object should be defined within path template', async function () {
const validationContext: ValidationContext = {
comments: DiagnosticSeverity.Error,
maxNumberOfProblems: 100,
relatedInformation: false,
};

const spec = fs
.readFileSync(
path.join(
__dirname,
'fixtures',
'validation',
'oas',
'parameter-defined-within-path-template-2-0.yaml',
),
)
.toString();
const doc: TextDocument = TextDocument.create(
'foo://bar/parameter-defined-within-path-template-2-0.yaml',
'yaml',
0,
spec,
);

const languageService: LanguageService = getLanguageService(contextNoSchema);

const result = await languageService.doValidation(doc, validationContext);
const expected: Diagnostic[] = [
{
range: { start: { line: 10, character: 8 }, end: { line: 17, character: 24 } },
message: 'parameter is not defined within path template',
severity: 1,
code: 3102000,
source: 'apilint',
},
{
range: { start: { line: 25, character: 8 }, end: { line: 32, character: 23 } },
message: 'parameter is not defined within path template',
severity: 1,
code: 3102000,
source: 'apilint',
},
];
assert.deepEqual(result, expected as Diagnostic[]);

languageService.terminate();
});

it('oas 3.0 / yaml - parameter object should be defined within path template', async function () {
const validationContext: ValidationContext = {
comments: DiagnosticSeverity.Error,
maxNumberOfProblems: 100,
relatedInformation: false,
};

const spec = fs
.readFileSync(
path.join(
__dirname,
'fixtures',
'validation',
'oas',
'parameter-defined-within-path-template-3-0.yaml',
),
)
.toString();
const doc: TextDocument = TextDocument.create(
'foo://bar/parameter-defined-within-path-template-3-0.yaml',
'yaml',
0,
spec,
);

const languageService: LanguageService = getLanguageService(contextNoSchema);

const result = await languageService.doValidation(doc, validationContext);
const expected: Diagnostic[] = [
{
range: { start: { line: 10, character: 8 }, end: { line: 16, character: 24 } },
message: 'parameter is not defined within path template',
severity: 1,
code: 3102000,
source: 'apilint',
},
{
range: { start: { line: 24, character: 8 }, end: { line: 30, character: 23 } },
message: 'parameter is not defined within path template',
severity: 1,
code: 3102000,
source: 'apilint',
},
];
assert.deepEqual(result, expected as Diagnostic[]);

languageService.terminate();
});

it('oas 3.1 / yaml - parameter object should be defined within path template', async function () {
const validationContext: ValidationContext = {
comments: DiagnosticSeverity.Error,
maxNumberOfProblems: 100,
relatedInformation: false,
};

const spec = fs
.readFileSync(
path.join(
__dirname,
'fixtures',
'validation',
'oas',
'parameter-defined-within-path-template-3-1.yaml',
),
)
.toString();
const doc: TextDocument = TextDocument.create(
'foo://bar/parameter-defined-within-path-template-3-1.yaml',
'yaml',
0,
spec,
);

const languageService: LanguageService = getLanguageService(contextNoSchema);

const result = await languageService.doValidation(doc, validationContext);
const expected: Diagnostic[] = [
{
range: { start: { line: 10, character: 8 }, end: { line: 16, character: 24 } },
message: 'parameter is not defined within path template',
severity: 1,
code: 3102000,
source: 'apilint',
},
{
code: 3102000,
message: 'parameter is not defined within path template',
range: {
end: {
character: 23,
line: 30,
},
start: {
character: 8,
line: 24,
},
},
severity: 1,
source: 'apilint',
},
];
assert.deepEqual(result, expected as Diagnostic[]);

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