Open savage-alex opened 2 years ago
I've solved the same problem by creating custom function base on oasExample:
rules.yaml:
oas3-valid-schema-example-strict:
severity: error
message: "{{error}}"
recommended: true
formats: ["oas3"]
given:
- "$.components.schemas..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0) && (@.enum || @.type || @.format || @.$ref || @.properties || @.items))]"
- "$..content..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0) && (@.enum || @.type || @.format || @.$ref || @.properties || @.items))]"
- "$..headers..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0) && (@.enum || @.type || @.format || @.$ref || @.properties || @.items))]"
- "$..parameters..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0) && (@.enum || @.type || @.format || @.$ref || @.properties || @.items))]"
then:
function: oasExampleStrict
functionOptions:
schemaField: "$"
oasVersion: 3
type: "schema"
oas3-valid-media-example-strict:
severity: error
message: "{{error}}"
recommended: true
formats: ["oas3"]
given:
- "$..content..[?(@ && @.schema && (@.example !== void 0 || @.examples))]"
- "$..headers..[?(@ && @.schema && (@.example !== void 0 || @.examples))]"
- "$..parameters..[?(@ && @.schema && (@.example !== void 0 || @.examples))]"
then:
function: oasExampleStrict
functionOptions:
schemaField: "schema"
oasVersion: 3
type: "media"
oasExampleStrict.ts
import { isObject } from './isObject';
import type { Dictionary, JsonPath, Optional } from '@stoplight/types';
import oasSchema, { Options as SchemaOptions } from './oasSchema';
import { createRulesetFunction, IFunctionResult } from '@stoplight/spectral-core';
import { oas2 } from '@stoplight/spectral-formats';
import { apierr } from './sm_common';
export type Options = {
oasVersion: 2 | 3;
schemaField: string;
type: 'media' | 'schema';
};
type MediaValidationItem = {
field: string;
multiple: boolean;
keyed: boolean;
};
const MEDIA_VALIDATION_ITEMS: Dictionary<MediaValidationItem[], 2 | 3> = {
2: [
{
field: 'examples',
multiple: true,
keyed: false,
},
],
3: [
{
field: 'example',
multiple: false,
keyed: false,
},
{
field: 'examples',
multiple: true,
keyed: true,
},
],
};
const SCHEMA_VALIDATION_ITEMS: Dictionary<string[], 2 | 3> = {
2: ['example', 'x-example', 'default'],
3: ['example', 'default'],
};
type ValidationItem = {
value: unknown;
path: JsonPath;
};
function* getMediaValidationItems(
items: MediaValidationItem[],
targetVal: Dictionary<unknown>,
givenPath: JsonPath,
oasVersion: 2 | 3,
): Iterable<ValidationItem> {
for (const { field, keyed, multiple } of items) {
if (!(field in targetVal)) {
continue;
}
const value = targetVal[field];
if (multiple) {
if (!isObject(value)) continue;
for (const exampleKey of Object.keys(value)) {
const exampleValue = value[exampleKey];
if (oasVersion === 3 && keyed && (!isObject(exampleValue) || 'externalValue' in exampleValue)) {
// should be covered by oas3-examples-value-or-externalValue
continue;
}
const targetPath = [...givenPath, field, exampleKey];
if (keyed) {
targetPath.push('value');
}
yield {
value: keyed && isObject(exampleValue) ? exampleValue.value : exampleValue,
path: targetPath,
};
}
return;
} else {
return yield {
value,
path: [...givenPath, field],
};
}
}
}
function* getSchemaValidationItems(
fields: string[],
targetVal: Record<string, unknown>,
givenPath: JsonPath,
): Iterable<ValidationItem> {
for (const field of fields) {
if (!(field in targetVal)) {
continue;
}
yield {
value: targetVal[field],
path: [...givenPath, field],
};
}
}
export default createRulesetFunction<Record<string, unknown>, Options>(
{
input: {
type: 'object',
},
options: {
type: 'object',
properties: {
oasVersion: {
enum: [2, 3],
},
schemaField: {
type: 'string',
},
type: {
enum: ['media', 'schema'],
},
},
additionalProperties: false,
},
},
function oasExample(targetVal, opts, context) {
const formats = context.document.formats;
const schemaOpts: SchemaOptions = {
schema: opts.schemaField === '$' ? targetVal : (targetVal[opts.schemaField] as SchemaOptions['schema']),
};
let results: Optional<IFunctionResult[]> = void 0;
const validationItems =
opts.type === 'schema'
? getSchemaValidationItems(SCHEMA_VALIDATION_ITEMS[opts.oasVersion], targetVal, context.path)
: getMediaValidationItems(MEDIA_VALIDATION_ITEMS[opts.oasVersion], targetVal, context.path, opts.oasVersion);
schemaOpts.schema = Object.assign({}, schemaOpts.schema);
disableAdditionalProperties(schemaOpts.schema);
if (formats?.has(oas2) && 'required' in schemaOpts.schema && typeof schemaOpts.schema.required === 'boolean') {
schemaOpts.schema = { ...schemaOpts.schema };
delete schemaOpts.schema.required;
}
for (const validationItem of validationItems) {
const result = oasSchema(validationItem.value, schemaOpts, {
...context,
path: validationItem.path,
});
if (Array.isArray(result)) {
if (results === void 0) results = [];
results.push(...result);
}
}
return results;
},
);
function disableAdditionalProperties(schema) {
if (schema.type == "object") {
schema.additionalProperties = false;
if (schema.properties && isObject(schema.properties)) {
schema.properties = Object.assign({}, schema.properties);
for (const propName in schema.properties) {
schema.properties[propName] = Object.assign({}, schema.properties[propName]);
disableAdditionalProperties(schema.properties[propName]);
}
}
}
}
It would be good to have an option in oasExample function and such rules as an optional part of base rules anyway.
User story. As an API designer I want to ensure my examples are correctly spelled and typed for properties that are in my API definition So that any consumers of mocked endpoints do not get incorrect properties
Is your feature request related to a problem? Spectral can find examples that are bad when additionalProperties is set to false but its not something we want to do when we release API definitions as its stops evolution
Describe the solution you'd like A mode for spectral to lint the examples against the definition and to ensure no additionalProperties are present (expect it to be a additional mode)
Additional context Add any other context or screenshots about the feature request here.