Repository: Redocly/redocly-cli
Pull request #942 (2022-11-17 22:09:14)

feat: new assert `notPattern`
## What/Why/How?

Negative regular expressions are really hard to author and maintain.
A negative regexp matcher simplifies a lot of real-life rules.

## Reference

## Testing

Covered with tests.

## Screenshots (optional)

## Check yourself

- [x] Code is linted
- [ ] Tested with redoc/reference-docs/workflows
- [x] All new/updated code is covered with tests

## Security

- [x] Security impact of change has been considered
- [x] Code follows company security practices and guidelines


github-actions[bot]: <!-- jest coverage report action for options with hash 3339ae53a38c44382c0fb4491bdb282d -->

## Coverage report 



| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category   | Percentage | Covered / Total |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------- | :--------- | :-------------: |
|                                                                           🟡                                                                           | Statements | 73.56%     |    3330/4527    |
|                                                                           🟡                                                                           | Branches   | 64.85%     |    1858/2865    |
|                                                                           🟡                                                                           | Functions  | 66.16%     |     565/854     |
|                                                                           🟡                                                                           | Lines      | 73.59%     |    3095/4206    |


## Test suite run success
514 tests passing in 84 suites.

<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from fc216df2a2c3de8aaee38e0b3fac0d52982654a5</p>


