Skip to content

Commit

Permalink
feat(apidom-ls): create schema rule for missing core keywords
Browse files Browse the repository at this point in the history
Fix #3549
  • Loading branch information
frantuma committed Dec 18, 2023
1 parent db0e48f commit 4080236
Show file tree
Hide file tree
Showing 10 changed files with 276 additions and 0 deletions.
18 changes: 18 additions & 0 deletions packages/apidom-ls/src/config/asyncapi/target-specs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const AsyncAPI200 = [{ namespace: 'asyncapi', version: '2.0.0' }];
export const AsyncAPI210 = [{ namespace: 'asyncapi', version: '2.1.0' }];
export const AsyncAPI220 = [{ namespace: 'asyncapi', version: '2.2.0' }];
export const AsyncAPI230 = [{ namespace: 'asyncapi', version: '2.3.0' }];
export const AsyncAPI240 = [{ namespace: 'asyncapi', version: '2.4.0' }];
export const AsyncAPI250 = [{ namespace: 'asyncapi', version: '2.5.0' }];
export const AsyncAPI260 = [{ namespace: 'asyncapi', version: '2.6.0' }];

export const AsyncAPI2 = [
...AsyncAPI200,
...AsyncAPI210,
...AsyncAPI220,
...AsyncAPI230,
...AsyncAPI240,
...AsyncAPI250,
...AsyncAPI260,
];
export const AsyncAPI = [...AsyncAPI2];
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 @@ -66,6 +66,7 @@ enum ApilintCodes {
SCHEMA_EXAMPLE_DEPRECATED,
SCHEMA_TYPE_OPENAPI_3_0,
SCHEMA_NULLABLE_NOT_RECOMMENDED,
SCHEMA_MISSING_CORE_FIELDS,

DUPLICATE_KEYS = 14999,
NOT_ALLOWED_FIELDS = 15000,
Expand Down
2 changes: 2 additions & 0 deletions packages/apidom-ls/src/config/common/schema/lint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import minLengthTypeLint from './min-length--type';
import minPropertiesNonObjectLint from './min-properties--non-object';
import minPropertiesTypeLint from './min-properties--type';
import minimumPatternLint from './minimum--pattern';
import missingCoreFieldsOpenAPI3_0AndAsyncAPILint from './missing-core-fields-oas-3-0-asyncapi';
import multipleOfTypeLint from './multiple-of--type';
import notTypeLint from './not--type';
import nullableNotRecommendedLint from './nullable--not-recommended';
Expand Down Expand Up @@ -102,6 +103,7 @@ const schemaLints = [
minPropertiesNonObjectLint,
minPropertiesTypeLint,
minimumPatternLint,
missingCoreFieldsOpenAPI3_0AndAsyncAPILint,
multipleOfTypeLint,
notTypeLint,
nullableNotRecommendedLint,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { DiagnosticSeverity } from 'vscode-languageserver-types';

import ApilintCodes from '../../../codes';
import { LinterMeta } from '../../../../apidom-language-types';
import { OpenAPI3 } from '../../../openapi/target-specs';
import { AsyncAPI } from '../../../asyncapi/target-specs';

// eslint-disable-next-line @typescript-eslint/naming-convention
const missingCoreFieldsOpenAPI3_0AndAsyncAPILint: LinterMeta = {
code: ApilintCodes.SCHEMA_MISSING_CORE_FIELDS,
source: 'apilint',
message: 'Schema does not include any JSON Schema core keywords',
severity: DiagnosticSeverity.Hint,
linterFunction: 'existAnyOfFields',
linterParams: [
[
'$anchor',
'$comment',
'$dynamicAnchor',
'$id',
'$ref',
'$schema',
'$vocabulary',
'additionalItems',
'additionalProperties',
'allOf',
'anyOf',
'booleanSchemaValue',
'const',
'contains',
'contentEncoding',
'contentMediaType',
'contentSchema',
'default',
'dependentSchemas',
'deprecated',
'description',
'discriminator',
'else',
'examples',
'exclusiveMaximum',
'exclusiveMaximumValue',
'exclusiveMinimum',
'exclusiveMinimumValue',
'externalDocs',
'format',
'if',
'items',
'dependentRequired',
'maxContains',
'maximum',
'maxItems',
'maxLength',
'maxProperties',
'minContains',
'minimum',
'minItems',
'minLength',
'minProperties',
'multipleOf',
'name',
'not',
'nullable',
'extensions',
'oneOf',
'pattern',
'patternProperties',
'prefixItems',
'properties',
'propertyNames',
'enum',
'example',
'readOnly',
'required',
'then',
'title',
'type',
'unevaluatedItems',
'unevaluatedProperties',
'uniqueItems',
'writeOnly',
'xml',
],
true,
'boolean',
],
marker: 'key',
targetSpecs: [...OpenAPI3, ...AsyncAPI],
};

export default missingCoreFieldsOpenAPI3_0AndAsyncAPILint;
24 changes: 24 additions & 0 deletions packages/apidom-ls/src/services/validation/linter-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,30 @@ export const standardLinterfunctions: FunctionItem[] = [
return true;
},
},
{
functionName: 'existAnyOfFields',
function: (
element: Element,
keys: string[],
allowEmpty: boolean,
skipIfType?: string,
): boolean => {
if (element && isObject(element)) {
if (skipIfType && isType(element, skipIfType)) {
return true;
}
if (!element.keys() || element.keys().length === 0) {
return allowEmpty;
}
for (const key of keys) {
if (element.hasKey(key)) {
return true;
}
}
}
return false;
},
},
{
functionName: 'allowedFields',
function: (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
asyncapi: 2.1.0
info:
version: '1.0.0'
title: missing schema keywords
description: 'desc'
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0

servers:
production:
url: mqtt://test.mosquitto.org
protocol: mqtt
description: Test MQTT broker

channels:
user/signedup:
publish:
operationId: onUserSignUp
message:
$ref : '#/components/messages/UserSignedUp'

components:
messages:
UserSignedUp:
name: userSignedUp
title: User signed up event
summary: Inform about a new user registration in the system
contentType: application/json
payload:
$ref: '#/components/schemas/userSignedUpPayload'

schemas:
userSignedUpPayload:
type: object
properties:
anyOf:
otherKey: foo
good:
otherKey: foo
$dynamicAnchor: good
boolFalse: false
boolTrue: true
emptyObject: {}
24 changes: 24 additions & 0 deletions packages/apidom-ls/test/fixtures/validation/oas/issue3549.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: 3.1.0
info:
title: missing schema keywords
version: 1.0.0
paths:
/a:
get:
operationId: aget
responses:
'200':
description: aget
content:
application/json:
schema:
type: object
properties:
anyOf:
otherKey: foo
good:
otherKey: foo
$dynamicAnchor: good
boolFalse: false
boolTrue: true
emptyObject: {}
7 changes: 7 additions & 0 deletions packages/apidom-ls/test/openapi-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,13 @@ describe('apidom-ls', function () {
code: 7030101,
source: 'apilint',
},
{
range: { start: { line: 30, character: 10 }, end: { line: 30, character: 14 } },
message: 'Schema does not include any JSON Schema core keywords',
severity: 4,
code: 10072,
source: 'apilint',
},
{
range: { start: { line: 185, character: 20 }, end: { line: 190, character: 7 } },
message: 'parameters must be an array',
Expand Down
7 changes: 7 additions & 0 deletions packages/apidom-ls/test/openapi-yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,13 @@ describe('apidom-ls-yaml', function () {
code: 7030101,
source: 'apilint',
},
{
range: { start: { line: 28, character: 8 }, end: { line: 28, character: 10 } },
message: 'Schema does not include any JSON Schema core keywords',
severity: 4,
code: 10072,
source: 'apilint',
},
{
range: { start: { line: 128, character: 6 }, end: { line: 132, character: 0 } },
message: 'parameters must be an array',
Expand Down
58 changes: 58 additions & 0 deletions packages/apidom-ls/test/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3549,4 +3549,62 @@ describe('apidom-ls-validate', function () {

languageService.terminate();
});

it('oas / yaml - schema should have at least one JSON Schema core keyword - issue #3549', async function () {
const validationContext: ValidationContext = {
comments: DiagnosticSeverity.Error,
maxNumberOfProblems: 100,
relatedInformation: false,
};

const spec = fs
.readFileSync(path.join(__dirname, 'fixtures', 'validation', 'oas', 'issue3549.yaml'))
.toString();
const doc: TextDocument = TextDocument.create('foo://bar/issue3549.yaml', 'yaml', 0, spec);

const languageService: LanguageService = getLanguageService(contextNoSchema);

const result = await languageService.doValidation(doc, validationContext);
const expected: Diagnostic[] = [
{
range: { start: { line: 16, character: 18 }, end: { line: 16, character: 23 } },
message: 'Schema does not include any JSON Schema core keywords',
severity: 4,
code: 10072,
source: 'apilint',
},
];
assert.deepEqual(result, expected as Diagnostic[]);

languageService.terminate();
});

it('asyncapi / yaml - schema should have at least one JSON Schema core keyword - issue #3549', async function () {
const validationContext: ValidationContext = {
comments: DiagnosticSeverity.Error,
maxNumberOfProblems: 100,
relatedInformation: false,
};

const spec = fs
.readFileSync(path.join(__dirname, 'fixtures', 'validation', 'asyncapi', 'issue3549.yaml'))
.toString();
const doc: TextDocument = TextDocument.create('foo://bar/issue3549.yaml', 'yaml', 0, spec);

const languageService: LanguageService = getLanguageService(contextNoSchema);

const result = await languageService.doValidation(doc, validationContext);
const expected: Diagnostic[] = [
{
range: { start: { line: 36, character: 8 }, end: { line: 36, character: 13 } },
message: 'Schema does not include any JSON Schema core keywords',
severity: 4,
code: 10072,
source: 'apilint',
},
];
assert.deepEqual(result, expected as Diagnostic[]);

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

0 comments on commit 4080236

Please sign in to comment.