diff --git a/docs/resources/built-in-rules.md b/docs/resources/built-in-rules.md index 1f145c6156..ed52bb8f27 100644 --- a/docs/resources/built-in-rules.md +++ b/docs/resources/built-in-rules.md @@ -365,6 +365,11 @@ response-contains-header: - title ``` + +### scalar-property-missing-example + +Verifies that every scalar property has an example defined. + ## Recommended config There are three built-in configurations: diff --git a/packages/core/src/config/all.ts b/packages/core/src/config/all.ts index 4398758d82..7c0916ed07 100644 --- a/packages/core/src/config/all.ts +++ b/packages/core/src/config/all.ts @@ -42,6 +42,7 @@ export default { spec: 'error', 'no-invalid-schema-examples': 'error', 'no-invalid-parameter-examples': 'error', + 'scalar-property-missing-example': 'error', }, oas3_0Rules: { 'no-invalid-media-type-examples': 'error', diff --git a/packages/core/src/rules/common/__tests__/scalar-property-missing-example.test.ts b/packages/core/src/rules/common/__tests__/scalar-property-missing-example.test.ts new file mode 100644 index 0000000000..a64f4107fd --- /dev/null +++ b/packages/core/src/rules/common/__tests__/scalar-property-missing-example.test.ts @@ -0,0 +1,207 @@ +import { outdent } from 'outdent'; +import { lintDocument } from '../../../lint'; +import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils'; +import { BaseResolver } from '../../../resolve'; + +describe('Oas3 scalar-property-missing-example', () => { + it('should report on a scalar property missing example', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.0.0 + components: + schemas: + User: + type: object + properties: + email: + description: User email address + type: string + format: email + `, + 'foobar.yaml', + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ 'scalar-property-missing-example': 'error' }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + Array [ + Object { + "location": Array [ + Object { + "pointer": "#/components/schemas/User/properties/email", + "reportOnKey": true, + "source": "foobar.yaml", + }, + ], + "message": "Scalar property should have \\"example\\" defined.", + "ruleId": "scalar-property-missing-example", + "severity": "error", + "suggest": Array [], + }, + ] + `); + }); +}); + +describe('Oas3.1 scalar-property-missing-example', () => { + it('should report on a scalar property missing example', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.1.0 + components: + schemas: + User: + type: object + properties: + email: + description: User email address + type: string + format: email + `, + 'foobar.yaml', + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ 'scalar-property-missing-example': 'error' }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + Array [ + Object { + "location": Array [ + Object { + "pointer": "#/components/schemas/User/properties/email", + "reportOnKey": true, + "source": "foobar.yaml", + }, + ], + "message": "Scalar property should have \\"example\\" or \\"examples\\" defined.", + "ruleId": "scalar-property-missing-example", + "severity": "error", + "suggest": Array [], + }, + ] + `); + }); + + it('should not report on a scalar property with an example', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.1.0 + components: + schemas: + User: + type: object + properties: + email: + description: User email address + type: string + format: email + example: john.smith@example.com + `, + 'foobar.yaml', + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ 'scalar-property-missing-example': 'error' }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`); + }); + + it('should not report on a scalar property with an examples', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.1.0 + components: + schemas: + User: + type: object + properties: + email: + description: User email address + type: string + format: email + examples: + - john.smith@example.com + - other@example.com + `, + 'foobar.yaml', + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ 'scalar-property-missing-example': 'error' }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`); + }); + + it('should not report on a non-scalar property missing an example', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.1.0 + components: + schemas: + Pet: + type: object + required: + - photoUrls + properties: + photoUrls: + description: The list of URL to a cute photos featuring pet + type: array + maxItems: 20 + xml: + name: photoUrl + wrapped: true + items: + type: string + format: url + `, + 'foobar.yaml', + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ 'scalar-property-missing-example': 'error' }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`); + }); + + it('should not report on a scalar property of binary format missing an example', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.1.0 + components: + schemas: + User: + type: object + properties: + responses: + type: string + format: binary + `, + 'foobar.yaml', + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ 'scalar-property-missing-example': 'error' }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`); + }); +}); diff --git a/packages/core/src/rules/common/scalar-property-missing-example.ts b/packages/core/src/rules/common/scalar-property-missing-example.ts new file mode 100644 index 0000000000..03da824580 --- /dev/null +++ b/packages/core/src/rules/common/scalar-property-missing-example.ts @@ -0,0 +1,55 @@ +import type { Oas2Rule, Oas3Rule } from '../../visitors'; +import type { UserContext } from '../../walk'; +import type { Oas2Schema } from '../../typings/swagger'; +import type { Oas3Schema, Oas3_1Schema } from '../../typings/openapi'; +import { OasVersion } from '../../oas-types'; + +const SCALAR_TYPES = ['string', 'integer', 'number', 'boolean', 'null']; + +export const ScalarPropertyMissingExample: Oas3Rule | Oas2Rule = () => { + return { + SchemaProperties( + properties: { [name: string]: Oas2Schema | Oas3Schema | Oas3_1Schema }, + { report, location, oasVersion, resolve }: UserContext, + ) { + for (const propName of Object.keys(properties)) { + const propSchema = resolve(properties[propName]).node; + + if (!propSchema || !isScalarSchema(propSchema)) { + continue; + } + + if (!propSchema.example && !(propSchema as Oas3_1Schema).examples) { + report({ + message: `Scalar property should have "example"${ + oasVersion === OasVersion.Version3_1 ? ' or "examples"' : '' + } defined.`, + location: location.child(propName).key(), + }); + } + } + }, + }; +}; + +function isScalarSchema(schema: Oas2Schema | Oas3Schema | Oas3_1Schema) { + if (!schema.type) { + return false; + } + + if (schema.allOf || (schema as Oas3Schema).anyOf || (schema as Oas3Schema).oneOf) { + // Skip allOf/oneOf/anyOf as it's complicated to validate it right now. + // We need core support for checking contrstrains through those keywords. + return false; + } + + if (schema.format === 'binary') { + return false; + } + + if (Array.isArray(schema.type)) { + return schema.type.every((t) => SCALAR_TYPES.includes(t)); + } + + return SCALAR_TYPES.includes(schema.type); +} diff --git a/packages/core/src/rules/oas2/index.ts b/packages/core/src/rules/oas2/index.ts index 9a50d35ddb..e16d83dde7 100644 --- a/packages/core/src/rules/oas2/index.ts +++ b/packages/core/src/rules/oas2/index.ts @@ -39,6 +39,7 @@ import { ResponseMimeType } from './response-mime-type'; import { PathSegmentPlural } from '../common/path-segment-plural'; import { ResponseContainsHeader } from '../common/response-contains-header'; import { ResponseContainsProperty } from './response-contains-property'; +import { ScalarPropertyMissingExample } from '../common/scalar-property-missing-example'; export const rules = { spec: OasSpec as Oas2Rule, @@ -82,6 +83,7 @@ export const rules = { 'path-segment-plural': PathSegmentPlural as Oas2Rule, 'response-contains-header': ResponseContainsHeader as Oas2Rule, 'response-contains-property': ResponseContainsProperty as Oas2Rule, + 'scalar-property-missing-example': ScalarPropertyMissingExample, }; export const preprocessors = {}; diff --git a/packages/core/src/rules/oas3/index.ts b/packages/core/src/rules/oas3/index.ts index cccb2cd050..48026f1a7e 100644 --- a/packages/core/src/rules/oas3/index.ts +++ b/packages/core/src/rules/oas3/index.ts @@ -47,6 +47,7 @@ import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples'; import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples'; import { ResponseContainsHeader } from '../common/response-contains-header'; import { ResponseContainsProperty } from './response-contains-property'; +import { ScalarPropertyMissingExample } from '../common/scalar-property-missing-example'; export const rules = { spec: OasSpec, @@ -98,6 +99,7 @@ export const rules = { 'no-invalid-parameter-examples': NoInvalidParameterExamples, 'response-contains-header': ResponseContainsHeader, 'response-contains-property': ResponseContainsProperty, + 'scalar-property-missing-example': ScalarPropertyMissingExample, } as Oas3RuleSet; export const preprocessors = {}; diff --git a/packages/core/src/typings/openapi.ts b/packages/core/src/typings/openapi.ts index 1aec56db80..a00ce1160b 100644 --- a/packages/core/src/typings/openapi.ts +++ b/packages/core/src/typings/openapi.ts @@ -153,7 +153,8 @@ export interface Oas3Schema { 'x-tags'?: string[]; } -export interface Oas3_1Schema extends Oas3Schema { +export type Oas3_1Schema = Oas3Schema & { + type?: string | string[]; examples?: any[]; }