-
-
Notifications
You must be signed in to change notification settings - Fork 96
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: move spectral ruleset to source code (#699)
- Loading branch information
1 parent
b135b9c
commit 066b9b4
Showing
71 changed files
with
20,874 additions
and
383 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/* eslint-disable security/detect-unsafe-regex */ | ||
|
||
import { isObject } from '../utils'; | ||
|
||
import type { Format } from '@stoplight/spectral-core'; | ||
import type { MaybeAsyncAPI } from '../types'; | ||
|
||
const aas2Regex = /^2\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/; | ||
const aas2_0Regex = /^2\.0(?:\.[0-9]*)?$/; | ||
const aas2_1Regex = /^2\.1(?:\.[0-9]*)?$/; | ||
const aas2_2Regex = /^2\.2(?:\.[0-9]*)?$/; | ||
const aas2_3Regex = /^2\.3(?:\.[0-9]*)?$/; | ||
const aas2_4Regex = /^2\.4(?:\.[0-9]*)?$/; | ||
const aas2_5Regex = /^2\.5(?:\.[0-9]*)?$/; | ||
const aas2_6Regex = /^2\.6(?:\.[0-9]*)?$/; | ||
|
||
const isAas2 = (document: unknown): document is { asyncapi: string } & Record<string, unknown> => | ||
isObject(document) && 'asyncapi' in document && aas2Regex.test(String((document as MaybeAsyncAPI).asyncapi)); | ||
|
||
export const aas2: Format = isAas2; | ||
aas2.displayName = 'AsyncAPI 2.x'; | ||
|
||
export const aas2_0: Format = (document: unknown): boolean => | ||
isAas2(document) && aas2_0Regex.test(String((document as MaybeAsyncAPI).asyncapi)); | ||
aas2_0.displayName = 'AsyncAPI 2.0.x'; | ||
|
||
export const aas2_1: Format = (document: unknown): boolean => | ||
isAas2(document) && aas2_1Regex.test(String((document as MaybeAsyncAPI).asyncapi)); | ||
aas2_1.displayName = 'AsyncAPI 2.1.x'; | ||
|
||
export const aas2_2: Format = (document: unknown): boolean => | ||
isAas2(document) && aas2_2Regex.test(String((document as MaybeAsyncAPI).asyncapi)); | ||
aas2_2.displayName = 'AsyncAPI 2.2.x'; | ||
|
||
export const aas2_3: Format = (document: unknown): boolean => | ||
isAas2(document) && aas2_3Regex.test(String((document as MaybeAsyncAPI).asyncapi)); | ||
aas2_3.displayName = 'AsyncAPI 2.3.x'; | ||
|
||
export const aas2_4: Format = (document: unknown): boolean => | ||
isAas2(document) && aas2_4Regex.test(String((document as MaybeAsyncAPI).asyncapi)); | ||
aas2_4.displayName = 'AsyncAPI 2.4.x'; | ||
|
||
export const aas2_5: Format = (document: unknown): boolean => | ||
isAas2(document) && aas2_5Regex.test(String((document as MaybeAsyncAPI).asyncapi)); | ||
aas2_5.displayName = 'AsyncAPI 2.5.x'; | ||
|
||
export const aas2_6: Format = (document: unknown): boolean => | ||
isAas2(document) && aas2_6Regex.test(String((document as MaybeAsyncAPI).asyncapi)); | ||
aas2_6.displayName = 'AsyncAPI 2.6.x'; | ||
|
||
export const aas2All = [aas2_0, aas2_1, aas2_2, aas2_3, aas2_4, aas2_5, aas2_6]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import specs from '@asyncapi/specs'; | ||
import { createRulesetFunction } from '@stoplight/spectral-core'; | ||
import { schema as schemaFn } from '@stoplight/spectral-functions'; | ||
import { aas2_0, aas2_1, aas2_2, aas2_3, aas2_4, aas2_5, aas2_6 } from '../formats'; | ||
|
||
// import type { ErrorObject } from 'ajv'; | ||
import type { IFunctionResult, Format } from '@stoplight/spectral-core'; | ||
|
||
type AsyncAPIVersions = keyof typeof specs; | ||
|
||
function getCopyOfSchema(version: AsyncAPIVersions): Record<string, unknown> { | ||
return JSON.parse(JSON.stringify(specs[version])) as Record<string, unknown>; | ||
} | ||
|
||
const serializedSchemas = new Map<AsyncAPIVersions, Record<string, unknown>>(); | ||
function getSerializedSchema(version: AsyncAPIVersions): Record<string, unknown> { | ||
const schema = serializedSchemas.get(version); | ||
if (schema) { | ||
return schema; | ||
} | ||
|
||
// Copy to not operate on the original json schema - between imports (in different modules) we operate on this same schema. | ||
const copied = getCopyOfSchema(version) as { definitions: Record<string, unknown> }; | ||
// Remove the meta schemas because they are already present within Ajv, and it's not possible to add duplicated schemas. | ||
delete copied.definitions['http://json-schema.org/draft-07/schema']; | ||
delete copied.definitions['http://json-schema.org/draft-04/schema']; | ||
|
||
serializedSchemas.set(version, copied); | ||
return copied; | ||
} | ||
|
||
const refErrorMessage = 'Property "$ref" is not expected to be here'; | ||
function filterRefErrors(errors: IFunctionResult[], resolved: boolean) { | ||
if (resolved) { | ||
return errors.filter(err => err.message !== refErrorMessage); | ||
} | ||
|
||
return errors | ||
.filter(err => err.message === refErrorMessage) | ||
.map(err => { | ||
err.message = 'Referencing in this place is not allowed'; | ||
return err; | ||
}); | ||
} | ||
|
||
function getSchema(formats: Set<Format>): Record<string, any> | void { | ||
switch (true) { | ||
case formats.has(aas2_6): | ||
return getSerializedSchema('2.6.0'); | ||
case formats.has(aas2_5): | ||
return getSerializedSchema('2.5.0'); | ||
case formats.has(aas2_4): | ||
return getSerializedSchema('2.4.0'); | ||
case formats.has(aas2_3): | ||
return getSerializedSchema('2.3.0'); | ||
case formats.has(aas2_2): | ||
return getSerializedSchema('2.2.0'); | ||
case formats.has(aas2_1): | ||
return getSerializedSchema('2.1.0'); | ||
case formats.has(aas2_0): | ||
return getSerializedSchema('2.0.0'); | ||
default: | ||
return; | ||
} | ||
} | ||
|
||
export const documentStructure = createRulesetFunction<unknown, { resolved: boolean }>( | ||
{ | ||
input: null, | ||
options: { | ||
type: 'object', | ||
properties: { | ||
resolved: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
required: ['resolved'], | ||
}, | ||
}, | ||
(targetVal, options, context) => { | ||
const formats = context.document?.formats; | ||
if (!formats) { | ||
return; | ||
} | ||
|
||
const schema = getSchema(formats); | ||
if (!schema) { | ||
return; | ||
} | ||
|
||
const errors = schemaFn(targetVal, { allErrors: true, schema }, context); | ||
if (!Array.isArray(errors)) { | ||
return; | ||
} | ||
|
||
return filterRefErrors(errors, options.resolved); | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { createRulesetFunction } from '@stoplight/spectral-core'; | ||
|
||
export const internal = createRulesetFunction<null, null>( | ||
{ | ||
input: null, | ||
options: null, | ||
}, | ||
(_, __, { document, documentInventory }) => { | ||
// adding document inventory in document - we need it in custom operations to resolve all circular refs | ||
(document as any).__documentInventory = documentInventory; | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { createRulesetFunction } from '@stoplight/spectral-core'; | ||
import { specVersions, lastVersion } from '../../constants'; | ||
import { isObject } from '../../utils'; | ||
|
||
import { MaybeAsyncAPI } from '../../types'; | ||
|
||
export const isAsyncAPIDocument = createRulesetFunction<MaybeAsyncAPI, null>( | ||
{ | ||
input: null, | ||
options: null, | ||
}, | ||
(targetVal) => { | ||
if (!isObject(targetVal) || typeof targetVal.asyncapi !== 'string') { | ||
return [ | ||
{ | ||
message: 'This is not an AsyncAPI document. The "asyncapi" field as string is missing.', | ||
path: [], | ||
} | ||
]; | ||
} | ||
|
||
if (!specVersions.includes(targetVal.asyncapi)) { | ||
return [ | ||
{ | ||
message: `Version "${targetVal.asyncapi}" is not supported. Please use "${lastVersion}" (latest) version of the specification.`, | ||
path: [], | ||
} | ||
]; | ||
} | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { createRulesetFunction } from '@stoplight/spectral-core'; | ||
|
||
import type { IFunctionResult } from '@stoplight/spectral-core'; | ||
|
||
type Tags = Array<{ name: string }>; | ||
|
||
function getDuplicateTagsIndexes(tags: Tags): number[] { | ||
return tags | ||
.map(item => item.name) | ||
.reduce<number[]>((acc, item, i, arr) => { | ||
if (arr.indexOf(item) !== i) { | ||
acc.push(i); | ||
} | ||
return acc; | ||
}, []); | ||
} | ||
|
||
export const uniquenessTags = createRulesetFunction<Tags, null>( | ||
{ | ||
input: { | ||
type: 'array', | ||
items: { | ||
type: 'object', | ||
properties: { | ||
name: { | ||
type: 'string', | ||
}, | ||
}, | ||
required: ['name'], | ||
}, | ||
}, | ||
options: null, | ||
}, | ||
(targetVal, _, ctx) => { | ||
const duplicatedTags = getDuplicateTagsIndexes(targetVal); | ||
if (duplicatedTags.length === 0) { | ||
return []; | ||
} | ||
|
||
const results: IFunctionResult[] = []; | ||
for (const duplicatedIndex of duplicatedTags) { | ||
const duplicatedTag = targetVal[duplicatedIndex].name; | ||
results.push({ | ||
message: `"tags" object contains duplicate tag name "${duplicatedTag}".`, | ||
path: [...ctx.path, duplicatedIndex, 'name'], | ||
}); | ||
} | ||
return results; | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { unreferencedReusableObject } from '@stoplight/spectral-functions'; | ||
import { createRulesetFunction } from '@stoplight/spectral-core'; | ||
import { isObject } from '../../utils'; | ||
|
||
import type { IFunctionResult } from '@stoplight/spectral-core'; | ||
|
||
export const unusedComponent = createRulesetFunction<{ components: Record<string, unknown> }, null>( | ||
{ | ||
input: { | ||
type: 'object', | ||
properties: { | ||
components: { | ||
type: 'object', | ||
}, | ||
}, | ||
required: ['components'], | ||
}, | ||
options: null, | ||
}, | ||
(targetVal, _, context) => { | ||
const components = targetVal.components; | ||
|
||
const results: IFunctionResult[] = []; | ||
Object.keys(components).forEach(componentType => { | ||
const value = components[componentType]; | ||
if (!isObject(value)) { | ||
return; | ||
} | ||
|
||
const resultsForType = unreferencedReusableObject( | ||
value, | ||
{ reusableObjectsLocation: `#/components/${componentType}` }, | ||
context, | ||
); | ||
|
||
if (resultsForType && Array.isArray(resultsForType)) { | ||
results.push(...resultsForType); | ||
} | ||
}); | ||
return results; | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { coreRuleset, recommendedRuleset } from './ruleset'; | ||
import { v2CoreRuleset, v2SchemasRuleset, v2RecommendedRuleset } from './v2'; | ||
|
||
import type { Parser } from '../parser'; | ||
import type { RulesetDefinition } from '@stoplight/spectral-core'; | ||
|
||
export type RulesetOptions = | ||
| RulesetDefinition & { core?: boolean, recommended?: boolean } | ||
| RulesetDefinition | ||
| { core?: boolean, recommended?: boolean }; | ||
|
||
export function createRuleset(parser: Parser, options?: RulesetOptions): RulesetDefinition { | ||
const { core: useCore = true, recommended: useRecommended = true, ...rest } = (options || {}) as (RulesetDefinition & { core?: boolean, recommended?: boolean }); | ||
|
||
const extendedRuleset = [ | ||
useCore && coreRuleset, | ||
useRecommended && recommendedRuleset, | ||
useCore && v2CoreRuleset, | ||
useCore && v2SchemasRuleset(parser), | ||
useRecommended && v2RecommendedRuleset, | ||
...(options as any || {})?.extends || [], | ||
].filter(Boolean); | ||
|
||
return { | ||
...rest || {}, | ||
extends: extendedRuleset, | ||
} as RulesetDefinition; | ||
} |
Oops, something went wrong.