Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/resources/built-in-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/config/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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 []`);
});
});
55 changes: 55 additions & 0 deletions packages/core/src/rules/common/scalar-property-missing-example.ts
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
tatomyr marked this conversation as resolved.
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);
}
2 changes: 2 additions & 0 deletions packages/core/src/rules/oas2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {};
2 changes: 2 additions & 0 deletions packages/core/src/rules/oas3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {};
3 changes: 2 additions & 1 deletion packages/core/src/typings/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

Expand Down