Patch:
diff --git a/docs/rules/custom-rules.md b/docs/rules/custom-rules.md
index ee98d6af..2ab7ff2c 100644
--- a/docs/rules/custom-rules.md
+++ b/docs/rules/custom-rules.md
@@ -65,6 +65,7 @@ maxLength | integer | Asserts a maximum length (exclusive) of a string or list (
 minLength | integer | Asserts a minimum length (inclusive) of a string or list (array). See [minLength example](#minlength-example).
 nonEmpty | boolean | Asserts a property is not empty. See [nonEmpty example](#nonempty-example).
 pattern | string | Asserts a value matches a regex pattern. See [regex pattern example](#pattern-example).
+notPattern | string | Asserts a value doesn't match a regex pattern. See [regex notPattern example](#notpattern-example).
 mutuallyExclusive | [string] | Asserts that listed properties (key names only) are mutually exclusive. See [mutuallyExclusive example](#mutuallyexclusive-example).
 mutuallyRequired | [string] | Asserts that listed properties (key names only) are mutually required. See [mutuallyRequired example](#mutuallyrequired-example).
 ref | boolean \| string | Asserts a reference object presence in object's property. A boolean value of `true` means the property has a `$ref` defined. A boolean value of `false` means the property has not defined a `$ref` (it has an in-place value). A string value means that the `$ref` is defined and the unresolved value must match the pattern (for example, `'/paths\/. *\.yaml$/'`). See [ref example](#ref-example).|
@@ -381,6 +382,20 @@ rules:
       pattern: /test/
 ```
 
+### `notPattern` example
+
+The following example asserts that the operation summary doesn't start with "The".
+
+```yaml
+rules:
+  assert/operation-summary-contains-test:
+    subject:
+      type: Operation
+      property: The summary
+    assertions:
+      notPattern: /^The/
+```
+
 ### `casing` example
 
 The following example asserts the casing style is `PascalCase` for `NamedExamples` map keys.
diff --git a/packages/core/src/rules/common/assertions/asserts.ts b/packages/core/src/rules/common/assertions/asserts.ts
index 58a9b5e7..e0d80105 100644
--- a/packages/core/src/rules/common/assertions/asserts.ts
+++ b/packages/core/src/rules/common/assertions/asserts.ts
@@ -18,6 +18,7 @@ export type AssertionFn = (
 
 export type Asserts = {
   pattern: AssertionFn;
+  notPattern: AssertionFn;
   enum: AssertionFn;
   defined: AssertionFn;
   required: AssertionFn;
@@ -40,6 +41,7 @@ export const runOnKeysSet = new Set<keyof Asserts>([
   'mutuallyRequired',
   'enum',
   'pattern',
+  'notPattern',
   'minLength',
   'maxLength',
   'casing',
@@ -53,6 +55,7 @@ export const runOnKeysSet = new Set<keyof Asserts>([
 ]);
 export const runOnValuesSet = new Set<keyof Asserts>([
   'pattern',
+  'notPattern',
   'enum',
   'defined',
   'undefined',
@@ -69,18 +72,33 @@ export const asserts: Asserts = {
   pattern: (value: string | string[], condition: string, baseLocation: Location) => {
     if (typeof value === 'undefined') return []; // property doesn't exist, no need to lint it with this assert
     const values = runOnValue(value) ? [value] : value;
-    const regx = regexFromString(condition);
+    const regex = regexFromString(condition);
 
     return values
       .map(
         (_val) =>
-          !regx?.test(_val) && {
+          !regex?.test(_val) && {
             message: `"${_val}" should match a regex ${condition}`,
             location: runOnValue(value) ? baseLocation : baseLocation.key(),
           }
       )
       .filter(isTruthy);
   },
+  notPattern: (value: string | string[], condition: string, baseLocation: Location) => {
+    if (typeof value === 'undefined') return []; // property doesn't exist, no need to lint it with this assert
+    const values = runOnValue(value) ? [value] : value;
+    const regex = regexFromString(condition);
+
+    return values
+      .map(
+        (_val) =>
+          regex?.test(_val) && {
+            message: `"${_val}" should not match a regex ${condition}`,
+            location: runOnValue(value) ? baseLocation : baseLocation.key(),
+          }
+      )
+      .filter(isTruthy);
+  },
   enum: (value: string | string[], condition: string[], baseLocation: Location) => {
     if (typeof value === 'undefined') return []; // property doesn't exist, no need to lint it with this assert
     const values = runOnValue(value) ? [value] : value;
diff --git a/packages/core/src/types/redocly-yaml.ts b/packages/core/src/types/redocly-yaml.ts
index 9ce67ac5..a50ce4ad 100644
--- a/packages/core/src/types/redocly-yaml.ts
+++ b/packages/core/src/types/redocly-yaml.ts
@@ -258,6 +258,7 @@ const AssertionDefinitionAssertions: NodeType = {
   properties: {
     enum: { type: 'array', items: { type: 'string' } },
     pattern: { type: 'string' },
+    notPattern: { type: 'string' },
     casing: {
       enum: [
         'camelCase',


Test patch:
diff --git a/__tests__/lint/assertions-pattern-error/.redocly.yaml b/__tests__/lint/assertions-pattern-error/.redocly.yaml
index 4bfa24bb..6d7c790d 100644
--- a/__tests__/lint/assertions-pattern-error/.redocly.yaml
+++ b/__tests__/lint/assertions-pattern-error/.redocly.yaml
@@ -4,9 +4,9 @@ apis:
 
 rules:
   assert/summary-pattern:
-    subject: 
+    subject:
       type: Operation
       property: summary
-    message: Operation summary should have 'test' word
-    assertions: 
+    assertions:
       pattern: '/test/'
+      notPattern: /summary/
diff --git a/__tests__/lint/assertions-pattern-error/snapshot.js b/__tests__/lint/assertions-pattern-error/snapshot.js
index 515db645..d4b5c1a0 100644
--- a/__tests__/lint/assertions-pattern-error/snapshot.js
+++ b/__tests__/lint/assertions-pattern-error/snapshot.js
@@ -5,7 +5,21 @@ exports[`E2E lint assertions-pattern-error 1`] = `
 validating /openapi.yaml...
 [1] openapi.yaml:20:16 at #/paths/~1pet~1findByStatus/get/summary
 
-Operation summary should have 'test' word
+summary-pattern assertion failed because the Operation summary didn't meet the assertions: "summary example" should match a regex /test/
+
+18 | get:
+19 |   operationId: example
+20 |   summary: summary example
+   |            ^^^^^^^^^^^^^^^
+21 |   tags:
+22 |     - foo
+
+Error was generated by the summary-pattern assertion rule.
+
+
+[2] openapi.yaml:20:16 at #/paths/~1pet~1findByStatus/get/summary
+
+summary-pattern assertion failed because the Operation summary didn't meet the assertions: "summary example" should not match a regex /summary/
 
 18 | get:
 19 |   operationId: example
@@ -19,7 +33,7 @@ Error was generated by the summary-pattern assertion rule.
 
 /openapi.yaml: validated in <test>ms
 
-❌ Validation failed with 1 error.
+❌ Validation failed with 2 errors.
 run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
 
 
diff --git a/packages/core/src/rules/common/assertions/__tests__/asserts.test.ts b/packages/core/src/rules/common/assertions/__tests__/asserts.test.ts
index acaefa8a..7a879317 100644
--- a/packages/core/src/rules/common/assertions/__tests__/asserts.test.ts
+++ b/packages/core/src/rules/common/assertions/__tests__/asserts.test.ts
@@ -43,6 +43,24 @@ describe('oas3 assertions', () => {
       });
     });
 
+    describe('notPattern', () => {
+      it('value should not match regex pattern', () => {
+        expect(asserts.notPattern('test string', '/test me/', baseLocation)).toEqual([]);
+        expect(asserts.notPattern('test string', '/test/', baseLocation)).toEqual([
+          { location: baseLocation, message: '"test string" should not match a regex /test/' },
+        ]);
+        expect(
+          asserts.notPattern(['test string', 'test me'], '/test other/', baseLocation)
+        ).toEqual([]);
+        expect(asserts.notPattern(['test string', 'test me'], '/test me/', baseLocation)).toEqual([
+          {
+            message: '"test me" should not match a regex /test me/',
+            location: baseLocation.key(),
+          },
+        ]);
+      });
+    });
+
     describe('ref', () => {
       it('value should have ref', () => {
         expect(asserts.ref({ $ref: 'text' }, true, baseLocation, { $ref: 'text' })).toEqual([]);