Skip to content

Commit

Permalink
feat: support custom message for built-in rules (#1834)
Browse files Browse the repository at this point in the history
* feat: support custom message for built-in rules

* add docs

* fix docs

* Update docs/configuration/reference/rules.md

Co-authored-by: Heather Cloward <[email protected]>

* Update docs/configuration/reference/rules.md

Co-authored-by: Heather Cloward <[email protected]>

* fix prettier issues

---------

Co-authored-by: Heather Cloward <[email protected]>
  • Loading branch information
tatomyr and HCloward authored Jan 2, 2025
1 parent 62ae54a commit cd768f9
Show file tree
Hide file tree
Showing 13 changed files with 260 additions and 20 deletions.
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
7 changes: 7 additions & 0 deletions __tests__/lint/default-message-override/split/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

1 comment on commit cd768f9

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

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

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements 78.62% 5045/6417
🟡 Branches 67.27% 2055/3055
🟡 Functions 73.11% 832/1138
🟡 Lines 78.91% 4759/6031

Test suite run success

833 tests passing in 120 suites.

Report generated by 🧪jest coverage report action from cd768f9

Please sign in to comment.