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
7 changes: 7 additions & 0 deletions .changeset/olive-pugs-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@redocly/openapi-core': minor
'@redocly/cli': minor
---

Added a `disallowDefault` option to the `operation-2xx-response` rule, which requires an explicit 2xx response when enabled.
There is no change in current behavior.
31 changes: 27 additions & 4 deletions docs/@v2/rules/oas/operation-2xx-response.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ You can greatly improve the developer and user experience of your APIs by making

## Configuration

| Option | Type | Description |
| ---------------- | ------- | ----------------------------------------------------------------------------------------- |
| severity | string | Possible values: `off`, `warn`, `error`. Default `warn` (in `recommended` configuration). |
| validateWebhooks | boolean | Determines if responses inside webhooks are validated. Default `false`. |
| Option | Type | Description |
| ---------------- | ------- | -------------------------------------------------------------------------------------------- |
| severity | string | Possible values: `off`, `warn`, `error`. Default: `warn` (in `recommended` configuration). |
| validateWebhooks | boolean | Determines if responses inside webhooks are validated. Default: `false`. |
| disallowDefault | boolean | Determines if a `default` response is disallowed from satisfying the rule. Default: `false`. |

An example configuration:

Expand All @@ -45,6 +46,28 @@ rules:
validateWebhooks: true
```

By default, a `default` response counts as a successful response.
Set `disallowDefault: true` to require an explicit 2xx status code:

```yaml
rules:
operation-2xx-response:
severity: error
disallowDefault: true
```

With `disallowDefault: true`, the following operation is reported, because `default` describes the responses the operation does not list rather than what a successful call returns:

```yaml
post:
responses:
default:
$ref: ../components/responses/Problem.yaml
```

This matters for code generation: a generator reads the 2xx response to produce the return type of the operation.
A `default`-only operation gives it no success shape to model, so the generated client falls back to an untyped or empty result.

## Examples

Given this configuration:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,50 @@ describe('Oas3 operation-2xx-response', () => {
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});

it('should report for present default when disallowDefault is true', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.0.0
paths:
'/test/':
put:
responses:
default:
description: ok
`,
'foobar.yaml'
);

const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await createConfig({
rules: {
'operation-2xx-response': { severity: 'error', disallowDefault: true },
},
}),
});

expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/paths/~1test~1/put/responses",
"reportOnKey": true,
"source": "foobar.yaml",
},
],
"message": "Operation must have at least one \`2XX\` response.",
"reference": "https://redocly.com/docs/cli/rules/oas/operation-2xx-response",
"ruleId": "operation-2xx-response",
"severity": "error",
"suggest": [],
},
]
`);
});

it('should report even if the responses are null', async () => {
const document = parseYamlToDocument(
outdent`
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/rules/common/operation-2xx-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import type { Oas3Rule, Oas2Rule } from '../../visitors.js';
import type { UserContext } from '../../walk.js';
import { validateResponseCodes } from '../utils.js';

export const Operation2xxResponse: Oas3Rule | Oas2Rule = ({ validateWebhooks }) => {
export const Operation2xxResponse: Oas3Rule | Oas2Rule = ({
validateWebhooks,
disallowDefault = false,
}) => {
return {
Paths: {
Responses(responses: Record<string, object>, { report }: UserContext) {
Expand All @@ -13,6 +16,7 @@ export const Operation2xxResponse: Oas3Rule | Oas2Rule = ({ validateWebhooks })
codeRange: '2XX',
report: report as UserContext['report'],
reference: 'https://redocly.com/docs/cli/rules/oas/operation-2xx-response',
disallowDefault,
});
},
},
Expand All @@ -27,6 +31,7 @@ export const Operation2xxResponse: Oas3Rule | Oas2Rule = ({ validateWebhooks })
codeRange: '2XX',
report: report as UserContext['report'],
reference: 'https://redocly.com/docs/cli/rules/oas/operation-2xx-response',
disallowDefault,
});
},
},
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/rules/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,17 +390,19 @@ export function validateResponseCodes({
codeRange,
report,
reference,
disallowDefault = false,
}: {
responseCodes: string[];
codeRange: string;
report: UserContext['report'];
reference?: string;
disallowDefault?: boolean;
}) {
const responseCodeRegexp = new RegExp(`^${codeRange[0]}[0-9Xx]{2}$`);

const containsNeededCode = responseCodes.some(
(code) =>
(codeRange === '2XX' && code === 'default') || // It's OK to replace 2xx codes with the default
(!disallowDefault && codeRange === '2XX' && code === 'default') || // It's OK to replace 2xx codes with the default
responseCodeRegexp.test(code)
);

Expand Down
Loading