feat: add ref assertion (#675)

This commit is contained in:
Andriy Kaminskyy
2022-06-17 17:54:20 +03:00
committed by GitHub
parent 83cc35b9f8
commit bfd9ec0f79
30 changed files with 1012 additions and 289 deletions

View File

@@ -1,5 +1,4 @@
{
"printWidth": 100,
"singleQuote": true,
"trailingComma": "all"
"singleQuote": true
}

View File

@@ -17,4 +17,8 @@ lint:
property: operationId
message: Operation id should be camelCase
casing: camelCase
assert/camel-case-on-value:
subject: NamedParameters
casing: camelCase
message: Named Parameters should be camelCase
extends: []

View File

@@ -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

View File

@@ -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 <test>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.

View File

@@ -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.

View File

@@ -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:
65 | type: 'object'
Error was generated by the operation-w-context assertion rule.

View File

@@ -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: []

View File

@@ -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

View File

@@ -0,0 +1,9 @@
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string

View File

@@ -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'

View File

@@ -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 <test>ms
❌ Validation failed with 2 errors.
run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
`;

View File

@@ -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: []

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,9 @@
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string

View File

@@ -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'

View File

@@ -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 <test>ms
❌ Validation failed with 5 errors.
run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
`;

View File

@@ -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: []

View File

@@ -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

View File

@@ -0,0 +1,9 @@
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string

View File

@@ -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'

View File

@@ -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 <test>ms
❌ Validation failed with 3 errors.
run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
`;

View File

@@ -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,7 +93,7 @@ lint:
mutuallyRequired:
- created_at
- updated_at
#mutuallyRequired example with context
# mutuallyRequired example with context
assert/response-map-required-with-context:
subject: ResponsesMap
context:
@@ -105,15 +105,15 @@ lint:
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

View File

@@ -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.

View File

@@ -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'],

View File

@@ -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() });
});
});
});

View File

@@ -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<string, (value: any, condition: any) => 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
mutuallyRequired: (value: string[], condition: string[], baseLocation: Location) => {
return {
isValid:
getIntersectionLength(value, condition) > 0
? getIntersectionLength(value, condition) === condition.length
: true;
: 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(),
};
},
};

View File

@@ -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<string, any>[],
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<string, any>[],
context?: Record<string, any>[]
) {
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]);
}

View File

@@ -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<T extends NonUndefined> =
export type ResolveFn<T> = (
node: Referenced<T>,
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<T>(
node: Referenced<T>,
node: Referenced<T>
): { location: Location; node: T } | { location: undefined; node: undefined };
parentLocations: Record<string, Location>;
type: NormalizedNodeType;
@@ -121,8 +130,9 @@ export function walkDocument<T>(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<VisitorLevelContext> = new Set();
@@ -138,15 +148,17 @@ export function walkDocument<T>(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<T>(opts: {
const anyEnterVisitors = normalizedVisitors.any.enter;
const currentEnterVisitors = anyEnterVisitors.concat(
normalizedVisitors[type.name]?.enter || [],
normalizedVisitors[type.name]?.enter || []
);
const activatedContexts: Array<VisitorSkippedLevelContext | VisitorLevelContext> = [];
@@ -205,7 +217,7 @@ export function walkDocument<T>(opts: {
while (ctx) {
ctx.activatedOn!.value.nextLevelTypeActivated = pushStack(
ctx.activatedOn!.value.nextLevelTypeActivated,
type,
type
);
ctx = ctx.parent;
}
@@ -216,9 +228,10 @@ export function walkDocument<T>(opts: {
const { ignoreNextVisitorsOnNode } = visitWithContext(
visit,
resolvedNode,
node,
context,
ruleId,
severity,
severity
);
if (ignoreNextVisitorsOnNode) break;
}
@@ -282,7 +295,7 @@ export function walkDocument<T>(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<T>(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<T>(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<T>(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<T>(opts: {
// returns true ignores all the next visitors on the specific node
function visitWithContext(
visit: VisitFunction<any>,
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<T>(
ref: Referenced<T>,
from: string = currentLocation.source.absoluteRef,
from: string = currentLocation.source.absoluteRef
): ResolveResult<T> {
if (!isRef(ref)) return { location, node: ref };
const refId = makeRefId(from, ref.$ref);