Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support custom message for built-in rules #1834

Merged
merged 6 commits into from
Jan 2, 2025
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
6 changes: 6 additions & 0 deletions .changeset/tough-clouds-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@redocly/openapi-core": minor
"@redocly/cli": minor
---

Added the ability to override default problem messages for built-in rules.
13 changes: 13 additions & 0 deletions __tests__/lint/default-message-override/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
openapi: 3.1.0
info:
version: 1.0.0
title: Custom messages test
paths:
/test:
get:
responses:
200:
content:
application/json:
schema:
type: object
28 changes: 28 additions & 0 deletions __tests__/lint/default-message-override/redocly.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
apis:
built-in-rule-message-override:
root: ./openapi.yaml
rules:
info-contact:
message: 'API LEVEL MESSAGE' # should override teh root-level message
severity: warn
operation-operationId:
severity: warn
message: 'API LEVEL WITH ORIGINAL MSG: {{message}}' # should enhance the original message
split-documentation:
root: split/openapi.yaml

rules:
info-contact:
message: ROOT LEVEL MESSAGE # should be replaced with api-level message
severity: error
struct:
message: 'ROOT LEVEL WITH ORIGINAL MSG: {{message}}' # should enhance the original message
severity: error
rule/operationId:
subject:
type: Operation
message: 'Original problem: {{problems}}' # should not interfere with assertion messages
severity: error
assertions:
required:
- operationId
109 changes: 109 additions & 0 deletions __tests__/lint/default-message-override/snapshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`E2E lint default-message-override 1`] = `

validating openapi.yaml...
[1] openapi.yaml:7:5 at #/paths/~1test/get

Original problem: operationId is required

5 | paths:
6 | /test:
7 | get:
| ^^^
8 | responses:
9 | 200:

Error was generated by the rule/operationId rule.


[2] openapi.yaml:9:9 at #/paths/~1test/get/responses/200

ROOT LEVEL WITH ORIGINAL MSG: The field \`description\` must be present on this level.

7 | get:
8 | responses:
9 | 200:
| ^^^
10 | content:
11 | application/json:

Error was generated by the struct rule.


[3] openapi.yaml:2:1 at #/info/contact

API LEVEL MESSAGE

1 | openapi: 3.1.0
2 | info:
| ^^^^
3 | version: 1.0.0
4 | title: Custom messages test

Warning was generated by the info-contact rule.


[4] openapi.yaml:7:5 at #/paths/~1test/get/operationId

API LEVEL WITH ORIGINAL MSG: Operation object should contain \`operationId\` field.

5 | paths:
6 | /test:
7 | get:
| ^^^
8 | responses:
9 | 200:

Warning was generated by the operation-operationId rule.


openapi.yaml: validated in <test>ms

validating split/openapi.yaml...
[1] split/info.yaml:1:1 at #/contact

ROOT LEVEL MESSAGE

1 | version: 1.0.0
| ^^^^^^^^^^^^^^
2 | title: Custom messages test
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 |

Error was generated by the info-contact rule.


[2] split/paths/test.yaml:1:1 at #/get

Original problem: operationId is required

1 | get:
| ^^^
2 | responses:
3 | '200':

Error was generated by the rule/operationId rule.


[3] split/paths/test.yaml:3:5 at #/get/responses/200

ROOT LEVEL WITH ORIGINAL MSG: The field \`description\` must be present on this level.

1 | get:
2 | responses:
3 | '200':
| ^^^^^
4 | content:
5 | application/json:

Error was generated by the struct rule.


split/openapi.yaml: validated in <test>ms

❌ Validation failed with 5 errors and 2 warnings.
run \`redocly lint --generate-ignore-file\` to add all problems to the ignore file.


`;
2 changes: 2 additions & 0 deletions __tests__/lint/default-message-override/split/info.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
version: 1.0.0
title: Custom messages test
6 changes: 6 additions & 0 deletions __tests__/lint/default-message-override/split/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
openapi: 3.1.0
info:
$ref: ./info.yaml
paths:
/test:
$ref: paths/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
get:
responses:
'200':
content:
application/json:
schema:
type: object
8 changes: 8 additions & 0 deletions docs/configuration/reference/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ The `rules` block can be used at the root of a configuration file, or inside an

