Skip to content

Commit 4576ad9

Browse files
committed
feat: add validation ruls for tags to have unique name
1 parent 774fd62 commit 4576ad9

File tree

7 files changed

+107
-0
lines changed

7 files changed

+107
-0
lines changed

packages/apidom-ls/src/config/codes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,7 @@ enum ApilintCodes {
791791
OPENAPI2_TAG_FIELD_NAME_REQUIRED,
792792
OPENAPI2_TAG_FIELD_DESCRIPTION_TYPE = 3190200,
793793
OPENAPI2_TAG_FIELD_EXTERNAL_DOCS_TYPE = 3190300,
794+
OPENAPI2_TAG_FIELD_UNIQUE_NAME_VALUE_TYPE = 3190400,
794795

795796
OPENAPI2_XML = 3200000,
796797
OPENAPI2_XML_FIELD_NAME_TYPE = 3200100,

packages/apidom-ls/src/config/openapi/tag/documentation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { OpenAPI2, OpenAPI30, OpenAPI31, OpenAPI3 } from '../target-specs.ts';
1313
const documentation = [
1414
{ target: 'name', docs: '**Required.** The name of the tag.', targetSpecs: OpenAPI2 },
1515
{ target: 'name', docs: '**REQUIRED**. The name of the tag.', targetSpecs: OpenAPI3 },
16+
{ target: 'name', docs: 'Tag Objects must have unique `name` field values.', targetSpecs: OpenAPI3 },
17+
{ target: 'name', docs: 'Tag Objects must have unique `name` field values.', targetSpecs: OpenAPI2 },
1618
{
1719
target: 'description',
1820
docs: 'A short description for the tag. [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) can be used for rich text representation.',

packages/apidom-ls/src/config/openapi/tag/lint/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import nameTypeLint from './name--type.ts';
33
import nameRequiredLint from './name--required.ts';
44
import descriptionTypeLint from './description--type.ts';
55
import externalDocsTypeLint from './external-docs--type.ts';
6+
import nameUniqueLint from './name--unique.ts';
67

78
const lints = [
89
allowedFieldsLint,
910
nameTypeLint,
1011
nameRequiredLint,
12+
nameUniqueLint,
1113
descriptionTypeLint,
1214
externalDocsTypeLint,
1315
];
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { DiagnosticSeverity } from 'vscode-languageserver-types';
2+
3+
import ApilintCodes from '../../../codes.ts';
4+
import { LinterMeta } from '../../../../apidom-language-types.ts';
5+
import { OpenAPI2, OpenAPI3 } from '../../target-specs.ts';
6+
7+
const nameUniqueLint: LinterMeta = {
8+
code: ApilintCodes.OPENAPI2_TAG_FIELD_UNIQUE_NAME_VALUE_TYPE,
9+
source: 'apilint',
10+
message: "Tag Objects must have unique `name` field values.",
11+
severity: DiagnosticSeverity.Error,
12+
linterFunction: 'apilintPropertyUniqueSiblingValue',
13+
linterParams: ['tags', 'name'],
14+
marker: 'value',
15+
target: 'name',
16+
markerTarget: 'name',
17+
data: {},
18+
targetSpecs: [...OpenAPI2, ...OpenAPI3],
19+
};
20+
21+
export default nameUniqueLint;
22+

packages/apidom-ls/src/services/validation/linter-functions.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,30 @@ export const standardLinterfunctions: FunctionItem[] = [
795795
return true;
796796
},
797797
},
798+
{
799+
functionName: 'apilintPropertyUniqueSiblingValue',
800+
function: (element, elementOrClasses, key) => {
801+
const value = toValue(element);
802+
803+
const filterSiblingsOAS2 = (
804+
el: Element & { key?: { content?: string }; content: { value?: string } },
805+
) => isString(el) && el.key?.content === key && toValue(el.content.value) === value;
806+
807+
const filterSiblingsOAS3 = (el: Element) =>
808+
isObject(el) && el.hasKey(key) && toValue(el.get(key)) === value;
809+
810+
const elements = filter((el: Element) => {
811+
const classes: string[] = toValue(el.getMetaProperty('classes', []));
812+
813+
return (
814+
(elementOrClasses.includes(el.element) ||
815+
classes.every((v) => elementOrClasses.includes(v))) &&
816+
(filterSiblingsOAS2(el) || filterSiblingsOAS3(el))
817+
);
818+
}, element.parent?.parent?.parent);
819+
return elements.length <= 1;
820+
},
821+
},
798822
{
799823
functionName: 'apilintChannelParameterExist',
800824
function: (element: Element): boolean => {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
openapi: 3.0.0
2+
info:
3+
title: test
4+
version: 1.0.12
5+
tags:
6+
- name: pet
7+
description: Everything about your Pets
8+
externalDocs:
9+
description: Find out more
10+
url: http://swagger.io
11+
- name: pet
12+
description: Access to Petstore orders
13+
externalDocs:
14+
description: Find out more about our store
15+
url: http://swagger.io
16+
- name: user
17+
description: Operations about user
18+
paths:

packages/apidom-ls/test/validate.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3788,4 +3788,42 @@ describe('apidom-ls-validate', function () {
37883788

37893789
languageService.terminate();
37903790
});
3791+
3792+
it('oas - tags name should have unique values', async function () {
3793+
const spec = fs
3794+
.readFileSync(
3795+
path.join(__dirname, 'fixtures', 'validation', 'oas', 'tags-unique-name.yaml'),
3796+
)
3797+
.toString();
3798+
const doc: TextDocument = TextDocument.create(
3799+
'foo://bar/tags-unique-name.yaml',
3800+
'yaml',
3801+
0,
3802+
spec,
3803+
);
3804+
const languageService: LanguageService = getLanguageService(contextNoSchema);
3805+
3806+
const result = await languageService.doValidation(doc);
3807+
const expected: Diagnostic[] = [
3808+
{
3809+
message: 'Tag Objects must have unique `name` field values.',
3810+
severity: 1,
3811+
code: 3190400,
3812+
source: 'apilint',
3813+
data: {},
3814+
range: { start: { line: 5, character: 10 }, end: { line: 5, character: 13 } },
3815+
},
3816+
{
3817+
message: 'Tag Objects must have unique `name` field values.',
3818+
severity: 1,
3819+
code: 3190400,
3820+
source: 'apilint',
3821+
data: {},
3822+
range: { start: { line: 10, character: 10 }, end: { line: 10, character: 13 } },
3823+
},
3824+
];
3825+
assert.deepEqual(result, expected);
3826+
3827+
languageService.terminate();
3828+
})
37913829
});

0 commit comments

Comments
 (0)