Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import testRule from './__helpers__/testRule';
import { DiagnosticSeverity } from '@stoplight/types';

const componentSchemas = {
schemas: {
SchemaResponse: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technical note to add:

  • We going to cover only top response object.
    • No oneOf support
    • No inline schemas
    • No checks for children
    • No checks for allOf

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I'm good with that, can document when merged

type: 'object',
},
Schema: {
type: 'object',
},
},
};

testRule('xgen-IPA-104-get-method-returns-response-suffixed-object', [
{
name: 'valid schema names names',
document: {
paths: {
'/resource/{id}': {
get: {
responses: {
200: {
content: {
'application/vnd.atlas.2023-01-01+json': {
schema: {
$ref: '#/components/schemas/SchemaResponse',
},
},
'application/vnd.atlas.2024-01-01+json': {
schema: {
$ref: '#/components/schemas/SchemaResponse',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be our approach for inline schemas (properties defined here).
IMHO we should allow them and skip reporting success

Copy link
Collaborator Author

@lovisaberggren lovisaberggren Mar 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the rule will fail and tell the API producer to implement a schema ref, same approach as we did in #473

},
},
'application/vnd.atlas.2025-01-01+json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/SchemaResponse',
},
},
},
},
},
400: {
content: {
'application/vnd.atlas.2023-01-01+json': {
schema: {
$ref: '#/components/schemas/Schema',
},
},
},
},
},
},
},
'/resourceTwo/{id}': {
get: {
responses: {
200: {
content: {
'application/vnd.atlas.2024-08-05+json': {
type: 'string',
},
},
},
},
},
},
'/resource/{id}/singleton': {
get: {
responses: {
200: {
content: {
'application/vnd.atlas.2024-08-05+json': {
schema: {
$ref: '#/components/schemas/SchemaResponse',
},
},
},
},
},
},
},
},
components: componentSchemas,
},
errors: [],
},
{
name: 'invalid resources',
document: {
paths: {
'/resource/{id}': {
get: {
responses: {
200: {
content: {
'application/vnd.atlas.2023-01-01+json': {
schema: {
$ref: '#/components/schemas/Schema',
},
},
'application/vnd.atlas.2024-01-01+json': {
schema: {
$ref: '#/components/schemas/Schema',
},
},
'application/vnd.atlas.2025-01-01+json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/Schema',
Copy link
Member

@wtrocki wtrocki Mar 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

example to ignore

Suggested change
$ref: '#/components/schemas/Schema',
oneOf: [ {$ref: '#/components/schemas/Schema'}, {$ref: '#/components/schemas/Schema'}}

},
},
},
},
},
},
},
},
'/resource/{id}/singleton': {
get: {
responses: {
200: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Test for two 2xx response codes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be an invalid json since we cannot have two keys of the same value

content: {
'application/vnd.atlas.2024-08-05+json': {
schema: {
$ref: '#/components/schemas/Schema',
},
},
},
},
},
},
},
},
components: componentSchemas,
},
errors: [
{
code: 'xgen-IPA-104-get-method-returns-response-suffixed-object',
message: 'The request schema must reference a schema with a Response suffix. http://go/ipa/104',
path: [
'paths',
'/resource/{id}',
'get',
'responses',
'200',
'content',
'application/vnd.atlas.2023-01-01+json',
],
severity: DiagnosticSeverity.Warning,
},
{
code: 'xgen-IPA-104-get-method-returns-response-suffixed-object',
message: 'The request schema must reference a schema with a Response suffix. http://go/ipa/104',
path: [
'paths',
'/resource/{id}',
'get',
'responses',
'200',
'content',
'application/vnd.atlas.2024-01-01+json',
],
severity: DiagnosticSeverity.Warning,
},
{
code: 'xgen-IPA-104-get-method-returns-response-suffixed-object',
message: 'The request schema must reference a schema with a Response suffix. http://go/ipa/104',
path: [
'paths',
'/resource/{id}',
'get',
'responses',
'200',
'content',
'application/vnd.atlas.2025-01-01+json',
],
severity: DiagnosticSeverity.Warning,
},
{
code: 'xgen-IPA-104-get-method-returns-response-suffixed-object',
message: 'The request schema must reference a schema with a Response suffix. http://go/ipa/104',
path: [
'paths',
'/resource/{id}/singleton',
'get',
'responses',
'200',
'content',
'application/vnd.atlas.2024-08-05+json',
],
severity: DiagnosticSeverity.Warning,
},
],
},
{
name: 'invalid resources with exceptions',
document: {
paths: {
'/resource/{id}': {
get: {
responses: {
200: {
content: {
'application/vnd.atlas.2023-01-01+json': {
'x-xgen-IPA-exception': {
'xgen-IPA-104-get-method-returns-response-suffixed-object': 'reason',
},
schema: {
$ref: '#/components/schemas/Schema',
},
},
'application/vnd.atlas.2024-01-01+json': {
'x-xgen-IPA-exception': {
'xgen-IPA-104-get-method-returns-response-suffixed-object': 'reason',
},
schema: {
$ref: '#/components/schemas/Schema',
},
},
'application/vnd.atlas.2025-01-01+json': {
'x-xgen-IPA-exception': {
'xgen-IPA-104-get-method-returns-response-suffixed-object': 'reason',
},
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/Schema',
},
},
},
},
},
},
},
},
'/resource/{id}/singleton': {
get: {
responses: {
200: {
content: {
'application/vnd.atlas.2024-08-05+json': {
'x-xgen-IPA-exception': {
'xgen-IPA-104-get-method-returns-response-suffixed-object': 'reason',
},
schema: {
$ref: '#/components/schemas/Schema',
},
},
},
},
},
},
},
},
components: componentSchemas,
},
errors: [],
},
]);
9 changes: 9 additions & 0 deletions tools/spectral/ipa/rulesets/IPA-104.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
functions:
- eachResourceHasGetMethod
- getMethodReturnsSingleResource
- getMethodReturnsResponseSuffixedObject
- getResponseCodeShouldBe200OK

