Skip to content

Commit

Permalink
fix(rulesets): example validation for required readOnly and writeOnly…
Browse files Browse the repository at this point in the history
… properties

Required readOnly and writeOnly properties should not be considered required
for respectively request and response bodies.
  • Loading branch information
pplr committed Jan 10, 2024
1 parent 8d31c1a commit 8ddecd1
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 0 deletions.
152 changes: 152 additions & 0 deletions packages/rulesets/src/oas/__tests__/oas3-valid-media-example.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,158 @@ testRule('oas3-valid-media-example', [
errors: [],
},

{
name: 'Ignore required readOnly parameters on requests',
document: {
openapi: '3.0.0',
paths: {
'/': {
post: {
requestBody: {
content: {
'application/json': {
schema: {
required: ['ro', 'wo'],
properties: {
ro: {
type: 'string',
readOnly: true,
},
wo: {
type: 'string',
writeOnly: true,
},
other: {
type: 'string',
},
},
},
example: {
other: 'foobar',
wo: 'some',
},
},
},
},
},
},
},
components: {
requestBodies: {
foo: {
content: {
'application/json': {
schema: {
required: ['ro', 'wo', 'other'],
properties: {
ro: {
type: 'string',
readOnly: true,
},
wo: {
type: 'string',
writeOnly: true,
},
other: {
type: 'string',
},
},
},
examples: {
valid: {
summary: 'should be valid',
value: {
other: 'foo',
wo: 'some',
},
},
},
},
},
},
},
},
},
errors: [],
},

{
name: 'Ignore required writeOnly parameters on responses',
document: {
openapi: '3.0.0',
paths: {
'/': {
post: {
responses: {
'200': {
content: {
'application/json': {
schema: {
required: ['ro', 'wo'],
properties: {
ro: {
type: 'string',
readOnly: true,
},
wo: {
type: 'string',
writeOnly: true,
},
other: {
type: 'string',
},
},
},
example: {
other: 'foobar',
ro: 'some',
},
},
},
},
},
},
},
},
components: {
responses: {
foo: {
content: {
'application/json': {
schema: {
required: ['ro', 'wo', 'other'],
properties: {
ro: {
type: 'string',
readOnly: true,
},
wo: {
type: 'string',
writeOnly: true,
},
other: {
type: 'string',
},
},
},
examples: {
valid: {
summary: 'should be valid',
value: {
other: 'foo',
ro: 'some',
},
},
},
},
},
},
},
},
},
errors: [],
},

{
name: 'parameters: will fail when complex example is used',
document: {
Expand Down
35 changes: 35 additions & 0 deletions packages/rulesets/src/oas/functions/oasExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export type Options = {
type: 'media' | 'schema';
};

type HasRequiredProperties = traverse.SchemaObject & {
required?: string[];
};

function hasRequiredProperties(schema: traverse.SchemaObject): schema is HasRequiredProperties {
return schema.required === undefined || Array.isArray(schema.required);
}

type MediaValidationItem = {
field: string;
multiple: boolean;
Expand Down Expand Up @@ -146,6 +154,27 @@ function cleanSchema(schema: Record<string, unknown>): void {
}));
}

function cleanRequired(schema: Record<string, unknown>, cleanup: { readOnly: boolean; writeOnly: boolean }): void {
traverse(schema, {}, <traverse.Callback>((
fragment,
jsonPtr,
rootSchema,
parentJsonPtr,
parentKeyword,
parent,
propertyName,
) => {
if ((fragment.readOnly === true && cleanup.readOnly) || (fragment.writeOnly === true && cleanup.writeOnly)) {
if (parentKeyword == 'properties' && parent && hasRequiredProperties(parent)) {
parent.required = parent.required?.filter(p => p !== propertyName);
if (parent.required?.length === 0) {
delete parent.required;
}
}
}
}));
}

export default createRulesetFunction<Record<string, unknown>, Options>(
{
input: {
Expand Down Expand Up @@ -191,6 +220,12 @@ export default createRulesetFunction<Record<string, unknown>, Options>(
schemaOpts.schema = JSON.parse(JSON.stringify(schemaOpts.schema));
cleanSchema(schemaOpts.schema);

const cleanup = {
writeOnly: opts.type === 'media' && (context.path[1] === 'responses' || context.path[3] === 'responses'),
readOnly: opts.type === 'media' && (context.path[1] === 'requestBodies' || context.path[3] === 'requestBody'),
};
cleanRequired(schemaOpts.schema, cleanup);

for (const validationItem of validationItems) {
const result = oasSchema(validationItem.value, schemaOpts, {
...context,
Expand Down

0 comments on commit 8ddecd1

Please sign in to comment.