Skip to content
Draft
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
6 changes: 3 additions & 3 deletions packages/core/src/rules/ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
ajvInstance = null;
}

function getAjv(resolve: ResolveFn, allowAdditionalProperties: boolean) {
function getAjv(resolve: ResolveFn) {
if (!ajvInstance) {
ajvInstance = new Ajv({
schemaId: '$id',
Expand All @@ -24,7 +24,6 @@
discriminator: true,
allowUnionTypes: true,
validateFormats: true,
defaultUnevaluatedProperties: allowAdditionalProperties,
loadSchemaSync(base: string, $ref: string, $id: string) {
const resolvedRef = resolve({ $ref }, base.split('#')[0]);
if (!resolvedRef || !resolvedRef.location) return false;
Expand All @@ -43,9 +42,10 @@
resolve: ResolveFn,
allowAdditionalProperties: boolean
): ValidateFunction | undefined {
const ajv = getAjv(resolve, allowAdditionalProperties);
const ajv = getAjv(resolve);

if (!ajv.getSchema(loc.absolutePointer)) {
ajv.setDefaultUnevaluatedProperties(allowAdditionalProperties);

Check failure on line 48 in packages/core/src/rules/ajv.ts

View workflow job for this annotation

GitHub Actions / run-smoke-rebilly

Property 'setDefaultUnevaluatedProperties' does not exist on type 'Ajv2020'.

Check failure on line 48 in packages/core/src/rules/ajv.ts

View workflow job for this annotation

GitHub Actions / require-changeset-or-label

Property 'setDefaultUnevaluatedProperties' does not exist on type 'Ajv2020'.

Check failure on line 48 in packages/core/src/rules/ajv.ts

View workflow job for this annotation

GitHub Actions / prepare-smoke

Property 'setDefaultUnevaluatedProperties' does not exist on type 'Ajv2020'.

Check failure on line 48 in packages/core/src/rules/ajv.ts

View workflow job for this annotation

GitHub Actions / prepare-smoke-plugins

Property 'setDefaultUnevaluatedProperties' does not exist on type 'Ajv2020'.

Check failure on line 48 in packages/core/src/rules/ajv.ts

View workflow job for this annotation

GitHub Actions / code-style-check

Property 'setDefaultUnevaluatedProperties' does not exist on type 'Ajv2020'.

Check failure on line 48 in packages/core/src/rules/ajv.ts

View workflow job for this annotation

GitHub Actions / cli-package-test

Property 'setDefaultUnevaluatedProperties' does not exist on type 'Ajv2020'.

Check failure on line 48 in packages/core/src/rules/ajv.ts

View workflow job for this annotation

GitHub Actions / latest-vs-next

Property 'setDefaultUnevaluatedProperties' does not exist on type 'Ajv2020'.

Check failure on line 48 in packages/core/src/rules/ajv.ts

View workflow job for this annotation

GitHub Actions / build-and-unit

Property 'setDefaultUnevaluatedProperties' does not exist on type 'Ajv2020'.
ajv.addSchema({ $id: loc.absolutePointer, ...schema }, loc.absolutePointer);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,125 @@ describe('no-invalid-parameter-examples', () => {
]
`);
});

it('should report on invalid example with additional properties when allowAdditionalProperties is false', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.0.0
paths:
/users:
get:
parameters:
- name: filter
in: query
schema:
type: object
properties:
name:
type: string
age:
type: number
example:
name: "John"
age: 30
extraProperty: "not allowed"
`,
'foobar.yaml'
);

const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await createConfig({
rules: {
'no-invalid-parameter-examples': {
severity: 'error',
allowAdditionalProperties: false,
},
},
}),
});

expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"from": {
"pointer": "#/paths/~1users/get/parameters/0",
"source": "foobar.yaml",
},
"location": [
{
"pointer": "#/paths/~1users/get/parameters/0/example/extraProperty",
"reportOnKey": true,
"source": "foobar.yaml",
},
],
"message": "Example value must conform to the schema: must NOT have unevaluated properties \`extraProperty\`.",
"ruleId": "no-invalid-parameter-examples",
"severity": "error",
"suggest": [],
},
]
`);
});

it('should report on invalid example in examples object when allowAdditionalProperties is false', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.0.0
paths:
/users:
get:
parameters:
- name: filter
in: query
schema:
type: object
properties:
name:
type: string
examples:
invalid:
value:
name: "Jane"
extraProperty: "not allowed"
`,
'foobar.yaml'
);

const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await createConfig({
rules: {
'no-invalid-parameter-examples': {
severity: 'error',
allowAdditionalProperties: false,
},
},
}),
});

expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"from": {
"pointer": "#/paths/~1users/get/parameters/0",
"source": "foobar.yaml",
},
"location": [
{
"pointer": "#/paths/~1users/get/parameters/0/examples/invalid/extraProperty",
"reportOnKey": true,
"source": "foobar.yaml",
},
],
"message": "Example value must conform to the schema: must NOT have unevaluated properties \`extraProperty\`.",
"ruleId": "no-invalid-parameter-examples",
"severity": "error",
"suggest": [],
},
]
`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,115 @@ describe('no-invalid-schema-examples', () => {

expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});

it('should report on invalid example with additional properties', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.0.0
components:
schemas:
Car:
type: object
properties:
color:
type: string
model:
type: string
example:
color: "red"
model: "sedan"
extraProperty: "not allowed"
`,
'foobar.yaml'
);

const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await createConfig({
rules: {
'no-invalid-schema-examples': {
severity: 'error',
allowAdditionalProperties: false,
},
},
}),
});

expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"from": {
"pointer": "#/components/schemas/Car",
"source": "foobar.yaml",
},
"location": [
{
"pointer": "#/components/schemas/Car/example/extraProperty",
"reportOnKey": true,
"source": "foobar.yaml",
},
],
"message": "Example value must conform to the schema: must NOT have unevaluated properties \`extraProperty\`.",
"ruleId": "no-invalid-schema-examples",
"severity": "error",
"suggest": [],
},
]
`);
});

it('should report on invalid examples with additional properties', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.0.0
components:
schemas:
Car:
type: object
properties:
color:
type: string
examples:
- color: "blue"
extraProperty: "not allowed"
`,
'foobar.yaml'
);

const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await createConfig({
rules: {
'no-invalid-schema-examples': {
severity: 'error',
allowAdditionalProperties: false,
},
},
}),
});

expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"from": {
"pointer": "#/components/schemas/Car",
"source": "foobar.yaml",
},
"location": [
{
"pointer": "#/components/schemas/Car/examples/0/extraProperty",
"reportOnKey": true,
"source": "foobar.yaml",
},
],
"message": "Example value must conform to the schema: must NOT have unevaluated properties \`extraProperty\`.",
"ruleId": "no-invalid-schema-examples",
"severity": "error",
"suggest": [],
},
]
`);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { validateExample } from '../utils.js';
import { isDefined } from '../../utils/is-defined.js';

import type { UserContext } from '../../walk.js';
import type { Oas3Parameter } from '../../typings/openapi.js';
Expand All @@ -7,13 +8,16 @@ export const NoInvalidParameterExamples: any = (opts: any) => {
return {
Parameter: {
leave(parameter: Oas3Parameter, ctx: UserContext) {
if (parameter.example !== undefined) {
const allowAdditionalProperties = isDefined(opts.allowAdditionalProperties)
? opts.allowAdditionalProperties
: true;
if (isDefined(parameter.example)) {
validateExample(
parameter.example,
parameter.schema!,
ctx.location.child('example'),
ctx,
!!opts.allowAdditionalProperties
allowAdditionalProperties
);
}

Expand All @@ -25,7 +29,7 @@ export const NoInvalidParameterExamples: any = (opts: any) => {
parameter.schema!,
ctx.location.child(['examples', key]),
ctx,
true
allowAdditionalProperties
);
}
}
Expand Down
17 changes: 14 additions & 3 deletions packages/core/src/rules/common/no-invalid-schema-examples.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { validateExample } from '../utils.js';
import { isDefined } from '../../utils/is-defined.js';

import type { UserContext } from '../../walk.js';
import type { Oas3_1Schema, Oas3Schema } from '../../typings/openapi.js';
Expand All @@ -9,19 +10,23 @@ export const NoInvalidSchemaExamples: Oas3Rule | Oas2Rule = (opts: any) => {
Schema: {
leave(schema: Oas3_1Schema | Oas3Schema, ctx: UserContext) {
const examples = (schema as Oas3_1Schema).examples;
const allowAdditionalProperties = isDefined(opts.allowAdditionalProperties)
? opts.allowAdditionalProperties
: true;

if (examples) {
for (const example of examples) {
validateExample(
example,
schema,
ctx.location.child(['examples', examples.indexOf(example)]),
ctx,
!!opts.allowAdditionalProperties
allowAdditionalProperties
);
}
}

if (schema.example !== undefined) {
if (isDefined(schema.example)) {
// Handle nullable example for OAS3
if (
(schema as Oas3Schema).nullable === true &&
Expand All @@ -31,7 +36,13 @@ export const NoInvalidSchemaExamples: Oas3Rule | Oas2Rule = (opts: any) => {
return;
}

validateExample(schema.example, schema, ctx.location.child('example'), ctx, true);
validateExample(
schema.example,
schema,
ctx.location.child('example'),
ctx,
allowAdditionalProperties
);
}
},
},
Expand Down
Loading
Loading