From bfd9ec0f79c6e8620371fdf58ed449a6d3b56436 Mon Sep 17 00:00:00 2001 From: Andriy Kaminskyy Date: Fri, 17 Jun 2022 17:54:20 +0300 Subject: [PATCH] feat: add ref assertion (#675) --- .prettierrc.json | 3 +- .../.redocly.yaml | 10 +- .../openapi.yaml | 13 + .../snapshot.js | 16 +- .../.redocly.yaml | 2 +- .../assertions-enum-on-keys-error/snapshot.js | 14 +- __tests__/lint/assertions-error/snapshot.js | 34 +- .../.redocly.yaml | 18 ++ .../components/paths/Owners.yaml | 27 ++ .../components/schemas/Error.yaml | 9 + .../openapi.yaml | 67 ++++ .../snapshot.js | 40 +++ .../.redocly.yaml | 18 ++ .../components/Vets.yaml | 26 ++ .../components/paths/Owners.yaml | 27 ++ .../components/schemas/Error.yaml | 9 + .../assertions-ref-pattern-error/openapi.yaml | 67 ++++ .../assertions-ref-pattern-error/snapshot.js | 82 +++++ .../.redocly.yaml | 18 ++ .../components/paths/Owners.yaml | 27 ++ .../components/schemas/Error.yaml | 9 + .../openapi.yaml | 67 ++++ .../assertions-ref-required-error/snapshot.js | 54 ++++ __tests__/lint/assertions/.redocly.yaml | 48 +-- docs/resources/rules/assertions.md | 29 ++ jest.config.js | 8 +- .../assertions/__tests__/asserts.test.ts | 295 +++++++++--------- .../src/rules/common/assertions/asserts.ts | 149 ++++++--- .../core/src/rules/common/assertions/utils.ts | 57 +++- packages/core/src/walk.ts | 58 ++-- 30 files changed, 1012 insertions(+), 289 deletions(-) create mode 100644 __tests__/lint/assertions-ref-forbidden-error/.redocly.yaml create mode 100644 __tests__/lint/assertions-ref-forbidden-error/components/paths/Owners.yaml create mode 100644 __tests__/lint/assertions-ref-forbidden-error/components/schemas/Error.yaml create mode 100644 __tests__/lint/assertions-ref-forbidden-error/openapi.yaml create mode 100644 __tests__/lint/assertions-ref-forbidden-error/snapshot.js create mode 100644 __tests__/lint/assertions-ref-pattern-error/.redocly.yaml create mode 100644 __tests__/lint/assertions-ref-pattern-error/components/Vets.yaml create mode 100644 __tests__/lint/assertions-ref-pattern-error/components/paths/Owners.yaml create mode 100644 __tests__/lint/assertions-ref-pattern-error/components/schemas/Error.yaml create mode 100644 __tests__/lint/assertions-ref-pattern-error/openapi.yaml create mode 100644 __tests__/lint/assertions-ref-pattern-error/snapshot.js create mode 100644 __tests__/lint/assertions-ref-required-error/.redocly.yaml create mode 100644 __tests__/lint/assertions-ref-required-error/components/paths/Owners.yaml create mode 100644 __tests__/lint/assertions-ref-required-error/components/schemas/Error.yaml create mode 100644 __tests__/lint/assertions-ref-required-error/openapi.yaml create mode 100644 __tests__/lint/assertions-ref-required-error/snapshot.js diff --git a/.prettierrc.json b/.prettierrc.json index 5e2863a1..5ac85e27 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,5 +1,4 @@ { "printWidth": 100, - "singleQuote": true, - "trailingComma": "all" + "singleQuote": true } diff --git a/__tests__/lint/assertions-casing-camel-case-error/.redocly.yaml b/__tests__/lint/assertions-casing-camel-case-error/.redocly.yaml index 51437c0f..0bcfea7b 100644 --- a/__tests__/lint/assertions-casing-camel-case-error/.redocly.yaml +++ b/__tests__/lint/assertions-casing-camel-case-error/.redocly.yaml @@ -6,8 +6,8 @@ lint: rules: assert/operation-get: context: - - type: Operation - matchParentKeys: [get] + - type: Operation + matchParentKeys: [get] subject: Operation property: operationId message: Operation id for get operation should be camelCase @@ -17,4 +17,8 @@ lint: property: operationId message: Operation id should be camelCase casing: camelCase - extends: [] \ No newline at end of file + assert/camel-case-on-value: + subject: NamedParameters + casing: camelCase + message: Named Parameters should be camelCase + extends: [] diff --git a/__tests__/lint/assertions-casing-camel-case-error/openapi.yaml b/__tests__/lint/assertions-casing-camel-case-error/openapi.yaml index 7f2017a2..f1e56a06 100644 --- a/__tests__/lint/assertions-casing-camel-case-error/openapi.yaml +++ b/__tests__/lint/assertions-casing-camel-case-error/openapi.yaml @@ -35,3 +35,16 @@ paths: description: example description '404': description: example description + +components: + parameters: + camelCase: + in: query + name: type + schema: + type: string + Wrong_casE: + in: query + name: per_page + schema: + type: integer diff --git a/__tests__/lint/assertions-casing-camel-case-error/snapshot.js b/__tests__/lint/assertions-casing-camel-case-error/snapshot.js index ee0ec6b5..2cfebdbf 100644 --- a/__tests__/lint/assertions-casing-camel-case-error/snapshot.js +++ b/__tests__/lint/assertions-casing-camel-case-error/snapshot.js @@ -45,9 +45,23 @@ Operation id should be camelCase Error was generated by the operation-id-camel-case assertion rule. +[4] openapi.yaml:46:5 at #/components/parameters/Wrong_casE + +Named Parameters should be camelCase + +44 | schema: +45 | type: string +46 | Wrong_casE: + | ^^^^^^^^^^ +47 | in: query +48 | name: per_page + +Error was generated by the camel-case-on-value assertion rule. + + /openapi.yaml: validated in ms -❌ Validation failed with 3 errors. +❌ Validation failed with 4 errors. run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file. diff --git a/__tests__/lint/assertions-enum-on-keys-error/.redocly.yaml b/__tests__/lint/assertions-enum-on-keys-error/.redocly.yaml index 88d85d99..d7b85332 100644 --- a/__tests__/lint/assertions-enum-on-keys-error/.redocly.yaml +++ b/__tests__/lint/assertions-enum-on-keys-error/.redocly.yaml @@ -10,4 +10,4 @@ lint: message: Only application/json can be used enum: - application/json - extends: [] \ No newline at end of file + extends: [] diff --git a/__tests__/lint/assertions-enum-on-keys-error/snapshot.js b/__tests__/lint/assertions-enum-on-keys-error/snapshot.js index 0c77f653..1825f071 100644 --- a/__tests__/lint/assertions-enum-on-keys-error/snapshot.js +++ b/__tests__/lint/assertions-enum-on-keys-error/snapshot.js @@ -3,16 +3,16 @@ exports[`E2E lint assertions-enum-on-keys-error 1`] = ` validating /openapi.yaml... -[1] openapi.yaml:28:11 at #/paths/~1pet~1findByStatus/get/responses/200/content +[1] openapi.yaml:34:13 at #/paths/~1pet~1findByStatus/get/responses/200/content/application~1xml Only application/json can be used -26 | '200': -27 | description: example description -28 | content: - | ^^^^^^^ -29 | application/json: -30 | schema: +32 | items: +33 | $ref: '#/components/schemas/Dog' +34 | application/xml: + | ^^^^^^^^^^^^^^^ +35 | schema: +36 | type: array Error was generated by the media-type-application-json assertion rule. diff --git a/__tests__/lint/assertions-error/snapshot.js b/__tests__/lint/assertions-error/snapshot.js index b5af92f3..6461f5af 100644 --- a/__tests__/lint/assertions-error/snapshot.js +++ b/__tests__/lint/assertions-error/snapshot.js @@ -124,16 +124,16 @@ Operation summary must have a maximum of 2 characters Error was generated by the operation-summary-max-length assertion rule. -[9] openapi.yaml:31:9 at #/paths/~1pet~1findByStatus/put/requestBody/content +[9] openapi.yaml:32:11 at #/paths/~1pet~1findByStatus/put/requestBody/content/application~1json Only application/pdf can be used -29 | url: "https://example.com" 30 | requestBody: 31 | content: - | ^^^^^^^ 32 | application/json: + | ^^^^^^^^^^^^^^^^ 33 | schema: +34 | type: 'object' Error was generated by the media-type-pdf assertion rule. @@ -212,16 +212,16 @@ NamedExamples key must be in PascalCase Error was generated by the operation-id-camel-case assertion rule. -[15] openapi.yaml:56:5 at #/paths/~1pet~1findByStatus/post +[15] openapi.yaml:78:7 at #/paths/~1pet~1findByStatus/post/x-code-samples x-code-samples and x-internal must not be defined -54 | type: string -55 | description: hooray -56 | post: - | ^^^^ -57 | operationId: EXAMPLE -58 | summary: '' +76 | credit-file-identity-address: +77 | summary: Credit file with fallback +78 | x-code-samples: + | ^^^^^^^^^^^^^^ +79 | - lang: 'C#' +80 | source: | Error was generated by the operation-disallowed assertion rule. @@ -307,16 +307,16 @@ Operation object \`summary\` must be non-empty string. Error was generated by the operation-summary rule. -[21] openapi.yaml:62:11 at #/paths/~1pet~1findByStatus/post/responses/201/content +[21] openapi.yaml:63:13 at #/paths/~1pet~1findByStatus/post/responses/201/content/application~1pdf Media type should not be pdf -60 | '201': -61 | description: Test description -62 | content: - | ^^^^^^^ -63 | application/pdf: -64 | schema: +61 | description: Test description +62 | content: +63 | application/pdf: + | ^^^^^^^^^^^^^^^ +64 | schema: +65 | type: 'object' Error was generated by the operation-w-context assertion rule. diff --git a/__tests__/lint/assertions-ref-forbidden-error/.redocly.yaml b/__tests__/lint/assertions-ref-forbidden-error/.redocly.yaml new file mode 100644 index 00000000..75ab0fbb --- /dev/null +++ b/__tests__/lint/assertions-ref-forbidden-error/.redocly.yaml @@ -0,0 +1,18 @@ +apis: + main: + root: ./openapi.yaml + +lint: + rules: + assert/ref-forbidden: + context: + - type: Response + subject: MediaType + property: schema + message: Response MediaType schema should NOT have a ref + ref: false + assert/ref-forbidden-no-property: + subject: PathItem + message: PathItems should NOT should have a ref + ref: false + extends: [] diff --git a/__tests__/lint/assertions-ref-forbidden-error/components/paths/Owners.yaml b/__tests__/lint/assertions-ref-forbidden-error/components/paths/Owners.yaml new file mode 100644 index 00000000..dc0ca6e5 --- /dev/null +++ b/__tests__/lint/assertions-ref-forbidden-error/components/paths/Owners.yaml @@ -0,0 +1,27 @@ +get: + summary: List all owners + operationId: listOwners + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int + responses: + 404: + description: test + 200: + description: An paged array of owners + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + properties: + id: + type: integer diff --git a/__tests__/lint/assertions-ref-forbidden-error/components/schemas/Error.yaml b/__tests__/lint/assertions-ref-forbidden-error/components/schemas/Error.yaml new file mode 100644 index 00000000..d19d5ca8 --- /dev/null +++ b/__tests__/lint/assertions-ref-forbidden-error/components/schemas/Error.yaml @@ -0,0 +1,9 @@ +required: + - code + - message +properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/__tests__/lint/assertions-ref-forbidden-error/openapi.yaml b/__tests__/lint/assertions-ref-forbidden-error/openapi.yaml new file mode 100644 index 00000000..1cc266db --- /dev/null +++ b/__tests__/lint/assertions-ref-forbidden-error/openapi.yaml @@ -0,0 +1,67 @@ +openapi: '3.0.0' +info: + version: 1.0.0 + title: Swagger Petstore + description: Information about Petstore + license: + name: MIT + url: https://opensource.org/licenses/MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int + responses: + 404: + description: test + 200: + description: An paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + properties: + id: + type: integer + default: + description: unexpected error + content: + application/json: + schema: + $ref: ./components/schemas/Error.yaml + /owners: + $ref: components/paths/Owners.yaml +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/components/schemas/Pet' diff --git a/__tests__/lint/assertions-ref-forbidden-error/snapshot.js b/__tests__/lint/assertions-ref-forbidden-error/snapshot.js new file mode 100644 index 00000000..93a51ba3 --- /dev/null +++ b/__tests__/lint/assertions-ref-forbidden-error/snapshot.js @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`E2E lint assertions-ref-forbidden-error 1`] = ` + +validating /openapi.yaml... +[1] openapi.yaml:47:17 at #/paths/~1pets/get/responses/default/content/application~1json/schema + +Response MediaType schema should NOT have a ref + +45 | application/json: +46 | schema: +47 | $ref: ./components/schemas/Error.yaml + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +48 | /owners: +49 | $ref: components/paths/Owners.yaml + +Error was generated by the ref-forbidden assertion rule. + + +[2] openapi.yaml:49:5 at #/paths/~1owners + +PathItems should NOT should have a ref + +47 | $ref: ./components/schemas/Error.yaml +48 | /owners: +49 | $ref: components/paths/Owners.yaml + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +50 | components: +51 | schemas: + +Error was generated by the ref-forbidden-no-property assertion rule. + + +/openapi.yaml: validated in ms + +❌ Validation failed with 2 errors. +run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file. + + +`; diff --git a/__tests__/lint/assertions-ref-pattern-error/.redocly.yaml b/__tests__/lint/assertions-ref-pattern-error/.redocly.yaml new file mode 100644 index 00000000..b5f5b86f --- /dev/null +++ b/__tests__/lint/assertions-ref-pattern-error/.redocly.yaml @@ -0,0 +1,18 @@ +apis: + main: + root: ./openapi.yaml + +lint: + rules: + assert/ref-pattern: + context: + - type: Response + subject: MediaType + property: schema + message: Response MediaType schema should contain ref to components/schemas folder + ref: ^(\.\/)?components\/schemas\/.*\.yaml$ + assert/ref-pattern-no-properties: + subject: PathItem + message: PathItem should contain ref to components/paths folder + ref: ^(\.\/)?components\/paths\/.*\.yaml$ + extends: [] diff --git a/__tests__/lint/assertions-ref-pattern-error/components/Vets.yaml b/__tests__/lint/assertions-ref-pattern-error/components/Vets.yaml new file mode 100644 index 00000000..319afff1 --- /dev/null +++ b/__tests__/lint/assertions-ref-pattern-error/components/Vets.yaml @@ -0,0 +1,26 @@ +get: + summary: List all Vets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int + responses: + 404: + description: test + 200: + description: An paged array of Vets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + properties: + id: + type: integer diff --git a/__tests__/lint/assertions-ref-pattern-error/components/paths/Owners.yaml b/__tests__/lint/assertions-ref-pattern-error/components/paths/Owners.yaml new file mode 100644 index 00000000..dc0ca6e5 --- /dev/null +++ b/__tests__/lint/assertions-ref-pattern-error/components/paths/Owners.yaml @@ -0,0 +1,27 @@ +get: + summary: List all owners + operationId: listOwners + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int + responses: + 404: + description: test + 200: + description: An paged array of owners + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + properties: + id: + type: integer diff --git a/__tests__/lint/assertions-ref-pattern-error/components/schemas/Error.yaml b/__tests__/lint/assertions-ref-pattern-error/components/schemas/Error.yaml new file mode 100644 index 00000000..d19d5ca8 --- /dev/null +++ b/__tests__/lint/assertions-ref-pattern-error/components/schemas/Error.yaml @@ -0,0 +1,9 @@ +required: + - code + - message +properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/__tests__/lint/assertions-ref-pattern-error/openapi.yaml b/__tests__/lint/assertions-ref-pattern-error/openapi.yaml new file mode 100644 index 00000000..525d9c35 --- /dev/null +++ b/__tests__/lint/assertions-ref-pattern-error/openapi.yaml @@ -0,0 +1,67 @@ +openapi: '3.0.0' +info: + version: 1.0.0 + title: Swagger Petstore + description: Information about Petstore + license: + name: MIT + url: https://opensource.org/licenses/MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int + responses: + 404: + description: test + 200: + description: An paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pets' + default: + description: unexpected error + content: + application/json: + schema: + $ref: ./components/schemas/Error.yaml + /owners: + $ref: components/paths/Owners.yaml + /vets: + $ref: components/Vets.yaml +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/components/schemas/Pet' diff --git a/__tests__/lint/assertions-ref-pattern-error/snapshot.js b/__tests__/lint/assertions-ref-pattern-error/snapshot.js new file mode 100644 index 00000000..9813bcc6 --- /dev/null +++ b/__tests__/lint/assertions-ref-pattern-error/snapshot.js @@ -0,0 +1,82 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`E2E lint assertions-ref-pattern-error 1`] = ` + +validating /openapi.yaml... +[1] openapi.yaml:12:3 at #/paths/~1pets + +PathItem should contain ref to components/paths folder + +10 | - url: http://petstore.swagger.io/v1 +11 | paths: +12 | /pets: + | ^^^^^ +13 | get: +14 | summary: List all pets + +Error was generated by the ref-pattern-no-properties assertion rule. + + +[2] openapi.yaml:39:17 at #/paths/~1pets/get/responses/200/content/application~1json/schema + +Response MediaType schema should contain ref to components/schemas folder + +37 | application/json: +38 | schema: +39 | $ref: '#/components/schemas/Pets' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +40 | default: +41 | description: unexpected error + +Error was generated by the ref-pattern assertion rule. + + +[3] components/paths/Owners.yaml:24:11 at #/get/responses/200/content/application~1json/schema + +Response MediaType schema should contain ref to components/schemas folder + +22 | content: +23 | application/json: +24 | schema: + | ^^^^^^ +25 | properties: +26 | id: + +Error was generated by the ref-pattern assertion rule. + + +[4] openapi.yaml:49:5 at #/paths/~1vets + +PathItem should contain ref to components/paths folder + +47 | $ref: components/paths/Owners.yaml +48 | /vets: +49 | $ref: components/Vets.yaml + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +50 | components: +51 | schemas: + +Error was generated by the ref-pattern-no-properties assertion rule. + + +[5] components/Vets.yaml:23:11 at #/get/responses/200/content/application~1json/schema + +Response MediaType schema should contain ref to components/schemas folder + +21 | content: +22 | application/json: +23 | schema: + | ^^^^^^ +24 | properties: +25 | id: + +Error was generated by the ref-pattern assertion rule. + + +/openapi.yaml: validated in ms + +❌ Validation failed with 5 errors. +run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file. + + +`; diff --git a/__tests__/lint/assertions-ref-required-error/.redocly.yaml b/__tests__/lint/assertions-ref-required-error/.redocly.yaml new file mode 100644 index 00000000..72acaa80 --- /dev/null +++ b/__tests__/lint/assertions-ref-required-error/.redocly.yaml @@ -0,0 +1,18 @@ +apis: + main: + root: ./openapi.yaml + +lint: + rules: + assert/ref-required: + context: + - type: Response + subject: MediaType + property: schema + message: Response MediaType schema should have a ref + ref: true + assert/ref-required-no-property: + subject: PathItem + message: PathItems should have refs + ref: true + extends: [] diff --git a/__tests__/lint/assertions-ref-required-error/components/paths/Owners.yaml b/__tests__/lint/assertions-ref-required-error/components/paths/Owners.yaml new file mode 100644 index 00000000..dc0ca6e5 --- /dev/null +++ b/__tests__/lint/assertions-ref-required-error/components/paths/Owners.yaml @@ -0,0 +1,27 @@ +get: + summary: List all owners + operationId: listOwners + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int + responses: + 404: + description: test + 200: + description: An paged array of owners + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + properties: + id: + type: integer diff --git a/__tests__/lint/assertions-ref-required-error/components/schemas/Error.yaml b/__tests__/lint/assertions-ref-required-error/components/schemas/Error.yaml new file mode 100644 index 00000000..d19d5ca8 --- /dev/null +++ b/__tests__/lint/assertions-ref-required-error/components/schemas/Error.yaml @@ -0,0 +1,9 @@ +required: + - code + - message +properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/__tests__/lint/assertions-ref-required-error/openapi.yaml b/__tests__/lint/assertions-ref-required-error/openapi.yaml new file mode 100644 index 00000000..1cc266db --- /dev/null +++ b/__tests__/lint/assertions-ref-required-error/openapi.yaml @@ -0,0 +1,67 @@ +openapi: '3.0.0' +info: + version: 1.0.0 + title: Swagger Petstore + description: Information about Petstore + license: + name: MIT + url: https://opensource.org/licenses/MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int + responses: + 404: + description: test + 200: + description: An paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + properties: + id: + type: integer + default: + description: unexpected error + content: + application/json: + schema: + $ref: ./components/schemas/Error.yaml + /owners: + $ref: components/paths/Owners.yaml +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/components/schemas/Pet' diff --git a/__tests__/lint/assertions-ref-required-error/snapshot.js b/__tests__/lint/assertions-ref-required-error/snapshot.js new file mode 100644 index 00000000..c02af022 --- /dev/null +++ b/__tests__/lint/assertions-ref-required-error/snapshot.js @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`E2E lint assertions-ref-required-error 1`] = ` + +validating /openapi.yaml... +[1] openapi.yaml:12:3 at #/paths/~1pets + +PathItems should have refs + +10 | - url: http://petstore.swagger.io/v1 +11 | paths: +12 | /pets: + | ^^^^^ +13 | get: +14 | summary: List all pets + +Error was generated by the ref-required-no-property assertion rule. + + +[2] openapi.yaml:38:15 at #/paths/~1pets/get/responses/200/content/application~1json/schema + +Response MediaType schema should have a ref + +36 | content: +37 | application/json: +38 | schema: + | ^^^^^^ +39 | properties: +40 | id: + +Error was generated by the ref-required assertion rule. + + +[3] components/paths/Owners.yaml:24:11 at #/get/responses/200/content/application~1json/schema + +Response MediaType schema should have a ref + +22 | content: +23 | application/json: +24 | schema: + | ^^^^^^ +25 | properties: +26 | id: + +Error was generated by the ref-required assertion rule. + + +/openapi.yaml: validated in ms + +❌ Validation failed with 3 errors. +run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file. + + +`; diff --git a/__tests__/lint/assertions/.redocly.yaml b/__tests__/lint/assertions/.redocly.yaml index 8130b206..c3fe690b 100644 --- a/__tests__/lint/assertions/.redocly.yaml +++ b/__tests__/lint/assertions/.redocly.yaml @@ -12,7 +12,7 @@ lint: severity: error minLength: 13 pattern: /\.$/ -#property example + # property example assert/path-item-get-defined: subject: PathItem property: get @@ -30,7 +30,7 @@ lint: - description message: Every tag must have a name and description. defined: true -#context example + # context example assert/media-type-map-not-pdf: subject: - MediaTypeMap @@ -40,9 +40,9 @@ lint: - put - type: Response matchParentKeys: ['201', '200'] - disallowed: [ 'application/pdf' ] + disallowed: ['application/pdf'] message: Media type should not be pdf -#enum example + # enum example assert/media-type-map-enum: subject: MediaTypeMap message: Only application/json and application/pdf can be used @@ -61,29 +61,29 @@ lint: enum: - My resource - My collection -#pattern example + # pattern example assert/operation-summary-pattern: subject: Operation property: summary message: Summary should match a regex severity: error pattern: /resource/ -#casing + # casing assert/operation-id-casing: subject: Operation property: operationId message: NamedExamples key must be in PascalCase severity: error casing: camelCase -#mutuallyExclusive example + # mutuallyExclusive example assert/operation-mutually-exclusive: subject: Operation - message: "Operation must not define both properties together: description and externalDocs" + message: 'Operation must not define both properties together: description and externalDocs' severity: error mutuallyExclusive: - description - externalDocs -#mutuallyRequired example + # mutuallyRequired example assert/schema-properties-mutually-required: subject: SchemaProperties context: @@ -93,27 +93,27 @@ lint: mutuallyRequired: - created_at - updated_at -#mutuallyRequired example with context + # mutuallyRequired example with context assert/response-map-required-with-context: subject: ResponsesMap context: - - type: Operation - matchParentKeys: - - put + - type: Operation + matchParentKeys: + - put message: Must mutually define 200 and 201 responses for PUT requests. severity: error mutuallyRequired: - '200' - '201' -#requireAny example + # requireAny example assert/operation-require-any-description-or-external: subject: Operation - message: "Operation must have one of properties defined: description or externalDocs" + message: 'Operation must have one of properties defined: description or externalDocs' severity: error requireAny: - description - externalDocs -#disallowed example + # disallowed example assert/operation-disallowed: subject: Operation message: x-code-samples and x-internal must not be defined @@ -121,14 +121,14 @@ lint: disallowed: - x-code-samples - x-internal -#defined example + # defined example assert/operation-defined: subject: Operation property: x-codeSamples message: x-codeSamples must be defined severity: error defined: true -# undefined example + # undefined example assert/operation-undefined: subject: Operation property: x-code-samples @@ -137,24 +137,30 @@ lint: - x-codeSamples instead of x-code-samples severity: error undefined: true -#nonEmpty example + # nonEmpty example assert/operation-non-empty: subject: Operation property: summary message: Operation summary should not be empty severity: error nonEmpty: true -#minLength example + # minLength example assert/operation-min-length: subject: Operation property: summary message: Operation summary must have minimum of 2 chars length severity: error minLength: 2 -#maxLength example + # maxLength example assert/operation-max-length: subject: Operation property: summary message: Operation summary must have a maximum of 2 characters severity: error maxLength: 20 + # ref example + assert/ref: + subject: PathItem + message: No refs on Path Items + severity: error + ref: false diff --git a/docs/resources/rules/assertions.md b/docs/resources/rules/assertions.md index e73d666e..a71b2fbf 100644 --- a/docs/resources/rules/assertions.md +++ b/docs/resources/rules/assertions.md @@ -43,6 +43,7 @@ undefined | `boolean` | Asserts a property is undefined. See [undefined example] nonEmpty | `boolean` | Asserts a property is not empty. See [nonEmpty example](#nonempty-example). minLength | `integer` | Asserts a minimum length (inclusive) of a string or list (array). See [minLength example](#minlength-example). maxLength | `integer` | Asserts a maximum length (exclusive) of a string or list (array). See [maxLength example](#maxlength-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).| ## Context object @@ -435,7 +436,35 @@ lint: severity: error maxLength: 20 ``` + +### `ref` example + +The following example asserts that schema in MediaType contains a Reference object ($ref). + +```yaml +lint: + rules: + assert/mediatype-schema-has-ref: + - subject: MediaType + property: schema + message: Ref is required. + ref: true +``` + +Also, you can specify a Regular Expression to check if the reference object conforms to it: + +```yaml +lint: + rules: + assert/mediatype-schema-ref-pattern: + - subject: MediaType + property: schema + message: Ref needs to point to components directory. + ref: /^(\.\/)?components\/.*\.yaml$/ +``` + Redocly CLI + ## OpenAPI node types Redocly defines a type tree based on the document type. diff --git a/jest.config.js b/jest.config.js index 9cff8243..c00e8dfe 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,14 +13,14 @@ module.exports = { 'packages/core/': { statements: 77, branches: 69, - functions: 65, + functions: 66, lines: 77, }, 'packages/cli/': { - statements: 30, - branches: 27, + statements: 33, + branches: 28, functions: 29, - lines: 32, + lines: 34, }, }, testMatch: ['**/__tests__/**/*.test.ts', '**/*.test.ts'], 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 9ddfdc1c..e7057ec2 100644 --- a/packages/core/src/rules/common/assertions/__tests__/asserts.test.ts +++ b/packages/core/src/rules/common/assertions/__tests__/asserts.test.ts @@ -1,5 +1,9 @@ +import { Location } from '../../../../ref-utils'; +import { Source } from '../../../../resolve'; import { asserts } from '../asserts'; +let baseLocation = new Location(jest.fn() as any as Source, 'pointer'); + describe('oas3 assertions', () => { describe('generic rules', () => { const fakeNode = { @@ -10,232 +14,231 @@ describe('oas3 assertions', () => { describe('pattern', () => { it('value should match regex pattern', () => { - expect(asserts.pattern('test string', '/test/')).toBeTruthy(); - expect(asserts.pattern('test string', '/test me/')).toBeFalsy(); - expect(asserts.pattern(['test string', 'test me'], '/test/')).toBeTruthy(); - expect(asserts.pattern(['test string', 'test me'], '/test me/')).toBeFalsy(); + expect(asserts.pattern('test string', '/test/', baseLocation)).toEqual({ isValid: true }); + expect(asserts.pattern('test string', '/test me/', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.pattern(['test string', 'test me'], '/test/', baseLocation)).toEqual({ isValid: true }); + expect(asserts.pattern(['test string', 'test me'], '/test me/', baseLocation)).toEqual({ isValid: false, location: baseLocation.key() }); + expect(asserts.pattern('./components/smth/test.yaml', '/^(./)?components/.*.yaml$/', baseLocation)).toEqual({ isValid: true }); + expect(asserts.pattern('./other.yaml', '/^(./)?components/.*.yaml$/', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + }); + }); + + describe('ref', () => { + it('value should have ref', () => { + expect(asserts.ref({ $ref: 'text' }, true, baseLocation, { $ref: 'text' })).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.ref({}, true, baseLocation, {})).toEqual({ isValid: false, location: baseLocation.key() }); + }); + + it('value should not have ref', () => { + expect(asserts.ref({ $ref: 'text' }, false, baseLocation, { $ref: 'text' })).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.ref({}, false, baseLocation, {})).toEqual({ isValid: true, location: baseLocation.key() }); + }); + + it('value should match regex pattern', () => { + expect(asserts.ref({ $ref: 'test string' }, '/test/', baseLocation, { $ref: 'test string' })).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.ref({ $ref: 'test string' }, '/test me/', baseLocation, { $ref: 'test string' })).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.ref({ $ref: './components/smth/test.yaml' }, '/^(./)?components/.*.yaml$/', baseLocation, { $ref: './components/smth/test.yaml' })).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.ref({ $ref: './paths/smth/test.yaml' }, '/^(./)?components/.*.yaml$/', baseLocation, { $ref: './paths/smth/test.yaml' })).toEqual({ isValid: false, location: baseLocation }); }); }); describe('enum', () => { it('value should be among predefined keys', () => { - expect(asserts.enum('test', ['test', 'example'])).toBeTruthy(); - expect(asserts.enum(['test'], ['test', 'example'])).toBeTruthy(); - expect(asserts.enum(['test', 'example'], ['test', 'example'])).toBeTruthy(); - expect(asserts.enum(['test', 'example', 'foo'], ['test', 'example'])).toBeFalsy(); - expect(asserts.enum('test', ['foo', 'example'])).toBeFalsy(); - expect(asserts.enum(['test', 'foo'], ['test', 'example'])).toBeFalsy(); + expect(asserts.enum('test', ['test', 'example'], baseLocation)).toEqual({ isValid: true }); + expect(asserts.enum(['test'], ['test', 'example'], baseLocation)).toEqual({ isValid: true }); + expect(asserts.enum(['test', 'example'], ['test', 'example'], baseLocation)).toEqual({ isValid: true }); + expect(asserts.enum(['test', 'example', 'foo'], ['test', 'example'], baseLocation)).toEqual({ isValid: false, location: baseLocation.child('foo').key() }); + expect(asserts.enum('test', ['foo', 'example'], baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.enum(['test', 'foo'], ['test', 'example'], baseLocation)).toEqual({ isValid: false, location: baseLocation.child('foo').key() }); }); }); describe('defined', () => { it('value should be defined', () => { - expect(asserts.defined('test', true)).toBeTruthy(); - expect(asserts.defined(undefined, true)).toBeFalsy(); + expect(asserts.defined('test', true, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.defined(undefined, true, baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); it('value should be undefined', () => { - expect(asserts.defined(undefined, false)).toBeTruthy(); - expect(asserts.defined('test', false)).toBeFalsy(); + expect(asserts.defined(undefined, false, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.defined('test', false, baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); }); describe('undefined', () => { it('value should be undefined', () => { - expect(asserts.undefined(undefined, true)).toBeTruthy(); - expect(asserts.undefined('test', true)).toBeFalsy(); + expect(asserts.undefined(undefined, true, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.undefined('test', true, baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); it('value should be defined', () => { - expect(asserts.undefined('test', false)).toBeTruthy(); - expect(asserts.undefined(undefined, false)).toBeFalsy(); + expect(asserts.undefined('test', false, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.undefined(undefined, false, baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); }); describe('required', () => { it('values should be required', () => { - expect(asserts.required(['one', 'two', 'three'], ['one', 'two'])).toBeTruthy(); - expect(asserts.required(['one', 'two'], ['one', 'two', 'three'])).toBeFalsy(); + expect(asserts.required(['one', 'two', 'three'], ['one', 'two'], baseLocation)).toEqual({ isValid: true }); + expect(asserts.required(['one', 'two'], ['one', 'two', 'three'], baseLocation)).toEqual({ isValid: false, location: baseLocation.key() }); }); }); describe('nonEmpty', () => { it('value should not be empty', () => { - expect(asserts.nonEmpty('test', true)).toBeTruthy(); - expect(asserts.nonEmpty('', true)).toBeFalsy(); - expect(asserts.nonEmpty(null, true)).toBeFalsy(); - expect(asserts.nonEmpty(undefined, true)).toBeFalsy(); + expect(asserts.nonEmpty('test', true, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.nonEmpty('', true, baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.nonEmpty(null, true, baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.nonEmpty(undefined, true, baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); it('value should be empty', () => { - expect(asserts.nonEmpty('', false)).toBeTruthy(); - expect(asserts.nonEmpty(null, false)).toBeTruthy(); - expect(asserts.nonEmpty(undefined, false)).toBeTruthy(); - expect(asserts.nonEmpty('test', false)).toBeFalsy(); + expect(asserts.nonEmpty('', false, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.nonEmpty(null, false, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.nonEmpty(undefined, false, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.nonEmpty('test', false, baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); }); describe('minLength', () => { it('value should have less or equal than 5 symbols length', () => { - expect(asserts.minLength('test', 5)).toBeFalsy(); - expect(asserts.minLength([1, 2, 3, 4], 5)).toBeFalsy(); - expect(asserts.minLength([1, 2, 3, 4, 5], 5)).toBeTruthy(); - expect(asserts.minLength([1, 2, 3, 4, 5, 6], 5)).toBeTruthy(); - expect(asserts.minLength('example', 5)).toBeTruthy(); - expect(asserts.minLength([], 5)).toBeFalsy(); - expect(asserts.minLength('', 5)).toBeFalsy(); + expect(asserts.minLength('test', 5, baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.minLength([1, 2, 3, 4], 5, baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.minLength([1, 2, 3, 4, 5], 5, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.minLength([1, 2, 3, 4, 5, 6], 5, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.minLength('example', 5, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.minLength([], 5, baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.minLength('', 5, baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); }); describe('maxLength', () => { it('value should have more or equal than 5 symbols length', () => { - expect(asserts.maxLength('test', 5)).toBeTruthy(); - expect(asserts.maxLength([1, 2, 3, 4], 5)).toBeTruthy(); - expect(asserts.maxLength([1, 2, 3, 4, 5], 5)).toBeTruthy(); - expect(asserts.maxLength([1, 2, 3, 4, 5, 6], 5)).toBeFalsy(); - expect(asserts.maxLength('example', 5)).toBeFalsy(); - expect(asserts.maxLength([], 5)).toBeTruthy(); - expect(asserts.maxLength('', 5)).toBeTruthy(); + expect(asserts.maxLength('test', 5, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.maxLength([1, 2, 3, 4], 5, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.maxLength([1, 2, 3, 4, 5], 5, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.maxLength([1, 2, 3, 4, 5, 6], 5, baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.maxLength('example', 5, baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.maxLength([], 5, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.maxLength('', 5, baseLocation)).toEqual({ isValid: true, location: baseLocation }); }); }); describe('casing', () => { it('value should be camelCase', () => { - expect(asserts.casing(['testExample', 'fooBar'], 'camelCase')).toBeTruthy(); - expect(asserts.casing(['testExample', 'FooBar'], 'camelCase')).toBeFalsy(); - expect(asserts.casing('testExample', 'camelCase')).toBeTruthy(); - expect(asserts.casing('TestExample', 'camelCase')).toBeFalsy(); - expect(asserts.casing('test-example', 'camelCase')).toBeFalsy(); - expect(asserts.casing('test_example', 'camelCase')).toBeFalsy(); + expect(asserts.casing(['testExample', 'fooBar'], 'camelCase', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['testExample', 'FooBar'], 'camelCase', baseLocation)).toEqual({ isValid: false, location: baseLocation.child('FooBar').key() }); + expect(asserts.casing('testExample', 'camelCase', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing('TestExample', 'camelCase', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('test-example', 'camelCase', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('test_example', 'camelCase', baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); it('value should be PascalCase', () => { - expect(asserts.casing('TestExample', 'PascalCase')).toBeTruthy(); - expect(asserts.casing(['TestExample', 'FooBar'], 'PascalCase')).toBeTruthy(); - expect(asserts.casing(['TestExample', 'fooBar'], 'PascalCase')).toBeFalsy(); - expect(asserts.casing('testExample', 'PascalCase')).toBeFalsy(); - expect(asserts.casing('test-example', 'PascalCase')).toBeFalsy(); - expect(asserts.casing('test_example', 'PascalCase')).toBeFalsy(); + expect(asserts.casing('TestExample', 'PascalCase', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['TestExample', 'FooBar'], 'PascalCase', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['TestExample', 'fooBar'], 'PascalCase', baseLocation)).toEqual({ isValid: false, location: baseLocation.child('fooBar').key() }); + expect(asserts.casing('testExample', 'PascalCase', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('test-example', 'PascalCase', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('test_example', 'PascalCase', baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); it('value should be kebab-case', () => { - expect(asserts.casing('test-example', 'kebab-case')).toBeTruthy(); - expect(asserts.casing(['test-example', 'foo-bar'], 'kebab-case')).toBeTruthy(); - expect(asserts.casing(['test-example', 'foo_bar'], 'kebab-case')).toBeFalsy(); - expect(asserts.casing('testExample', 'kebab-case')).toBeFalsy(); - expect(asserts.casing('TestExample', 'kebab-case')).toBeFalsy(); - expect(asserts.casing('test_example', 'kebab-case')).toBeFalsy(); + expect(asserts.casing('test-example', 'kebab-case', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['test-example', 'foo-bar'], 'kebab-case', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['test-example', 'foo_bar'], 'kebab-case', baseLocation)).toEqual({ isValid: false, location: baseLocation.child('foo_bar').key() }); + expect(asserts.casing('testExample', 'kebab-case', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('TestExample', 'kebab-case', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('test_example', 'kebab-case', baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); it('value should be snake_case', () => { - expect(asserts.casing('test_example', 'snake_case')).toBeTruthy(); - expect(asserts.casing(['test_example', 'foo_bar'], 'snake_case')).toBeTruthy(); - expect(asserts.casing(['test_example', 'foo-bar'], 'snake_case')).toBeFalsy(); - expect(asserts.casing('testExample', 'snake_case')).toBeFalsy(); - expect(asserts.casing('TestExample', 'snake_case')).toBeFalsy(); - expect(asserts.casing('test-example', 'snake_case')).toBeFalsy(); + expect(asserts.casing('test_example', 'snake_case', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['test_example', 'foo_bar'], 'snake_case', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['test_example', 'foo-bar'], 'snake_case', baseLocation)).toEqual({ isValid: false, location: baseLocation.child('foo-bar').key() }); + expect(asserts.casing('testExample', 'snake_case', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('TestExample', 'snake_case', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('test-example', 'snake_case', baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); it('value should be MACRO_CASE', () => { - expect(asserts.casing('TEST_EXAMPLE', 'MACRO_CASE')).toBeTruthy(); - expect(asserts.casing(['TEST_EXAMPLE', 'FOO_BAR'], 'MACRO_CASE')).toBeTruthy(); - expect(asserts.casing(['TEST_EXAMPLE', 'FOO-BAR'], 'MACRO_CASE')).toBeFalsy(); - expect(asserts.casing('TEST_EXAMPLE_', 'MACRO_CASE')).toBeFalsy(); - expect(asserts.casing('_TEST_EXAMPLE', 'MACRO_CASE')).toBeFalsy(); - expect(asserts.casing('TEST__EXAMPLE', 'MACRO_CASE')).toBeFalsy(); - expect(asserts.casing('TEST-EXAMPLE', 'MACRO_CASE')).toBeFalsy(); - expect(asserts.casing('testExample', 'MACRO_CASE')).toBeFalsy(); - expect(asserts.casing('TestExample', 'MACRO_CASE')).toBeFalsy(); - expect(asserts.casing('test-example', 'MACRO_CASE')).toBeFalsy(); + expect(asserts.casing('TEST_EXAMPLE', 'MACRO_CASE', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['TEST_EXAMPLE', 'FOO_BAR'], 'MACRO_CASE', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['TEST_EXAMPLE', 'FOO-BAR'], 'MACRO_CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation.child('FOO-BAR').key() }); + expect(asserts.casing('TEST_EXAMPLE_', 'MACRO_CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('_TEST_EXAMPLE', 'MACRO_CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('TEST__EXAMPLE', 'MACRO_CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('TEST-EXAMPLE', 'MACRO_CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('testExample', 'MACRO_CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('TestExample', 'MACRO_CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('test-example', 'MACRO_CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); it('value should be COBOL-CASE', () => { - expect(asserts.casing('TEST-EXAMPLE', 'COBOL-CASE')).toBeTruthy(); - expect(asserts.casing(['TEST-EXAMPLE', 'FOO-BAR'], 'COBOL-CASE')).toBeTruthy(); - expect(asserts.casing(['TEST-EXAMPLE', 'FOO_BAR'], 'COBOL-CASE')).toBeFalsy(); - expect(asserts.casing('TEST-EXAMPLE-', 'COBOL-CASE')).toBeFalsy(); - expect(asserts.casing('0TEST-EXAMPLE', 'COBOL-CASE')).toBeFalsy(); - expect(asserts.casing('-TEST-EXAMPLE', 'COBOL-CASE')).toBeFalsy(); - expect(asserts.casing('TEST--EXAMPLE', 'COBOL-CASE')).toBeFalsy(); - expect(asserts.casing('TEST_EXAMPLE', 'COBOL-CASE')).toBeFalsy(); - expect(asserts.casing('testExample', 'COBOL-CASE')).toBeFalsy(); - expect(asserts.casing('TestExample', 'COBOL-CASE')).toBeFalsy(); - expect(asserts.casing('test-example', 'COBOL-CASE')).toBeFalsy(); + expect(asserts.casing('TEST-EXAMPLE', 'COBOL-CASE', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['TEST-EXAMPLE', 'FOO-BAR'], 'COBOL-CASE', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['TEST-EXAMPLE', 'FOO_BAR'], 'COBOL-CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation.child('FOO_BAR').key() }); + expect(asserts.casing('TEST-EXAMPLE-', 'COBOL-CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('0TEST-EXAMPLE', 'COBOL-CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('-TEST-EXAMPLE', 'COBOL-CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('TEST--EXAMPLE', 'COBOL-CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('TEST_EXAMPLE', 'COBOL-CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('testExample', 'COBOL-CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('TestExample', 'COBOL-CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('test-example', 'COBOL-CASE', baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); it('value should be flatcase', () => { - expect(asserts.casing('testexample', 'flatcase')).toBeTruthy(); - expect(asserts.casing(['testexample', 'foobar'], 'flatcase')).toBeTruthy(); - expect(asserts.casing(['testexample', 'foo_bar'], 'flatcase')).toBeFalsy(); - expect(asserts.casing('testexample_', 'flatcase')).toBeFalsy(); - expect(asserts.casing('0testexample', 'flatcase')).toBeFalsy(); - expect(asserts.casing('testExample', 'flatcase')).toBeFalsy(); - expect(asserts.casing('TestExample', 'flatcase')).toBeFalsy(); - expect(asserts.casing('test-example', 'flatcase')).toBeFalsy(); + expect(asserts.casing('testexample', 'flatcase', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['testexample', 'foobar'], 'flatcase', baseLocation)).toEqual({ isValid: true }); + expect(asserts.casing(['testexample', 'foo_bar'], 'flatcase', baseLocation)).toEqual({ isValid: false, location: baseLocation.child('foo_bar').key() }); + expect(asserts.casing('testexample_', 'flatcase', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('0testexample', 'flatcase', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('testExample', 'flatcase', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('TestExample', 'flatcase', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.casing('test-example', 'flatcase', baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); }); describe.skip('sortOrder', () => { it('value should be ordered in ASC direction', () => { - expect(asserts.sortOrder(['example', 'foo', 'test'], 'asc')).toBeTruthy(); - expect(asserts.sortOrder(['example', 'foo', 'test'], { direction: 'asc' })).toBeTruthy(); - expect(asserts.sortOrder(['example'], 'asc')).toBeTruthy(); - expect(asserts.sortOrder(['example', 'test', 'foo'], 'asc')).toBeFalsy(); - expect(asserts.sortOrder(['example', 'foo', 'test'], 'desc')).toBeFalsy(); - expect( - asserts.sortOrder([{ name: 'bar' }, { name: 'baz' }, { name: 'foo' }], { - direction: 'asc', - property: 'name', - }), - ).toBeTruthy(); - expect( - asserts.sortOrder([{ name: 'bar' }, { name: 'baz' }, { name: 'foo' }], { - direction: 'desc', - property: 'name', - }), - ).toBeFalsy(); + expect(asserts.sortOrder(['example', 'foo', 'test'], 'asc', baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.sortOrder(['example', 'foo', 'test'], { direction: 'asc' }, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.sortOrder(['example'], 'asc', baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.sortOrder(['example', 'test', 'foo'], 'asc', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.sortOrder(['example', 'foo', 'test'], 'desc', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.sortOrder([{ name: 'bar' }, { name: 'baz' }, { name: 'foo' }], { direction: 'asc', property: 'name' }, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.sortOrder([{ name: 'bar' }, { name: 'baz' }, { name: 'foo' }], { direction: 'desc', property: 'name' }, baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); it('value should be ordered in DESC direction', () => { - expect(asserts.sortOrder(['test', 'foo', 'example'], 'desc')).toBeTruthy(); - expect(asserts.sortOrder(['test', 'foo', 'example'], { direction: 'desc' })).toBeTruthy(); - expect(asserts.sortOrder(['example'], 'desc')).toBeTruthy(); - expect(asserts.sortOrder(['example', 'test', 'foo'], 'desc')).toBeFalsy(); - expect(asserts.sortOrder(['test', 'foo', 'example'], 'asc')).toBeFalsy(); - expect( - asserts.sortOrder([{ name: 'foo' }, { name: 'baz' }, { name: 'bar' }], { - direction: 'desc', - property: 'name', - }), - ).toBeTruthy(); - expect( - asserts.sortOrder([{ name: 'foo' }, { name: 'baz' }, { name: 'bar' }], { - direction: 'asc', - property: 'name', - }), - ).toBeFalsy(); + expect(asserts.sortOrder(['test', 'foo', 'example'], 'desc', baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.sortOrder(['test', 'foo', 'example'], { direction: 'desc' }, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.sortOrder(['example'], 'desc', baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.sortOrder(['example', 'test', 'foo'], 'desc', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.sortOrder(['test', 'foo', 'example'], 'asc', baseLocation)).toEqual({ isValid: false, location: baseLocation }); + expect(asserts.sortOrder([{ name: 'foo' }, { name: 'baz' }, { name: 'bar' }], { direction: 'desc', property: 'name' }, baseLocation)).toEqual({ isValid: true, location: baseLocation }); + expect(asserts.sortOrder([{ name: 'foo' }, { name: 'baz' }, { name: 'bar' }], { direction: 'asc', property: 'name' }, baseLocation)).toEqual({ isValid: false, location: baseLocation }); }); }); describe('mutuallyExclusive', () => { it('node should not have more than one property from predefined list', () => { - expect(asserts.mutuallyExclusive(Object.keys(fakeNode), ['foo', 'test'])).toBeTruthy(); - expect(asserts.mutuallyExclusive(Object.keys(fakeNode), [])).toBeTruthy(); - expect(asserts.mutuallyExclusive(Object.keys(fakeNode), ['foo', 'bar'])).toBeFalsy(); - expect( - asserts.mutuallyExclusive(Object.keys(fakeNode), ['foo', 'bar', 'test']), - ).toBeFalsy(); + expect(asserts.mutuallyExclusive(Object.keys(fakeNode), ['foo', 'test'], baseLocation)).toEqual({ isValid: true, location: baseLocation.key() }); + expect(asserts.mutuallyExclusive(Object.keys(fakeNode), [], baseLocation)).toEqual({ isValid: true, location: baseLocation.key() }); + expect(asserts.mutuallyExclusive(Object.keys(fakeNode), ['foo', 'bar'], baseLocation)).toEqual({ isValid: false, location: baseLocation.key() }); + expect(asserts.mutuallyExclusive(Object.keys(fakeNode), ['foo', 'bar', 'test'], baseLocation)).toEqual({ isValid: false, location: baseLocation.key() }); }); }); describe('mutuallyRequired', () => { it('node should have all the properties from predefined list', () => { - expect(asserts.mutuallyRequired(Object.keys(fakeNode), ['foo', 'bar'])).toBeTruthy(); - expect(asserts.mutuallyRequired(Object.keys(fakeNode), ['foo', 'bar', 'baz'])).toBeTruthy(); - expect(asserts.mutuallyRequired(Object.keys(fakeNode), [])).toBeTruthy(); - expect(asserts.mutuallyRequired(Object.keys(fakeNode), ['foo', 'test'])).toBeFalsy(); - expect(asserts.mutuallyRequired(Object.keys(fakeNode), ['foo', 'bar', 'test'])).toBeFalsy(); + expect(asserts.mutuallyRequired(Object.keys(fakeNode), ['foo', 'bar'], baseLocation)).toEqual({ isValid: true, location: baseLocation.key() }); + expect(asserts.mutuallyRequired(Object.keys(fakeNode), ['foo', 'bar', 'baz'], baseLocation)).toEqual({ isValid: true, location: baseLocation.key() }); + expect(asserts.mutuallyRequired(Object.keys(fakeNode), [], baseLocation)).toEqual({ isValid: true, location: baseLocation.key() }); + expect(asserts.mutuallyRequired(Object.keys(fakeNode), ['foo', 'test'], baseLocation)).toEqual({ isValid: false, location: baseLocation.key() }); + expect(asserts.mutuallyRequired(Object.keys(fakeNode), ['foo', 'bar', 'test'], baseLocation)).toEqual({ isValid: false, location: baseLocation.key() }); }); }); describe('requireAny', () => { it('node must have at least one property from predefined list', () => { - expect(asserts.requireAny(Object.keys(fakeNode), ['foo', 'test'])).toBeTruthy(); - expect(asserts.requireAny(Object.keys(fakeNode), ['test', 'bar'])).toBeTruthy(); - expect(asserts.requireAny(Object.keys(fakeNode), [])).toBeFalsy(); - expect(asserts.requireAny(Object.keys(fakeNode), ['test', 'test1'])).toBeFalsy(); - expect(asserts.requireAny(Object.keys(fakeNode), ['foo', 'bar'])).toBeTruthy(); - expect(asserts.requireAny(Object.keys(fakeNode), ['foo', 'bar', 'test'])).toBeTruthy(); + expect(asserts.requireAny(Object.keys(fakeNode), ['foo', 'test'], baseLocation)).toEqual({ isValid: true, location: baseLocation.key() }); + expect(asserts.requireAny(Object.keys(fakeNode), ['test', 'bar'], baseLocation)).toEqual({ isValid: true, location: baseLocation.key() }); + expect(asserts.requireAny(Object.keys(fakeNode), [], baseLocation)).toEqual({ isValid: false, location: baseLocation.key() }); + expect(asserts.requireAny(Object.keys(fakeNode), ['test', 'test1'], baseLocation)).toEqual({ isValid: false, location: baseLocation.key() }); + expect(asserts.requireAny(Object.keys(fakeNode), ['foo', 'bar'], baseLocation)).toEqual({ isValid: true, location: baseLocation.key() }); + expect(asserts.requireAny(Object.keys(fakeNode), ['foo', 'bar', 'test'], baseLocation)).toEqual({ isValid: true, location: baseLocation.key() }); }); }); }); diff --git a/packages/core/src/rules/common/assertions/asserts.ts b/packages/core/src/rules/common/assertions/asserts.ts index b40512ca..199b00a3 100644 --- a/packages/core/src/rules/common/assertions/asserts.ts +++ b/packages/core/src/rules/common/assertions/asserts.ts @@ -1,6 +1,18 @@ -import { OrderOptions, OrderDirection, isOrdered, getIntersectionLength } from './utils'; +import { Location } from '../../../ref-utils'; +import { isString as runOnValue } from '../../../utils'; +import { + OrderOptions, + OrderDirection, + isOrdered, + getIntersectionLength, + regexFromString, +} from './utils'; -type Asserts = Record boolean>; +type AssertResult = { isValid: boolean; location?: Location }; +type Asserts = Record< + string, + (value: any, condition: any, baseLocation: Location, rawValue?: any) => AssertResult +>; export const runOnKeysSet = new Set([ 'mutuallyExclusive', @@ -13,6 +25,8 @@ export const runOnKeysSet = new Set([ 'sortOrder', 'disallowed', 'required', + 'requireAny', + 'ref', ]); export const runOnValuesSet = new Set([ 'pattern', @@ -24,73 +38,82 @@ export const runOnValuesSet = new Set([ 'maxLength', 'casing', 'sortOrder', + 'ref', ]); export const asserts: Asserts = { - pattern: (value: string | string[], condition: string): boolean => { - if (typeof value === 'undefined') return true; // property doesn't exist, no need to lint it with this assert - const values = typeof value === 'string' ? [value] : value; - const regexOptions = condition.match(/(\b\/\b)(.+)/g) || ['/']; - condition = condition.slice(1).replace(regexOptions[0], ''); - const regx = new RegExp(condition, regexOptions[0].slice(1)); + pattern: (value: string | string[], condition: string, baseLocation: Location) => { + if (typeof value === 'undefined') return { isValid: true }; // property doesn't exist, no need to lint it with this assert + const values = runOnValue(value) ? [value] : value; + const regx = regexFromString(condition); for (let _val of values) { - if (!_val.match(regx)) { - return false; + if (!regx?.test(_val)) { + return { isValid: false, location: runOnValue(value) ? baseLocation : baseLocation.key() }; } } - return true; + return { isValid: true }; }, - enum: (value: string | string[], condition: string[]): boolean => { - if (typeof value === 'undefined') return true; // property doesn't exist, no need to lint it with this assert - const values = typeof value === 'string' ? [value] : value; + enum: (value: string | string[], condition: string[], baseLocation: Location) => { + if (typeof value === 'undefined') return { isValid: true }; // property doesn't exist, no need to lint it with this assert + const values = runOnValue(value) ? [value] : value; for (let _val of values) { if (!condition.includes(_val)) { - return false; + return { + isValid: false, + location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(), + }; } } - return true; + return { isValid: true }; }, - defined: (value: string | undefined, condition: boolean = true): boolean => { + defined: (value: string | undefined, condition: boolean = true, baseLocation: Location) => { const isDefined = typeof value !== 'undefined'; - return condition ? isDefined : !isDefined; + return { isValid: condition ? isDefined : !isDefined, location: baseLocation }; }, - required: (value: string[], keys: string[]): boolean => { + required: (value: string[], keys: string[], baseLocation: Location) => { for (const requiredKey of keys) { if (!value.includes(requiredKey)) { - return false; + return { isValid: false, location: baseLocation.key() }; } } - return true; + return { isValid: true }; }, - disallowed: (value: string | string[], condition: string[]): boolean => { - if (typeof value === 'undefined') return true; // property doesn't exist, no need to lint it with this assert - const values = typeof value === 'string' ? [value] : value; + disallowed: (value: string | string[], condition: string[], baseLocation: Location) => { + if (typeof value === 'undefined') return { isValid: true }; // property doesn't exist, no need to lint it with this assert + const values = runOnValue(value) ? [value] : value; for (let _val of values) { if (condition.includes(_val)) { - return false; + return { + isValid: false, + location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(), + }; } } - return true; + return { isValid: true }; }, - undefined: (value: any, condition: boolean = true): boolean => { + undefined: (value: any, condition: boolean = true, baseLocation: Location) => { const isUndefined = typeof value === 'undefined'; - return condition ? isUndefined : !isUndefined; + return { isValid: condition ? isUndefined : !isUndefined, location: baseLocation }; }, - nonEmpty: (value: string | undefined | null, condition: boolean = true): boolean => { + nonEmpty: ( + value: string | undefined | null, + condition: boolean = true, + baseLocation: Location + ) => { const isEmpty = typeof value === 'undefined' || value === null || value === ''; - return condition ? !isEmpty : isEmpty; + return { isValid: condition ? !isEmpty : isEmpty, location: baseLocation }; }, - minLength: (value: string | any[], condition: number): boolean => { - if (typeof value === 'undefined') return true; // property doesn't exist, no need to lint it with this assert - return value.length >= condition; + minLength: (value: string | any[], condition: number, baseLocation: Location) => { + if (typeof value === 'undefined') return { isValid: true }; // property doesn't exist, no need to lint it with this assert + return { isValid: value.length >= condition, location: baseLocation }; }, - maxLength: (value: string | any[], condition: number): boolean => { - if (typeof value === 'undefined') return true; // property doesn't exist, no need to lint it with this assert - return value.length <= condition; + maxLength: (value: string | any[], condition: number, baseLocation: Location) => { + if (typeof value === 'undefined') return { isValid: true }; // property doesn't exist, no need to lint it with this assert + return { isValid: value.length <= condition, location: baseLocation }; }, - casing: (value: string | string[], condition: string): boolean => { - if (typeof value === 'undefined') return true; // property doesn't exist, no need to lint it with this assert - const values = typeof value === 'string' ? [value] : value; + casing: (value: string | string[], condition: string, baseLocation: Location) => { + if (typeof value === 'undefined') return { isValid: true }; // property doesn't exist, no need to lint it with this assert + const values: string[] = runOnValue(value) ? [value] : value; for (let _val of values) { let matchCase = false; switch (condition) { @@ -117,24 +140,46 @@ export const asserts: Asserts = { break; } if (!matchCase) { - return false; + return { + isValid: false, + location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(), + }; } } - return true; + return { isValid: true }; }, - sortOrder: (value: any[], condition: OrderOptions | OrderDirection): boolean => { - if (typeof value === 'undefined') return true; - return isOrdered(value, condition); + sortOrder: (value: any[], condition: OrderOptions | OrderDirection, baseLocation: Location) => { + if (typeof value === 'undefined') return { isValid: true }; + return { isValid: isOrdered(value, condition), location: baseLocation }; }, - mutuallyExclusive: (value: string[], condition: string[]): boolean => { - return getIntersectionLength(value, condition) < 2; + mutuallyExclusive: (value: string[], condition: string[], baseLocation: Location) => { + return { isValid: getIntersectionLength(value, condition) < 2, location: baseLocation.key() }; }, - mutuallyRequired: (value: string[], condition: string[]): boolean => { - return getIntersectionLength(value, condition) > 0 - ? getIntersectionLength(value, condition) === condition.length - : true; + mutuallyRequired: (value: string[], condition: string[], baseLocation: Location) => { + return { + isValid: + getIntersectionLength(value, condition) > 0 + ? getIntersectionLength(value, condition) === condition.length + : true, + location: baseLocation.key(), + }; }, - requireAny: (value: string[], condition: string[]): boolean => { - return getIntersectionLength(value, condition) >= 1; + requireAny: (value: string[], condition: string[], baseLocation: Location) => { + return { isValid: getIntersectionLength(value, condition) >= 1, location: baseLocation.key() }; + }, + ref: (_value: any, condition: string | boolean, baseLocation, rawValue: any) => { + if (typeof rawValue === 'undefined') return { isValid: true }; // property doesn't exist, no need to lint it with this assert + const hasRef = rawValue.hasOwnProperty('$ref'); + if (typeof condition === 'boolean') { + return { + isValid: condition ? hasRef : !hasRef, + location: hasRef ? baseLocation : baseLocation.key(), + }; + } + const regex = regexFromString(condition); + return { + isValid: hasRef && regex?.test(rawValue['$ref']), + location: hasRef ? baseLocation : baseLocation.key(), + }; }, }; diff --git a/packages/core/src/rules/common/assertions/utils.ts b/packages/core/src/rules/common/assertions/utils.ts index 4c9ff455..03e89c49 100644 --- a/packages/core/src/rules/common/assertions/utils.ts +++ b/packages/core/src/rules/common/assertions/utils.ts @@ -1,4 +1,4 @@ -import { isRef } from '../../../ref-utils'; +import { isRef, Location } from '../../../ref-utils'; import { Problem, ProblemSeverity, UserContext } from '../../../walk'; import { asserts } from './asserts'; @@ -23,7 +23,7 @@ export type AssertToApply = { export function buildVisitorObject( subject: string, context: Record[], - subjectVisitor: any, + subjectVisitor: any ) { if (!context) { return { [subject]: subjectVisitor }; @@ -46,7 +46,7 @@ export function buildVisitorObject( if (matchParentKeys && excludeParentKeys) { throw new Error( - `Both 'matchParentKeys' and 'excludeParentKeys' can't be under one context item`, + `Both 'matchParentKeys' and 'excludeParentKeys' can't be under one context item` ); } @@ -75,9 +75,12 @@ export function buildVisitorObject( export function buildSubjectVisitor( properties: string | string[], asserts: AssertToApply[], - context?: Record[], + context?: Record[] ) { - return function (node: any, { report, location, key, type, resolve }: UserContext) { + return ( + node: any, + { report, location, rawLocation, key, type, resolve, rawNode }: UserContext + ) => { // We need to check context's last node if it has the same type as subject node; // if yes - that means we didn't create context's last node visitor, // so we need to handle 'matchParentKeys' and 'excludeParentKeys' conditions here; @@ -101,14 +104,28 @@ export function buildSubjectVisitor( } for (const assert of asserts) { + const currentLocation = assert.name === 'ref' ? rawLocation : location; if (properties) { for (const property of properties) { // we can have resolvable scalar so need to resolve value here. const value = isRef(node[property]) ? resolve(node[property])?.node : node[property]; - runAssertion(value, assert, location.child(property), report); + runAssertion({ + values: value, + rawValues: rawNode[property], + assert, + location: currentLocation.child(property), + report, + }); } } else { - runAssertion(Object.keys(node), assert, location.key(), report); + const value = assert.name === 'ref' ? rawNode : Object.keys(node); + runAssertion({ + values: Object.keys(node), + rawValues: value, + assert, + location: currentLocation, + report, + }); } } }; @@ -148,20 +165,28 @@ export function isOrdered(value: any[], options: OrderOptions | OrderDirection): return true; } -function runAssertion( - values: string | string[], - assert: AssertToApply, - location: any, - report: (problem: Problem) => void, -) { - const lintResult = asserts[assert.name](values, assert.conditions); - if (!lintResult) { +type RunAssertionParams = { + values: string | string[]; + rawValues: any; + assert: AssertToApply; + location: Location; + report: (problem: Problem) => void; +}; + +function runAssertion({ values, rawValues, assert, location, report }: RunAssertionParams) { + const lintResult = asserts[assert.name](values, assert.conditions, location, rawValues); + if (!lintResult.isValid) { report({ message: assert.message || `The ${assert.assertId} doesn't meet required conditions`, - location, + location: lintResult.location || location, forceSeverity: assert.severity, suggest: assert.suggest, ruleId: assert.assertId, }); } } + +export function regexFromString(input: string): RegExp | null { + const matches = input.match(/^\/(.*)\/(.*)|(.*)/); + return matches && new RegExp(matches[1] || matches[3], matches[2]); +} diff --git a/packages/core/src/walk.ts b/packages/core/src/walk.ts index f4b48efe..ca204929 100644 --- a/packages/core/src/walk.ts +++ b/packages/core/src/walk.ts @@ -7,7 +7,14 @@ import { VisitFunction, } from './visitors'; -import { ResolvedRefMap, Document, ResolveError, YamlParseError, Source, makeRefId } from './resolve'; +import { + ResolvedRefMap, + Document, + ResolveError, + YamlParseError, + Source, + makeRefId, +} from './resolve'; import { pushStack, popStack } from './utils'; import { OasVersion } from './oas-types'; import { NormalizedNodeType, isNamedType } from './types'; @@ -19,14 +26,16 @@ export type ResolveResult = export type ResolveFn = ( node: Referenced, - from?: string, + from?: string ) => { location: Location; node: T } | { location: undefined; node: undefined }; export type UserContext = { report(problem: Problem): void; location: Location; + rawNode: any; + rawLocation: Location; resolve( - node: Referenced, + node: Referenced ): { location: Location; node: T } | { location: undefined; node: undefined }; parentLocations: Record; type: NormalizedNodeType; @@ -121,8 +130,9 @@ export function walkDocument(opts: { type: NormalizedNodeType, location: Location, parent: any, - key: string | number, + key: string | number ) { + const rawLocation = location; let currentLocation = location; const { node: resolvedNode, location: resolvedLocation, error } = resolve(node); const enteredContexts: Set = new Set(); @@ -138,15 +148,17 @@ export function walkDocument(opts: { { report, resolve, + rawNode: node, + rawLocation, location, type, parent, key, parentLocations: {}, oasVersion: ctx.oasVersion, - getVisitorData: getVisitorDataFn.bind(undefined, ruleId) + getVisitorData: getVisitorDataFn.bind(undefined, ruleId), }, - { node: resolvedNode, location: resolvedLocation, error }, + { node: resolvedNode, location: resolvedLocation, error } ); if (resolvedLocation?.source.absoluteRef && ctx.refTypes) { ctx.refTypes.set(resolvedLocation?.source.absoluteRef, type); @@ -162,7 +174,7 @@ export function walkDocument(opts: { const anyEnterVisitors = normalizedVisitors.any.enter; const currentEnterVisitors = anyEnterVisitors.concat( - normalizedVisitors[type.name]?.enter || [], + normalizedVisitors[type.name]?.enter || [] ); const activatedContexts: Array = []; @@ -205,7 +217,7 @@ export function walkDocument(opts: { while (ctx) { ctx.activatedOn!.value.nextLevelTypeActivated = pushStack( ctx.activatedOn!.value.nextLevelTypeActivated, - type, + type ); ctx = ctx.parent; } @@ -216,9 +228,10 @@ export function walkDocument(opts: { const { ignoreNextVisitorsOnNode } = visitWithContext( visit, resolvedNode, + node, context, ruleId, - severity, + severity ); if (ignoreNextVisitorsOnNode) break; } @@ -282,7 +295,7 @@ export function walkDocument(opts: { const anyLeaveVisitors = normalizedVisitors.any.leave; const currentLeaveVisitors = (normalizedVisitors[type.name]?.leave || []).concat( - anyLeaveVisitors, + anyLeaveVisitors ); for (const context of activatedContexts.reverse()) { @@ -294,7 +307,7 @@ export function walkDocument(opts: { let ctx: VisitorLevelContext | null = context.parent; while (ctx) { ctx.activatedOn!.value.nextLevelTypeActivated = popStack( - ctx.activatedOn!.value.nextLevelTypeActivated, + ctx.activatedOn!.value.nextLevelTypeActivated ); ctx = ctx.parent; } @@ -304,7 +317,7 @@ export function walkDocument(opts: { for (const { context, visit, ruleId, severity } of currentLeaveVisitors) { if (!context.isSkippedLevel && enteredContexts.has(context)) { - visitWithContext(visit, resolvedNode, context, ruleId, severity); + visitWithContext(visit, resolvedNode, node, context, ruleId, severity); } } } @@ -321,15 +334,17 @@ export function walkDocument(opts: { { report, resolve, + rawNode: node, + rawLocation, location, type, parent, key, parentLocations: {}, oasVersion: ctx.oasVersion, - getVisitorData: getVisitorDataFn.bind(undefined, ruleId) + getVisitorData: getVisitorDataFn.bind(undefined, ruleId), }, - { node: resolvedNode, location: resolvedLocation, error }, + { node: resolvedNode, location: resolvedLocation, error } ); } } @@ -338,37 +353,42 @@ export function walkDocument(opts: { // returns true ignores all the next visitors on the specific node function visitWithContext( visit: VisitFunction, + resolvedNode: any, node: any, context: VisitorLevelContext, ruleId: string, - severity: ProblemSeverity, + severity: ProblemSeverity ) { const report = reportFn.bind(undefined, ruleId, severity); let ignoreNextVisitorsOnNode = false; visit( - node, + resolvedNode, { report, resolve, + rawNode: node, location: currentLocation, + rawLocation, type, parent, key, parentLocations: collectParentsLocations(context), oasVersion: ctx.oasVersion, - ignoreNextVisitorsOnNode: () => { ignoreNextVisitorsOnNode = true; }, + ignoreNextVisitorsOnNode: () => { + ignoreNextVisitorsOnNode = true; + }, getVisitorData: getVisitorDataFn.bind(undefined, ruleId), }, collectParents(context), - context, + context ); return { ignoreNextVisitorsOnNode }; } function resolve( ref: Referenced, - from: string = currentLocation.source.absoluteRef, + from: string = currentLocation.source.absoluteRef ): ResolveResult { if (!isRef(ref)) return { location, node: ref }; const refId = makeRefId(from, ref.$ref);