---

- message
- string
- Optional custom message for this rule.
Example: `My Error Description. {{message}}`.
The {{message}} placeholder renders with the default error message for the rule. Include the {{message}} placeholder if you want to provide the user with your custom message as well as the default error message for the rule.

---

- {additional properties}
- any
- Some rules allow additional configuration, check the details of each rule to find out the values that can be supplied here. For example the [`boolean-parameter-prefixes` rule](../../rules/oas/boolean-parameter-prefixes.md) supports an additional option of `prefixes` that accepts an array of strings.
Expand Down
51 changes: 51 additions & 0 deletions packages/core/src/__tests__/walk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { BaseResolver, Document } from '../resolve';
import { listOf } from '../types';
import { Oas3RuleSet } from '../oas-types';
import { createConfig } from '../config';

describe('walk order', () => {
it('should run visitors', async () => {
Expand Down Expand Up @@ -1338,6 +1339,56 @@ describe('context.report', () => {
]
`);
});

it('should report errors with custom messages', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.0.0
info:
license: {}
paths: {}
`,
'foobar.yaml'
);

const config = await createConfig(`
rules:
info-contact:
message: "MY ERR DESCRIPTION: {{message}}"
severity: "error"
`);

const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: config.styleguide,
});

expect(results).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/info/contact",
"reportOnKey": true,
"source": Source {
"absoluteRef": "foobar.yaml",
"body": "openapi: 3.0.0
info:
license: {}
paths: {}",
"mimeType": undefined,
},
},
],
"message": "MY ERR DESCRIPTION: Info object should contain \`contact\` field.",
"ruleId": "info-contact",
"severity": "error",
"suggest": [],
},
]
`);
});
});

describe('context.resolve', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/config/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,21 @@ export function initRules(
return undefined;
}
const severity: ProblemSeverity = ruleSettings.severity;

const message = ruleSettings.message;
const visitors = rule(ruleSettings);

if (Array.isArray(visitors)) {
return visitors.map((visitor: any) => ({
severity,
ruleId,
message,
visitor: visitor,
}));
}

return {
severity,
message,
ruleId,
visitor: visitors, // note: actually it is only one visitor object
};
Expand Down
8 changes: 2 additions & 6 deletions packages/core/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,11 @@ import type { JSONSchema } from 'json-schema-to-ts';

export type RuleSeverity = ProblemSeverity | 'off';

export type RuleSettings = { severity: RuleSeverity };
export type RuleSettings = { severity: RuleSeverity; message?: string };

export type PreprocessorSeverity = RuleSeverity | 'on';

export type RuleConfig =
| RuleSeverity
| ({
severity?: ProblemSeverity;
} & Record<string, any>);
export type RuleConfig = RuleSeverity | (Partial<RuleSettings> & Record<string, any>);

export type PreprocessorConfig =
| PreprocessorSeverity
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/visitors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ type VisitFunctionOrObject<T> = VisitFunction<T> | VisitObject<T>;
export type VisitorNode<T> = {
ruleId: string;
severity: ProblemSeverity;
message?: string;
context: VisitorLevelContext | VisitorSkippedLevelContext;
depth: number;
visit: VisitFunction<T>;
Expand All @@ -106,6 +107,7 @@ export type VisitorNode<T> = {
type VisitorRefNode = {
ruleId: string;
severity: ProblemSeverity;
message?: string;
context: VisitorLevelContext;
depth: number;
visit: VisitRefFunction;
Expand Down Expand Up @@ -365,6 +367,7 @@ export type OasDecorator = Oas3Decorator;
export type RuleInstanceConfig = {
ruleId: string;
severity: ProblemSeverity;
message?: string;
};

export function normalizeVisitors<T extends BaseVisitor>(
Expand All @@ -390,8 +393,8 @@ export function normalizeVisitors<T extends BaseVisitor>(
leave: [],
};

for (const { ruleId, severity, visitor } of visitorsConfig) {
normalizeVisitorLevel({ ruleId, severity }, visitor, null);
for (const { ruleId, severity, message, visitor } of visitorsConfig) {
normalizeVisitorLevel({ ruleId, severity, message }, visitor, null);
}

for (const v of Object.keys(normalizedVisitors)) {
Expand Down
Loading
Loading