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
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: [],
},
]);
13 changes: 11 additions & 2 deletions tools/spectral/ipa/rulesets/IPA-104.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@
functions:
- eachResourceHasGetMethod
- getMethodReturnsSingleResource
- getMethodReturnsResponseSuffixedObject
- getResponseCodeShouldBe200OK

rules:
xgen-IPA-104-resource-has-GET:
description: 'APIs must provide a get method for resources. http://go/ipa/104'
description: 'APIs must provide a Get method for resources. http://go/ipa/104'
message: '{{error}} http://go/ipa/104'
severity: warn
given: '$.paths'
then:
field: '@key'
function: 'eachResourceHasGetMethod'
xgen-IPA-104-get-method-returns-single-resource:
description: 'The purpose of the get method is to return data from a single resource. http://go/ipa/104'
description: 'The purpose of the Get method is to return data from a single resource. http://go/ipa/104'
message: '{{error}} http://go/ipa/104'
severity: warn
given: '$.paths[*].get'
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'
11 changes: 6 additions & 5 deletions tools/spectral/ipa/rulesets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ For rule definitions, see [IPA-102.yaml](https://github.com/mongodb/openapi/blob

For rule definitions, see [IPA-104.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-104.yaml).

| Rule Name | Description | Severity |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------- | -------- |
| xgen-IPA-104-resource-has-GET | APIs must provide a get method for resources. http://go/ipa/104 | warn |
| xgen-IPA-104-get-method-returns-single-resource | The purpose of the get method is to return data from a single resource. http://go/ipa/104 | warn |
| xgen-IPA-104-get-method-response-code-is-200 | The Get method must return a 200 OK response. http://go/ipa/104 | warn |
| Rule Name | Description | Severity |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------- |
| xgen-IPA-104-resource-has-GET | APIs must provide a Get method for resources. http://go/ipa/104 | warn |
| xgen-IPA-104-get-method-returns-single-resource | The purpose of the Get method is to return data from a single resource. http://go/ipa/104 | warn |
| xgen-IPA-104-get-method-response-code-is-200 | The Get method must return a 200 OK response. http://go/ipa/104 | warn |
| xgen-IPA-104-get-method-returns-response-suffixed-object | The Get method of a resource should return a "Response" suffixed object. http://go/ipa/104 | warn |

### IPA-106

Expand Down
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 @@ -10,8 +11,9 @@ const ERROR_MESSAGE_SCHEMA_REF = 'The response body schema is defined inline and
export default (input, _, { path, documentInventory }) => {
const oas = documentInventory.unresolved;
const resourcePath = path[1];
const contentMediaType = path[path.length - 1];

if (isCustomMethodIdentifier(resourcePath)) {
if (isCustomMethodIdentifier(resourcePath) || !contentMediaType.endsWith('json')) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added check to only apply rule to json content types, since csv and gzip don't really have a schema, it's alright if they don't reference one

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.

Good call as this might cause failures for new content types. Good to have short spec for it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure! I'll add to the spec tab

return;
}

Expand All @@ -24,24 +26,13 @@ export default (input, _, { path, documentInventory }) => {

if (contentPerMediaType.schema) {
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);
}
};
Loading
Loading