rules:
Expand All @@ -29,3 +30,11 @@ rules:
given: '$.paths[*].get'
then:
function: 'getResponseCodeShouldBe200OK'
xgen-IPA-104-get-method-returns-response-suffixed-object:
description: 'The get method of a resource should return a "Response" suffixed object. http://go/ipa/104'
message: '{{error}} http://go/ipa/104'
severity: warn
given: '$.paths[*].get.responses[*].content'
then:
field: '@key'
function: 'getMethodReturnsResponseSuffixedObject'
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { hasException } from './utils/exceptions.js';
import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js';
import { isCustomMethodIdentifier } from './utils/resourceEvaluation.js';
import { resolveObject } from './utils/componentUtils.js';
import { getSchemaRef } from './utils/methodUtils.js';

const RULE_NAME = 'xgen-IPA-106-create-method-request-body-is-request-suffixed-object';
const ERROR_MESSAGE_SCHEMA_NAME = 'The response body schema must reference a schema with a Request suffix.';
Expand All @@ -23,25 +24,15 @@ export default (input, _, { path, documentInventory }) => {
}

if (contentPerMediaType.schema) {
console.log(contentPerMediaType);
const schema = contentPerMediaType.schema;
if (schema.type === 'array' && schema.items) {
let schemaItems = schema.items;
if (!schemaItems.$ref) {
return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE_SCHEMA_REF);
}
if (!schemaItems.$ref.endsWith('Request')) {
return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE_SCHEMA_NAME);
}
} else {
if (!schema.$ref) {
return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE_SCHEMA_REF);
}

if (!schema.$ref.endsWith('Request')) {
return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE_SCHEMA_NAME);
}
const schemaRef = getSchemaRef(schema);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a shared function since this logic is pretty much the same for the new rule as well

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking for that function as well :)
Nice!

if (!schemaRef) {
return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE_SCHEMA_REF);
}
if (!schemaRef.endsWith('Request')) {
return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE_SCHEMA_NAME);
}

collectAdoption(path, RULE_NAME);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
isSingleResourceIdentifier,
isSingletonResource,
getResourcePathItems,
isResourceCollectionIdentifier,
} from './utils/resourceEvaluation.js';
import { resolveObject } from './utils/componentUtils.js';
import { hasException } from './utils/exceptions.js';
import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js';
import { getSchemaRef } from './utils/methodUtils.js';

const RULE_NAME = 'xgen-IPA-104-get-method-returns-response-suffixed-object';
const ERROR_MESSAGE_SCHEMA_NAME = 'The request schema must reference a schema with a Response suffix.';
const ERROR_MESSAGE_SCHEMA_REF = 'The response body schema is defined inline and must reference a predefined schema.';

export default (input, _, { path, documentInventory }) => {
const resourcePath = path[1];
const responseCode = path[4];
const oas = documentInventory.unresolved;
const resourcePaths = getResourcePathItems(resourcePath, oas.paths);

if (
responseCode.startsWith('2') ||
isResourceCollectionIdentifier(resourcePath) ||
(isSingleResourceIdentifier(resourcePath) && isSingletonResource(resourcePaths))
) {
const contentPerMediaType = resolveObject(oas, path);

if (hasException(contentPerMediaType, RULE_NAME)) {
collectException(contentPerMediaType, RULE_NAME, path);
return;
}

if (contentPerMediaType.schema) {
const schema = contentPerMediaType.schema;
const schemaRef = getSchemaRef(schema);
if (!schemaRef) {
return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE_SCHEMA_REF);
}
if (!schemaRef.endsWith('Response')) {
return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE_SCHEMA_NAME);
}
collectAdoption(path, RULE_NAME);
}
}
};
13 changes: 13 additions & 0 deletions tools/spectral/ipa/rulesets/functions/utils/methodUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,16 @@ export function getAllSuccessfulResponseSchemas(operationObject) {
});
return result;
}

/**
* Gets the schema reference for a schema object. If the schema does not have a reference, undefined is returned.
*
* @param {object} schema the schema object
* @returns {string} the schema ref
*/
export function getSchemaRef(schema) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

if (schema.type === 'array' && schema.items) {
return schema.items.$ref;
}
return schema.$ref;
}
Loading