Projektstart

This commit is contained in:
2026-01-22 15:49:12 +01:00
parent 7212eb6f7a
commit 57e5f652f8
10637 changed files with 2598792 additions and 64 deletions

View File

@@ -0,0 +1,2 @@
# Set default behavior to automatically convert line endings
* text=auto eol=lf

View File

@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

View File

@@ -0,0 +1,21 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 15
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- "discussion"
- "feature request"
- "bug"
- "help wanted"
- "plugin suggestion"
- "good first issue"
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@@ -0,0 +1,85 @@
name: Benchmark PR
on:
pull_request_target:
types: [labeled]
jobs:
benchmark:
if: ${{ github.event.label.name == 'benchmark' }}
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
PR-BENCH: ${{ steps.benchmark-pr.outputs.BENCH_RESULT }}
MASTER-BENCH: ${{ steps.benchmark-master.outputs.BENCH_RESULT }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
ref: ${{github.event.pull_request.head.sha}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install
run: |
npm install --ignore-scripts
- name: Run benchmark
id: benchmark-pr
run: |
npm run --silent bench > ./bench-result
content=$(cat ./bench-result)
content="${content//'%'/'%25'}"
content="${content//$'\n'/'%0A'}"
content="${content//$'\r'/'%0D'}"
echo "::set-output name=BENCH_RESULT::$content"
# master benchmark
- uses: actions/checkout@v4
with:
ref: 'master'
- name: Install
run: |
npm install --ignore-scripts
- name: Run benchmark
id: benchmark-master
run: |
npm run --silent bench > ./bench-result
content=$(cat ./bench-result)
content="${content//'%'/'%25'}"
content="${content//$'\n'/'%0A'}"
content="${content//$'\r'/'%0D'}"
echo "::set-output name=BENCH_RESULT::$content"
output-benchmark:
if: "always()"
needs: [benchmark]
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Comment PR
uses: thollander/actions-comment-pull-request@v2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
message: |
**PR**:
```
${{ needs.benchmark.outputs.PR-BENCH }}
```
**MASTER**:
```
${{ needs.benchmark.outputs.MASTER-BENCH }}
```
- uses: actions-ecosystem/action-remove-labels@v1
with:
labels: |
benchmark
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,23 @@
name: CI
on:
push:
branches:
- main
- master
- next
- 'v*'
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
with:
license-check: true
lint: true

14
backend/node_modules/fast-json-stringify/.taprc generated vendored Normal file
View File

@@ -0,0 +1,14 @@
ts: false
jsx: false
branches: 65
functions: 100
lines: 89
statements: 89
nyc-arg:
- "--exclude=.cache/*"
- "--exclude=lib/schema-validator.js"
files:
- test/**/*.test.js

21
backend/node_modules/fast-json-stringify/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016-2018 Matteo Collina
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

739
backend/node_modules/fast-json-stringify/README.md generated vendored Normal file
View File

@@ -0,0 +1,739 @@
# fast-json-stringify
![CI](https://github.com/fastify/fast-json-stringify/workflows/CI/badge.svg)
[![NPM version](https://img.shields.io/npm/v/fast-json-stringify.svg?style=flat)](https://www.npmjs.com/package/fast-json-stringify)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
[![NPM downloads](https://img.shields.io/npm/dm/fast-json-stringify.svg?style=flat)](https://www.npmjs.com/package/fast-json-stringify)
__fast-json-stringify__ is significantly faster than `JSON.stringify()` for small payloads.
Its performance advantage shrinks as your payload grows.
It pairs well with [__flatstr__](https://www.npmjs.com/package/flatstr), which triggers a V8 optimization that improves performance when eventually converting the string to a `Buffer`.
### How it works
fast-json-stringify requires a [JSON Schema Draft 7](https://json-schema.org/specification-links.html#draft-7) input to generate a fast `stringify` function.
##### Benchmarks
- Machine: `EX41S-SSD, Intel Core i7, 4Ghz, 64GB RAM, 4C/8T, SSD`.
- Node.js `v18.12.1`
```
FJS creation x 4,129 ops/sec ±0.82% (92 runs sampled)
CJS creation x 184,196 ops/sec ±0.12% (97 runs sampled)
AJV Serialize creation x 61,130,591 ops/sec ±0.40% (92 runs sampled)
JSON.stringify array x 5,057 ops/sec ±0.10% (100 runs sampled)
fast-json-stringify array default x 6,243 ops/sec ±0.14% (98 runs sampled)
fast-json-stringify array json-stringify x 6,261 ops/sec ±0.30% (99 runs sampled)
compile-json-stringify array x 6,842 ops/sec ±0.18% (96 runs sampled)
AJV Serialize array x 6,964 ops/sec ±0.11% (95 runs sampled)
JSON.stringify large array x 248 ops/sec ±0.07% (90 runs sampled)
fast-json-stringify large array default x 99.96 ops/sec ±0.22% (74 runs sampled)
fast-json-stringify large array json-stringify x 248 ops/sec ±0.07% (90 runs sampled)
compile-json-stringify large array x 317 ops/sec ±0.09% (89 runs sampled)
AJV Serialize large array x 111 ops/sec ±0.07% (33 runs sampled)
JSON.stringify long string x 16,002 ops/sec ±0.09% (98 runs sampled)
fast-json-stringify long string x 15,979 ops/sec ±0.09% (96 runs sampled)
compile-json-stringify long string x 15,952 ops/sec ±0.31% (97 runs sampled)
AJV Serialize long string x 21,416 ops/sec ±0.08% (98 runs sampled)
JSON.stringify short string x 12,944,272 ops/sec ±0.09% (96 runs sampled)
fast-json-stringify short string x 30,585,790 ops/sec ±0.27% (97 runs sampled)
compile-json-stringify short string x 30,656,406 ops/sec ±0.12% (96 runs sampled)
AJV Serialize short string x 30,406,785 ops/sec ±0.37% (96 runs sampled)
JSON.stringify obj x 3,153,043 ops/sec ±0.33% (99 runs sampled)
fast-json-stringify obj x 6,866,434 ops/sec ±0.11% (100 runs sampled)
compile-json-stringify obj x 15,886,723 ops/sec ±0.15% (98 runs sampled)
AJV Serialize obj x 8,969,043 ops/sec ±0.36% (97 runs sampled)
JSON stringify date x 1,126,547 ops/sec ±0.09% (97 runs sampled)
fast-json-stringify date format x 1,836,188 ops/sec ±0.12% (99 runs sampled)
compile-json-stringify date format x 1,125,735 ops/sec ±0.19% (98 runs sampled)
```
#### Table of contents:
- <a href="#example">`Example`</a>
- <a href="#options">`Options`</a>
- <a href="#api">`API`</a>
- <a href="#fastJsonStringify">`fastJsonStringify`</a>
- <a href="#specific">`Specific use cases`</a>
- <a href="#required">`Required`</a>
- <a href="#missingFields">`Missing fields`</a>
- <a href="#patternProperties">`Pattern Properties`</a>
- <a href="#additionalProperties">`Additional Properties`</a>
- <a href="#AnyOf-and-OneOf">`AnyOf` and `OneOf`</a>
- <a href="#ref">`Reuse - $ref`</a>
- <a href="#long">`Long integers`</a>
- <a href="#integer">`Integers`</a>
- <a href="#nullable">`Nullable`</a>
- <a href="#largearrays">`Large Arrays`</a>
- <a href="#security">`Security Notice`</a>
- <a href="#debug">`Debug Mode`</a>
- <a href="#standalone">`Standalone Mode`</a>
- <a href="#acknowledgements">`Acknowledgements`</a>
- <a href="#license">`License`</a>
<a name="example"></a>
Try it out on RunKit: <a href="https://runkit.com/npm/fast-json-stringify">https://runkit.com/npm/fast-json-stringify</a>
## Example
```js
const fastJson = require('fast-json-stringify')
const stringify = fastJson({
title: 'Example Schema',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
age: {
description: 'Age in years',
type: 'integer'
},
reg: {
type: 'string'
}
}
})
console.log(stringify({
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
reg: /"([^"]|\\")*"/
}))
```
<a name="options"></a>
## Options
Optionally, you may provide to `fast-json-stringify` an option object as second parameter:
```js
const fastJson = require('fast-json-stringify')
const stringify = fastJson(mySchema, {
schema: { ... },
ajv: { ... },
rounding: 'ceil'
})
```
- `schema`: external schemas references by $ref property. [More details](#ref)
- `ajv`: [ajv v8 instance's settings](https://ajv.js.org/options.html) for those properties that require `ajv`. [More details](#anyof)
- `rounding`: setup how the `integer` types will be rounded when not integers. [More details](#integer)
- `largeArrayMechanism`: set the mechanism that should be used to handle large
(by default `20000` or more items) arrays. [More details](#largearrays)
<a name="api"></a>
## API
<a name="fastJsonStringify"></a>
### fastJsonStringify(schema)
Build a `stringify()` function based on [jsonschema draft 7 spec](https://json-schema.org/specification-links.html#draft-7).
Supported types:
* `'string'`
* `'integer'`
* `'number'`
* `'array'`
* `'object'`
* `'boolean'`
* `'null'`
And nested ones, too.
<a name="specific"></a>
#### Specific use cases
| Instance | Serialized as |
| -------- | ---------------------------- |
| `Date` | `string` via `toISOString()` |
| `RegExp` | `string` |
| `BigInt` | `integer` via `toString` |
[JSON Schema built-in formats](https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats) for dates are supported and will be serialized as:
| Format | Serialized format example |
| ----------- | -------------------------- |
| `date-time` | `2020-04-03T09:11:08.615Z` |
| `date` | `2020-04-03` |
| `time` | `09:11:08` |
**Note**: In the case of string formatted Date and not Date Object, there will be no manipulation on it. It should be properly formatted.
Example with a Date object:
```javascript
const stringify = fastJson({
title: 'Example Schema with string date-time field',
type: 'string',
format: 'date-time'
})
const date = new Date()
console.log(stringify(date)) // '"YYYY-MM-DDTHH:mm:ss.sssZ"'
```
<a name="required"></a>
#### Required
You can set specific fields of an object as required in your schema by adding the field name inside the `required` array in your schema.
Example:
```javascript
const schema = {
title: 'Example Schema with required field',
type: 'object',
properties: {
nickname: {
type: 'string'
},
mail: {
type: 'string'
}
},
required: ['mail']
}
```
If the object to stringify is missing the required field(s), `fast-json-stringify` will throw an error.
<a name="missingFields"></a>
#### Missing fields
If a field *is present* in the schema (and is not required) but it *is not present* in the object to stringify, `fast-json-stringify` will not write it in the final string.
Example:
```javascript
const stringify = fastJson({
title: 'Example Schema',
type: 'object',
properties: {
nickname: {
type: 'string'
},
mail: {
type: 'string'
}
}
})
const obj = {
mail: 'mail@example.com'
}
console.log(stringify(obj)) // '{"mail":"mail@example.com"}'
```
<a name="defaults"></a>
#### Defaults
`fast-json-stringify` supports `default` jsonschema key in order to serialize a value
if it is `undefined` or not present.
Example:
```javascript
const stringify = fastJson({
title: 'Example Schema',
type: 'object',
properties: {
nickname: {
type: 'string',
default: 'the default string'
}
}
})
console.log(stringify({})) // '{"nickname":"the default string"}'
console.log(stringify({nickname: 'my-nickname'})) // '{"nickname":"my-nickname"}'
```
<a name="patternProperties"></a>
#### Pattern properties
`fast-json-stringify` supports pattern properties as defined by JSON schema.
*patternProperties* must be an object, where the key is a valid regex and the value is an object, declared in this way: `{ type: 'type' }`.
*patternProperties* will work only for the properties that are not explicitly listed in the properties object.
Example:
```javascript
const stringify = fastJson({
title: 'Example Schema',
type: 'object',
properties: {
nickname: {
type: 'string'
}
},
patternProperties: {
'num': {
type: 'number'
},
'.*foo$': {
type: 'string'
}
}
})
const obj = {
nickname: 'nick',
matchfoo: 42,
otherfoo: 'str',
matchnum: 3
}
console.log(stringify(obj)) // '{"matchfoo":"42","otherfoo":"str","matchnum":3,"nickname":"nick"}'
```
<a name="additionalProperties"></a>
#### Additional properties
`fast-json-stringify` supports additional properties as defined by JSON schema.
*additionalProperties* must be an object or a boolean, declared in this way: `{ type: 'type' }`.
*additionalProperties* will work only for the properties that are not explicitly listed in the *properties* and *patternProperties* objects.
If *additionalProperties* is not present or is set to `false`, every property that is not explicitly listed in the *properties* and *patternProperties* objects,will be ignored, as described in <a href="#missingFields">Missing fields</a>.
Missing fields are ignored to avoid having to rewrite objects before serializing. However, other schema rules would throw in similar situations.
If *additionalProperties* is set to `true`, it will be used by `JSON.stringify` to stringify the additional properties. If you want to achieve maximum performance, we strongly encourage you to use a fixed schema where possible.
The additional properties will always be serialized at the end of the object.
Example:
```javascript
const stringify = fastJson({
title: 'Example Schema',
type: 'object',
properties: {
nickname: {
type: 'string'
}
},
patternProperties: {
'num': {
type: 'number'
},
'.*foo$': {
type: 'string'
}
},
additionalProperties: {
type: 'string'
}
})
const obj = {
nickname: 'nick',
matchfoo: 42,
otherfoo: 'str',
matchnum: 3,
nomatchstr: 'valar morghulis',
nomatchint: 313
}
console.log(stringify(obj)) // '{"nickname":"nick","matchfoo":"42","otherfoo":"str","matchnum":3,"nomatchstr":"valar morghulis",nomatchint:"313"}'
```
#### AnyOf and OneOf
`fast-json-stringify` supports the **anyOf** and **oneOf** keywords as defined by JSON schema. Both must be an array of valid JSON schemas. The different schemas will be tested in the specified order. The more schemas `stringify` has to try before finding a match, the slower it will be.
*anyOf* and *oneOf* use [ajv](https://www.npmjs.com/package/ajv) as a JSON schema validator to find the schema that matches the data. This has an impact on performance—only use it as a last resort.
Example:
```javascript
const stringify = fastJson({
title: 'Example Schema',
type: 'object',
properties: {
'undecidedType': {
'anyOf': [{
type: 'string'
}, {
type: 'boolean'
}]
}
}
})
```
When specifying object JSON schemas for *anyOf*, add *required* validation keyword to match only the objects with the properties you want.
Example:
```javascript
const stringify = fastJson({
title: 'Example Schema',
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
savedId: { type: 'string' }
},
// without "required" validation any object will match
required: ['savedId']
},
{
type: 'object',
properties: {
error: { type: 'string' }
},
required: ['error']
}
]
}
})
```
<a name="if-then-else"></a>
#### If/then/else
`fast-json-stringify` supports `if/then/else` jsonschema feature. See [ajv documentation](https://ajv.js.org/keywords.html#ifthenelse).
Example:
```javascript
const stringify = fastJson({
'type': 'object',
'properties': {
},
'if': {
'properties': {
'kind': { 'type': 'string', 'enum': ['foobar'] }
}
},
'then': {
'properties': {
'kind': { 'type': 'string', 'enum': ['foobar'] },
'foo': { 'type': 'string' },
'bar': { 'type': 'number' }
}
},
'else': {
'properties': {
'kind': { 'type': 'string', 'enum': ['greeting'] },
'hi': { 'type': 'string' },
'hello': { 'type': 'number' }
}
}
})
console.log(stringify({
kind: 'greeting',
foo: 'FOO',
bar: 42,
hi: 'HI',
hello: 45
})) // {"kind":"greeting","hi":"HI","hello":45}
console.log(stringify({
kind: 'foobar',
foo: 'FOO',
bar: 42,
hi: 'HI',
hello: 45
})) // {"kind":"foobar","foo":"FOO","bar":42}
```
**NB** Do not declare the properties twice or you will print them twice!
<a name="ref"></a>
#### Reuse - $ref
If you want to reuse a definition of a value, you can use the property `$ref`.
The value of `$ref` must be a string in [JSON Pointer](https://tools.ietf.org/html/rfc6901) format.
Example:
```javascript
const schema = {
title: 'Example Schema',
definitions: {
num: {
type: 'object',
properties: {
int: {
type: 'integer'
}
}
},
str: {
type: 'string'
}
},
type: 'object',
properties: {
nickname: {
$ref: '#/definitions/str'
}
},
patternProperties: {
'num': {
$ref: '#/definitions/num'
}
},
additionalProperties: {
$ref: '#/definitions/def'
}
}
const stringify = fastJson(schema)
```
If you need to use an external definition, you can pass it as an option to `fast-json-stringify`.
Example:
```javascript
const schema = {
title: 'Example Schema',
type: 'object',
properties: {
nickname: {
$ref: 'strings#/definitions/str'
}
},
patternProperties: {
'num': {
$ref: 'numbers#/definitions/num'
}
},
additionalProperties: {
$ref: 'strings#/definitions/def'
}
}
const externalSchema = {
numbers: {
definitions: {
num: {
type: 'object',
properties: {
int: {
type: 'integer'
}
}
}
}
},
strings: require('./string-def.json')
}
const stringify = fastJson(schema, { schema: externalSchema })
```
External definitions can also reference each other.
Example:
```javascript
const schema = {
title: 'Example Schema',
type: 'object',
properties: {
foo: {
$ref: 'strings#/definitions/foo'
}
}
}
const externalSchema = {
strings: {
definitions: {
foo: {
$ref: 'things#/definitions/foo'
}
}
},
things: {
definitions: {
foo: {
type: 'string'
}
}
}
}
const stringify = fastJson(schema, { schema: externalSchema })
```
<a name="long"></a>
#### Long integers
By default the library will handle automatically [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt).
<a name="integer"></a>
#### Integers
The `type: integer` property will be truncated if a floating point is provided.
You can customize this behaviour with the `rounding` option that will accept [`round`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round), [`ceil`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil), [`floor`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor) or [`trunc`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc). Default is `trunc`:
```js
const stringify = fastJson(schema, { rounding: 'ceil' })
```
<a name="nullable"></a>
#### Nullable
According to the [Open API 3.0 specification](https://swagger.io/docs/specification/data-models/data-types/#null), a value that can be null must be declared `nullable`.
##### Nullable object
```javascript
const stringify = fastJson({
'title': 'Nullable schema',
'type': 'object',
'nullable': true,
'properties': {
'product': {
'nullable': true,
'type': 'object',
'properties': {
'name': {
'type': 'string'
}
}
}
}
})
console.log(stringify({product: {name: "hello"}})) // "{"product":{"name":"hello"}}"
console.log(stringify({product: null})) // "{"product":null}"
console.log(stringify(null)) // null
```
Otherwise, instead of raising an error, null values will be coerced as follows:
- `integer` -> `0`
- `number` -> `0`
- `string` -> `""`
- `boolean` -> `false`
- `object` -> `{}`
- `array` -> `[]`
<a name="largearrays"></a>
#### Large Arrays
Large arrays are, for the scope of this document, defined as arrays containing,
by default, `20000` elements or more. That value can be adjusted via the option
parameter `largeArraySize`.
At some point the overhead caused by the default mechanism used by
`fast-json-stringify` to handle arrays starts increasing exponentially, leading
to slow overall executions.
##### Settings
In order to improve that the user can set the `largeArrayMechanism` and
`largeArraySize` options.
`largeArrayMechanism`'s default value is `default`. Valid values for it are:
- `default` - This option is a compromise between performance and feature set by
still providing the expected functionality out of this lib but giving up some
possible performance gain. With this option set, **large arrays** would be
stringified by joining their stringified elements using `Array.join` instead of
string concatenation for better performance
- `json-stringify` - This option will remove support for schema validation
within **large arrays** completely. By doing so the overhead previously
mentioned is nulled, greatly improving execution time. Mind there's no change
in behavior for arrays not considered _large_
`largeArraySize`'s default value is `20000`. Valid values for it are
integer-like values, such as:
- `20000`
- `2e4`
- `'20000'`
- `'2e4'` - _note this will be converted to `2`, not `20000`_
- `1.5` - _note this will be converted to `1`_
<a name="unsafe"></a>
#### Unsafe string
By default, the library escapes all strings. With the 'unsafe' format, the string isn't escaped. This has a potentially dangerous security issue. You can use it only if you are sure that your data doesn't need escaping. The advantage is a significant performance improvement.
Example:
```javascript
const stringify = fastJson({
title: 'Example Schema',
type: 'object',
properties: {
'code': {
type: 'string',
format 'unsafe'
}
}
})
```
##### Benchmarks
For reference, here goes some benchmarks for comparison over the three
mechanisms. Benchmarks conducted on an old machine.
- Machine: `ST1000LM024 HN-M 1TB HDD, Intel Core i7-3610QM @ 2.3GHz, 12GB RAM, 4C/8T`.
- Node.js `v16.13.1`
```
JSON.stringify large array x 157 ops/sec ±0.73% (86 runs sampled)
fast-json-stringify large array default x 48.72 ops/sec ±4.92% (48 runs sampled)
fast-json-stringify large array json-stringify x 157 ops/sec ±0.76% (86 runs sampled)
compile-json-stringify large array x 175 ops/sec ±4.47% (79 runs sampled)
AJV Serialize large array x 58.76 ops/sec ±4.59% (60 runs sampled)
```
<a name="security"></a>
## Security notice
Treat the schema definition as application code, it
is not safe to use user-provided schemas.
To achieve low cost and high performance redaction `fast-json-stringify`
creates and compiles a function (using the `Function` constructor) on initialization.
While the `schema` is currently validated for any developer errors,
there is no guarantee that supplying user-generated schema could not
expose your application to remote attacks.
Users are responsible for sending trusted data. `fast-json-stringify` guarantees that you will get
a valid output only if your input matches the schema or can be coerced to the schema. If your input
doesn't match the schema, you will get undefined behavior.
<a name="debug"></a>
### Debug Mode
The debug mode can be activated during your development to understand what is going on when things do not
work as you expect.
```js
const debugCompiled = fastJson({
title: 'default string',
type: 'object',
properties: {
firstName: {
type: 'string'
}
}
}, { mode: 'debug' })
console.log(debugCompiled) // it is a object contain code, ajv instance
const rawString = debugCompiled.code // it is the generated code
console.log(rawString)
const stringify = fastJson.restore(debugCompiled) // use the generated string to get back the `stringify` function
console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"Foo"}'
```
<a name="standalone"></a>
### Standalone Mode
The standalone mode is used to compile the code that can be directly run by `node`
itself. You need to have `fast-json-stringify` installed for the standalone code to work.
```js
const fs = require('fs')
const code = fastJson({
title: 'default string',
type: 'object',
properties: {
firstName: {
type: 'string'
}
}
}, { mode: 'standalone' })
fs.writeFileSync('stringify.js', code)
const stringify = require('stringify.js')
console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"Foo"}'
```
<a name="acknowledgements"></a>
## Acknowledgements
This project was kindly sponsored by [nearForm](https://nearform.com).
<a name="license"></a>
## License
MIT

View File

@@ -0,0 +1,116 @@
'use strict'
const { spawn } = require('child_process')
const cliSelect = require('cli-select')
const simpleGit = require('simple-git')
const git = simpleGit(process.cwd())
const COMMAND = 'npm run bench'
const DEFAULT_BRANCH = 'master'
const PERCENT_THRESHOLD = 5
const greyColor = '\x1b[30m'
const redColor = '\x1b[31m'
const greenColor = '\x1b[32m'
const resetColor = '\x1b[0m'
async function selectBranchName (message, branches) {
console.log(message)
const result = await cliSelect({
type: 'list',
name: 'branch',
values: branches
})
console.log(result.value)
return result.value
}
async function executeCommandOnBranch (command, branch) {
console.log(`${greyColor}Checking out "${branch}"${resetColor}`)
await git.checkout(branch)
console.log(`${greyColor}Execute "${command}"${resetColor}`)
const childProcess = spawn(command, { stdio: 'pipe', shell: true })
let result = ''
childProcess.stdout.on('data', (data) => {
process.stdout.write(data.toString())
result += data.toString()
})
await new Promise(resolve => childProcess.on('close', resolve))
console.log()
return parseBenchmarksStdout(result)
}
function parseBenchmarksStdout (text) {
const results = []
const lines = text.split('\n')
for (const line of lines) {
const match = /^(.+?)(\.*) x (.+) ops\/sec .*$/.exec(line)
if (match !== null) {
results.push({
name: match[1],
alignedName: match[1] + match[2],
result: parseInt(match[3].split(',').join(''))
})
}
}
return results
}
function compareResults (featureBranch, mainBranch) {
for (const { name, alignedName, result: mainBranchResult } of mainBranch) {
const featureBranchBenchmark = featureBranch.find(result => result.name === name)
if (featureBranchBenchmark) {
const featureBranchResult = featureBranchBenchmark.result
const percent = (featureBranchResult - mainBranchResult) * 100 / mainBranchResult
const roundedPercent = Math.round(percent * 100) / 100
const percentString = roundedPercent > 0 ? `+${roundedPercent}%` : `${roundedPercent}%`
const message = alignedName + percentString.padStart(7, '.')
if (roundedPercent > PERCENT_THRESHOLD) {
console.log(`${greenColor}${message}${resetColor}`)
} else if (roundedPercent < -PERCENT_THRESHOLD) {
console.log(`${redColor}${message}${resetColor}`)
} else {
console.log(message)
}
}
}
}
(async function () {
const branches = await git.branch()
const currentBranch = branches.branches[branches.current]
let featureBranch = null
let mainBranch = null
if (process.argv[2] === '--ci') {
featureBranch = currentBranch.name
mainBranch = DEFAULT_BRANCH
} else {
featureBranch = await selectBranchName('Select the branch you want to compare (feature branch):', branches.all)
mainBranch = await selectBranchName('Select the branch you want to compare with (main branch):', branches.all)
}
try {
const featureBranchResult = await executeCommandOnBranch(COMMAND, featureBranch)
const mainBranchResult = await executeCommandOnBranch(COMMAND, mainBranch)
compareResults(featureBranchResult, mainBranchResult)
} catch (error) {
console.error('Switch to origin branch due to an error', error.message)
}
await git.checkout(currentBranch.commit)
await git.checkout(currentBranch.name)
console.log(`${greyColor}Back to ${currentBranch.name} ${currentBranch.commit}${resetColor}`)
})()

View File

@@ -0,0 +1,281 @@
'use strict'
const benchmark = require('benchmark')
const suite = new benchmark.Suite()
const STR_LEN = 1e4
const LARGE_ARRAY_SIZE = 2e4
const MULTI_ARRAY_LENGTH = 1e3
const schema = {
title: 'Example Schema',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: ['string', 'null']
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 0
}
}
}
const schemaCJS = {
title: 'Example Schema',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: ['string', 'null']
},
age: {
description: 'Age in years',
type: 'number',
minimum: 0
}
}
}
const schemaAJVJTD = {
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string',
nullable: true
},
age: {
type: 'uint8'
}
}
}
const arraySchema = {
title: 'array schema',
type: 'array',
items: schema
}
const arraySchemaCJS = {
title: 'array schema',
type: 'array',
items: schemaCJS
}
const arraySchemaAJVJTD = {
elements: schemaAJVJTD
}
const dateFormatSchema = {
description: 'Date of birth',
type: 'string',
format: 'date'
}
const dateFormatSchemaCJS = {
description: 'Date of birth',
type: 'string',
format: 'date'
}
const obj = {
firstName: 'Matteo',
lastName: 'Collina',
age: 32
}
const date = new Date()
const multiArray = new Array(MULTI_ARRAY_LENGTH)
const largeArray = new Array(LARGE_ARRAY_SIZE)
const CJS = require('compile-json-stringify')
const CJSStringify = CJS(schemaCJS)
const CJSStringifyArray = CJS(arraySchemaCJS)
const CJSStringifyDate = CJS(dateFormatSchemaCJS)
const CJSStringifyString = CJS({ type: 'string' })
const FJS = require('..')
const stringify = FJS(schema)
const stringifyArrayDefault = FJS(arraySchema)
const stringifyArrayJSONStringify = FJS(arraySchema, {
largeArrayMechanism: 'json-stringify'
})
const stringifyDate = FJS(dateFormatSchema)
const stringifyString = FJS({ type: 'string' })
let str = ''
const Ajv = require('ajv/dist/jtd')
const ajv = new Ajv()
const ajvSerialize = ajv.compileSerializer(schemaAJVJTD)
const ajvSerializeArray = ajv.compileSerializer(arraySchemaAJVJTD)
const ajvSerializeString = ajv.compileSerializer({ type: 'string' })
const getRandomString = (length) => {
if (!Number.isInteger(length)) {
throw new Error('Expected integer length')
}
const validCharacters = 'abcdefghijklmnopqrstuvwxyz'
const nValidCharacters = 26
let result = ''
for (let i = 0; i < length; ++i) {
result += validCharacters[Math.floor(Math.random() * nValidCharacters)]
}
return result[0].toUpperCase() + result.slice(1)
}
// eslint-disable-next-line
for (let i = 0; i < STR_LEN; i++) {
largeArray[i] = {
firstName: getRandomString(8),
lastName: getRandomString(6),
age: Math.ceil(Math.random() * 99)
}
str += i
if (i % 100 === 0) {
str += '"'
}
}
for (let i = STR_LEN; i < LARGE_ARRAY_SIZE; ++i) {
largeArray[i] = {
firstName: getRandomString(10),
lastName: getRandomString(4),
age: Math.ceil(Math.random() * 99)
}
}
Number(str)
for (let i = 0; i < MULTI_ARRAY_LENGTH; i++) {
multiArray[i] = obj
}
suite.add('FJS creation', function () {
FJS(schema)
})
suite.add('CJS creation', function () {
CJS(schemaCJS)
})
suite.add('AJV Serialize creation', function () {
ajv.compileSerializer(schemaAJVJTD)
})
suite.add('JSON.stringify array', function () {
JSON.stringify(multiArray)
})
suite.add('fast-json-stringify array default', function () {
stringifyArrayDefault(multiArray)
})
suite.add('fast-json-stringify array json-stringify', function () {
stringifyArrayJSONStringify(multiArray)
})
suite.add('compile-json-stringify array', function () {
CJSStringifyArray(multiArray)
})
suite.add('AJV Serialize array', function () {
ajvSerializeArray(multiArray)
})
suite.add('JSON.stringify large array', function () {
JSON.stringify(largeArray)
})
suite.add('fast-json-stringify large array default', function () {
stringifyArrayDefault(largeArray)
})
suite.add('fast-json-stringify large array json-stringify', function () {
stringifyArrayJSONStringify(largeArray)
})
suite.add('compile-json-stringify large array', function () {
CJSStringifyArray(largeArray)
})
suite.add('AJV Serialize large array', function () {
ajvSerializeArray(largeArray)
})
suite.add('JSON.stringify long string', function () {
JSON.stringify(str)
})
suite.add('fast-json-stringify long string', function () {
stringifyString(str)
})
suite.add('compile-json-stringify long string', function () {
CJSStringifyString(str)
})
suite.add('AJV Serialize long string', function () {
ajvSerializeString(str)
})
suite.add('JSON.stringify short string', function () {
JSON.stringify('hello world')
})
suite.add('fast-json-stringify short string', function () {
stringifyString('hello world')
})
suite.add('compile-json-stringify short string', function () {
CJSStringifyString('hello world')
})
suite.add('AJV Serialize short string', function () {
ajvSerializeString('hello world')
})
suite.add('JSON.stringify obj', function () {
JSON.stringify(obj)
})
suite.add('fast-json-stringify obj', function () {
stringify(obj)
})
suite.add('compile-json-stringify obj', function () {
CJSStringify(obj)
})
suite.add('AJV Serialize obj', function () {
ajvSerialize(obj)
})
suite.add('JSON stringify date', function () {
JSON.stringify(date)
})
suite.add('fast-json-stringify date format', function () {
stringifyDate(date)
})
suite.add('compile-json-stringify date format', function () {
CJSStringifyDate(date)
})
suite.on('cycle', cycle)
suite.run()
function cycle (e) {
console.log(e.target.toString())
}

View File

@@ -0,0 +1,21 @@
'use strict'
const { workerData: benchmark, parentPort } = require('worker_threads')
const Benchmark = require('benchmark')
Benchmark.options.minSamples = 100
const suite = Benchmark.Suite()
const FJS = require('..')
const stringify = FJS(benchmark.schema)
suite
.add(benchmark.name, () => {
stringify(benchmark.input)
})
.on('cycle', (event) => {
parentPort.postMessage(String(event.target))
})
.on('complete', () => {})
.run()

View File

@@ -0,0 +1,391 @@
'use strict'
const path = require('path')
const { Worker } = require('worker_threads')
const BENCH_THREAD_PATH = path.join(__dirname, 'bench-thread.js')
const LONG_STRING_LENGTH = 1e4
const SHORT_ARRAY_SIZE = 1e3
const shortArrayOfNumbers = new Array(SHORT_ARRAY_SIZE)
const shortArrayOfIntegers = new Array(SHORT_ARRAY_SIZE)
const shortArrayOfShortStrings = new Array(SHORT_ARRAY_SIZE)
const shortArrayOfLongStrings = new Array(SHORT_ARRAY_SIZE)
const shortArrayOfMultiObject = new Array(SHORT_ARRAY_SIZE)
function getRandomInt (max) {
return Math.floor(Math.random() * max)
}
let longSimpleString = ''
for (let i = 0; i < LONG_STRING_LENGTH; i++) {
longSimpleString += i
}
let longString = ''
for (let i = 0; i < LONG_STRING_LENGTH; i++) {
longString += i
if (i % 100 === 0) {
longString += '"'
}
}
for (let i = 0; i < SHORT_ARRAY_SIZE; i++) {
shortArrayOfNumbers[i] = getRandomInt(1000)
shortArrayOfIntegers[i] = getRandomInt(1000)
shortArrayOfShortStrings[i] = 'hello world'
shortArrayOfLongStrings[i] = longString
shortArrayOfMultiObject[i] = { s: 'hello world', n: 42, b: true }
}
const benchmarks = [
{
name: 'short string',
schema: {
type: 'string'
},
input: 'hello world'
},
{
name: 'unsafe short string',
schema: {
type: 'string',
format: 'unsafe'
},
input: 'hello world'
},
{
name: 'short string with double quote',
schema: {
type: 'string'
},
input: 'hello " world'
},
{
name: 'long string without double quotes',
schema: {
type: 'string'
},
input: longSimpleString
},
{
name: 'unsafe long string without double quotes',
schema: {
type: 'string',
format: 'unsafe'
},
input: longSimpleString
},
{
name: 'long string',
schema: {
type: 'string'
},
input: longString
},
{
name: 'unsafe long string',
schema: {
type: 'string',
format: 'unsafe'
},
input: longString
},
{
name: 'number',
schema: {
type: 'number'
},
input: 42
},
{
name: 'integer',
schema: {
type: 'integer'
},
input: 42
},
{
name: 'formatted date-time',
schema: {
type: 'string',
format: 'date-time'
},
input: new Date()
},
{
name: 'formatted date',
schema: {
type: 'string',
format: 'date'
},
input: new Date()
},
{
name: 'formatted time',
schema: {
type: 'string',
format: 'time'
},
input: new Date()
},
{
name: 'short array of numbers',
schema: {
type: 'array',
items: { type: 'number' }
},
input: shortArrayOfNumbers
},
{
name: 'short array of integers',
schema: {
type: 'array',
items: { type: 'integer' }
},
input: shortArrayOfIntegers
},
{
name: 'short array of short strings',
schema: {
type: 'array',
items: { type: 'string' }
},
input: shortArrayOfShortStrings
},
{
name: 'short array of long strings',
schema: {
type: 'array',
items: { type: 'string' }
},
input: shortArrayOfShortStrings
},
{
name: 'short array of objects with properties of different types',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
s: { type: 'string' },
n: { type: 'number' },
b: { type: 'boolean' }
}
}
},
input: shortArrayOfMultiObject
},
{
name: 'object with number property',
schema: {
type: 'object',
properties: {
a: { type: 'number' }
}
},
input: { a: 42 }
},
{
name: 'object with integer property',
schema: {
type: 'object',
properties: {
a: { type: 'integer' }
}
},
input: { a: 42 }
},
{
name: 'object with short string property',
schema: {
type: 'object',
properties: {
a: { type: 'string' }
}
},
input: { a: 'hello world' }
},
{
name: 'object with long string property',
schema: {
type: 'object',
properties: {
a: { type: 'string' }
}
},
input: { a: longString }
},
{
name: 'object with properties of different types',
schema: {
type: 'object',
properties: {
s1: { type: 'string' },
n1: { type: 'number' },
b1: { type: 'boolean' },
s2: { type: 'string' },
n2: { type: 'number' },
b2: { type: 'boolean' },
s3: { type: 'string' },
n3: { type: 'number' },
b3: { type: 'boolean' },
s4: { type: 'string' },
n4: { type: 'number' },
b4: { type: 'boolean' },
s5: { type: 'string' },
n5: { type: 'number' },
b5: { type: 'boolean' }
}
},
input: {
s1: 'hello world',
n1: 42,
b1: true,
s2: 'hello world',
n2: 42,
b2: true,
s3: 'hello world',
n3: 42,
b3: true,
s4: 'hello world',
n4: 42,
b4: true,
s5: 'hello world',
n5: 42,
b5: true
}
},
{
name: 'simple object',
schema: {
title: 'Example Schema',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: ['string', 'null']
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 0
}
}
},
input: { firstName: 'Max', lastName: 'Power', age: 22 }
},
{
name: 'simple object with required fields',
schema: {
title: 'Example Schema',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: ['string', 'null']
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 0
}
},
required: ['firstName', 'lastName', 'age']
},
input: { firstName: 'Max', lastName: 'Power', age: 22 }
},
{
name: 'object with const string property',
schema: {
type: 'object',
properties: {
a: { const: 'const string' }
}
},
input: { a: 'const string' }
},
{
name: 'object with const number property',
schema: {
type: 'object',
properties: {
a: { const: 1 }
}
},
input: { a: 1 }
},
{
name: 'object with const bool property',
schema: {
type: 'object',
properties: {
a: { const: true }
}
},
input: { a: true }
},
{
name: 'object with const object property',
schema: {
type: 'object',
properties: {
foo: { const: { bar: 'baz' } }
}
},
input: {
foo: { bar: 'baz' }
}
},
{
name: 'object with const null property',
schema: {
type: 'object',
properties: {
foo: { const: null }
}
},
input: {
foo: null
}
}
]
async function runBenchmark (benchmark) {
const worker = new Worker(BENCH_THREAD_PATH, { workerData: benchmark })
return new Promise((resolve, reject) => {
let result = null
worker.on('error', reject)
worker.on('message', (benchResult) => {
result = benchResult
})
worker.on('exit', (code) => {
if (code === 0) {
resolve(result)
} else {
reject(new Error(`Worker stopped with exit code ${code}`))
}
})
})
}
async function runBenchmarks () {
let maxNameLength = 0
for (const benchmark of benchmarks) {
maxNameLength = Math.max(benchmark.name.length, maxNameLength)
}
for (const benchmark of benchmarks) {
benchmark.name = benchmark.name.padEnd(maxNameLength, '.')
const resultMessage = await runBenchmark(benchmark)
console.log(resultMessage)
}
}
runBenchmarks()

View File

@@ -0,0 +1,26 @@
'use strict'
const Ajv = require('ajv')
const standaloneCode = require('ajv/dist/standalone').default
const ajvFormats = require('ajv-formats')
const fs = require('fs')
const path = require('path')
const ajv = new Ajv({
addUsedSchema: false,
allowUnionTypes: true,
code: {
source: true,
lines: true,
optimize: 3
}
})
ajvFormats(ajv)
const schema = require('ajv/lib/refs/json-schema-draft-07.json')
const validate = ajv.compile(schema)
const validationCode = standaloneCode(ajv, validate)
const moduleCode = `/* CODE GENERATED BY '${path.basename(__filename)}' DO NOT EDIT! */\n${validationCode}`
fs.writeFileSync(path.join(__dirname, '../lib/schema-validator.js'), moduleCode)

View File

@@ -0,0 +1,81 @@
'use strict'
const fastJson = require('..')
const stringify = fastJson({
title: 'Example Schema',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
age: {
description: 'Age in years',
type: 'integer'
},
now: {
type: 'string'
},
birthdate: {
type: ['string'],
format: 'date-time'
},
reg: {
type: 'string'
},
obj: {
type: 'object',
properties: {
bool: {
type: 'boolean'
}
}
},
arr: {
type: 'array',
items: {
type: 'object',
properties: {
str: {
type: 'string'
}
}
}
}
},
required: ['now'],
patternProperties: {
'.*foo$': {
type: 'string'
},
test: {
type: 'number'
},
date: {
type: 'string',
format: 'date-time'
}
},
additionalProperties: {
type: 'string'
}
})
console.log(stringify({
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
now: new Date(),
reg: /"([^"]|\\")*"/,
foo: 'hello',
numfoo: 42,
test: 42,
strtest: '23',
arr: [{ str: 'stark' }, { str: 'lannister' }],
obj: { bool: true },
notmatch: 'valar morghulis',
notmatchobj: { a: true },
notmatchnum: 42
}))

View File

@@ -0,0 +1,42 @@
'use strict'
const http = require('http')
const stringify = require('fast-json-stringify')({
type: 'object',
properties: {
hello: {
type: 'string'
},
data: {
type: 'number'
},
nested: {
type: 'object',
properties: {
more: {
type: 'string'
}
}
}
}
})
const server = http.createServer(handle)
function handle (req, res) {
const data = {
hello: 'world',
data: 42,
nested: {
more: 'data'
}
}
if (req.url === '/JSON') {
res.end(JSON.stringify(data))
} else {
res.end(stringify(data))
}
}
server.listen(3000)

1032
backend/node_modules/fast-json-stringify/index.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
'use strict'
class Location {
constructor (schema, schemaId, jsonPointer = '#') {
this.schema = schema
this.schemaId = schemaId
this.jsonPointer = jsonPointer
}
getPropertyLocation (propertyName) {
const propertyLocation = new Location(
this.schema[propertyName],
this.schemaId,
this.jsonPointer + '/' + propertyName
)
return propertyLocation
}
getSchemaRef () {
return this.schemaId + this.jsonPointer
}
}
module.exports = Location

View File

@@ -0,0 +1,9 @@
'use strict'
const { mergeSchemas: _mergeSchemas } = require('@fastify/merge-json-schemas')
function mergeSchemas (schemas) {
return _mergeSchemas(schemas, { onConflict: 'skip' })
}
module.exports = mergeSchemas

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,140 @@
'use strict'
// eslint-disable-next-line
const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/
module.exports = class Serializer {
constructor (options) {
switch (options && options.rounding) {
case 'floor':
this.parseInteger = Math.floor
break
case 'ceil':
this.parseInteger = Math.ceil
break
case 'round':
this.parseInteger = Math.round
break
case 'trunc':
default:
this.parseInteger = Math.trunc
break
}
this._options = options
}
asInteger (i) {
if (Number.isInteger(i)) {
return '' + i
} else if (typeof i === 'bigint') {
return i.toString()
}
/* eslint no-undef: "off" */
const integer = this.parseInteger(i)
// check if number is Infinity or NaN
// eslint-disable-next-line no-self-compare
if (integer === Infinity || integer === -Infinity || integer !== integer) {
throw new Error(`The value "${i}" cannot be converted to an integer.`)
}
return '' + integer
}
asNumber (i) {
// fast cast to number
const num = Number(i)
// check if number is NaN
// eslint-disable-next-line no-self-compare
if (num !== num) {
throw new Error(`The value "${i}" cannot be converted to a number.`)
} else if (num === Infinity || num === -Infinity) {
return 'null'
} else {
return '' + num
}
}
asBoolean (bool) {
return bool && 'true' || 'false' // eslint-disable-line
}
asDateTime (date) {
if (date === null) return '""'
if (date instanceof Date) {
return '"' + date.toISOString() + '"'
}
if (typeof date === 'string') {
return '"' + date + '"'
}
throw new Error(`The value "${date}" cannot be converted to a date-time.`)
}
asDate (date) {
if (date === null) return '""'
if (date instanceof Date) {
return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + '"'
}
if (typeof date === 'string') {
return '"' + date + '"'
}
throw new Error(`The value "${date}" cannot be converted to a date.`)
}
asTime (date) {
if (date === null) return '""'
if (date instanceof Date) {
return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + '"'
}
if (typeof date === 'string') {
return '"' + date + '"'
}
throw new Error(`The value "${date}" cannot be converted to a time.`)
}
asString (str) {
const len = str.length
if (len < 42) {
// magically escape strings for json
// relying on their charCodeAt
// everything below 32 needs JSON.stringify()
// every string that contain surrogate needs JSON.stringify()
// 34 and 92 happens all the time, so we
// have a fast case for them
let result = ''
let last = -1
let point = 255
// eslint-disable-next-line
for (var i = 0; i < len; i++) {
point = str.charCodeAt(i)
if (
point === 0x22 || // '"'
point === 0x5c // '\'
) {
last === -1 && (last = 0)
result += str.slice(last, i) + '\\'
last = i
} else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
// The current character is non-printable characters or a surrogate.
return JSON.stringify(str)
}
}
return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
} else if (len < 5000 && STR_ESCAPE.test(str) === false) {
// Only use the regular expression for shorter input. The overhead is otherwise too much.
return '"' + str + '"'
} else {
return JSON.stringify(str)
}
}
asUnsafeString (str) {
return '"' + str + '"'
}
getState () {
return this._options
}
static restoreFromState (state) {
return new Serializer(state)
}
}

View File

@@ -0,0 +1,34 @@
'use strict'
function buildStandaloneCode (contextFunc, context, serializer, validator) {
let ajvDependencyCode = ''
if (context.validatorSchemasIds.size > 0) {
ajvDependencyCode += 'const Validator = require(\'fast-json-stringify/lib/validator\')\n'
ajvDependencyCode += `const validatorState = ${JSON.stringify(validator.getState())}\n`
ajvDependencyCode += 'const validator = Validator.restoreFromState(validatorState)\n'
} else {
ajvDependencyCode += 'const validator = null\n'
}
// Don't need to keep external schemas once compiled
// validatorState will hold external schemas if it needs them
const { schema, ...serializerState } = serializer.getState()
return `
'use strict'
const Serializer = require('fast-json-stringify/lib/serializer')
const serializerState = ${JSON.stringify(serializerState)}
const serializer = Serializer.restoreFromState(serializerState)
${ajvDependencyCode}
module.exports = ${contextFunc.toString()}(validator, serializer)`
}
module.exports = buildStandaloneCode
module.exports.dependencies = {
Serializer: require('./serializer'),
Validator: require('./validator')
}

View File

@@ -0,0 +1,94 @@
'use strict'
const Ajv = require('ajv')
const fastUri = require('fast-uri')
const ajvFormats = require('ajv-formats')
const clone = require('rfdc')({ proto: true })
class Validator {
constructor (ajvOptions) {
this.ajv = new Ajv({
...ajvOptions,
strictSchema: false,
validateSchema: false,
allowUnionTypes: true,
uriResolver: fastUri
})
ajvFormats(this.ajv)
this.ajv.addKeyword({
keyword: 'fjs_type',
type: 'object',
errors: false,
validate: (type, date) => {
return date instanceof Date
}
})
this._ajvSchemas = {}
this._ajvOptions = ajvOptions || {}
}
addSchema (schema, schemaName) {
let schemaKey = schema.$id || schemaName
if (schema.$id !== undefined && schema.$id[0] === '#') {
schemaKey = schemaName + schema.$id // relative URI
}
if (
this.ajv.refs[schemaKey] === undefined &&
this.ajv.schemas[schemaKey] === undefined
) {
const ajvSchema = clone(schema)
this.convertSchemaToAjvFormat(ajvSchema)
this.ajv.addSchema(ajvSchema, schemaKey)
this._ajvSchemas[schemaKey] = schema
}
}
validate (schemaRef, data) {
return this.ajv.validate(schemaRef, data)
}
// Ajv does not support js date format. In order to properly validate objects containing a date,
// it needs to replace all occurrences of the string date format with a custom keyword fjs_type.
// (see https://github.com/fastify/fast-json-stringify/pull/441)
convertSchemaToAjvFormat (schema) {
if (schema === null) return
if (schema.type === 'string') {
schema.fjs_type = 'string'
schema.type = ['string', 'object']
} else if (
Array.isArray(schema.type) &&
schema.type.includes('string') &&
!schema.type.includes('object')
) {
schema.fjs_type = 'string'
schema.type.push('object')
}
for (const property in schema) {
if (typeof schema[property] === 'object') {
this.convertSchemaToAjvFormat(schema[property])
}
}
}
getState () {
return {
ajvOptions: this._ajvOptions,
ajvSchemas: this._ajvSchemas
}
}
static restoreFromState (state) {
const validator = new Validator(state.ajvOptions)
for (const [id, ajvSchema] of Object.entries(state.ajvSchemas)) {
validator.ajv.addSchema(ajvSchema, id)
}
return validator
}
}
module.exports = Validator

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Evgeny Poberezkin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,125 @@
# ajv-formats
JSON Schema formats for Ajv
[![Build Status](https://travis-ci.org/ajv-validator/ajv-formats.svg?branch=master)](https://travis-ci.org/ajv-validator/ajv-formats)
[![npm](https://img.shields.io/npm/v/ajv-formats.svg)](https://www.npmjs.com/package/ajv-formats)
[![Gitter](https://img.shields.io/gitter/room/ajv-validator/ajv.svg)](https://gitter.im/ajv-validator/ajv)
[![GitHub Sponsors](https://img.shields.io/badge/$-sponsors-brightgreen)](https://github.com/sponsors/epoberezkin)
## Usage
```javascript
// ESM/TypeScript import
import Ajv from "ajv"
import addFormats from "ajv-formats"
// Node.js require:
const Ajv = require("ajv")
const addFormats = require("ajv-formats")
const ajv = new Ajv()
addFormats(ajv)
```
## Formats
The package defines these formats:
- _date_: full-date according to [RFC3339](http://tools.ietf.org/html/rfc3339#section-5.6).
- _time_: time (time-zone is mandatory).
- _date-time_: date-time (time-zone is mandatory).
- _iso-time_: time with optional time-zone.
- _iso-date-time_: date-time with optional time-zone.
- _duration_: duration from [RFC3339](https://tools.ietf.org/html/rfc3339#appendix-A)
- _uri_: full URI.
- _uri-reference_: URI reference, including full and relative URIs.
- _uri-template_: URI template according to [RFC6570](https://tools.ietf.org/html/rfc6570)
- _url_ (deprecated): [URL record](https://url.spec.whatwg.org/#concept-url).
- _email_: email address.
- _hostname_: host name according to [RFC1034](http://tools.ietf.org/html/rfc1034#section-3.5).
- _ipv4_: IP address v4.
- _ipv6_: IP address v6.
- _regex_: tests whether a string is a valid regular expression by passing it to RegExp constructor.
- _uuid_: Universally Unique IDentifier according to [RFC4122](http://tools.ietf.org/html/rfc4122).
- _json-pointer_: JSON-pointer according to [RFC6901](https://tools.ietf.org/html/rfc6901).
- _relative-json-pointer_: relative JSON-pointer according to [this draft](http://tools.ietf.org/html/draft-luff-relative-json-pointer-00).
- _byte_: base64 encoded data according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types)
- _int32_: signed 32 bits integer according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types)
- _int64_: signed 64 bits according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types)
- _float_: float according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types)
- _double_: double according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types)
- _password_: password string according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types)
- _binary_: binary string according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types)
See regular expressions used for format validation and the sources that were used in [formats.ts](https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts).
**Please note**: JSON Schema draft-07 also defines formats `iri`, `iri-reference`, `idn-hostname` and `idn-email` for URLs, hostnames and emails with international characters. These formats are available in [ajv-formats-draft2019](https://github.com/luzlab/ajv-formats-draft2019) plugin.
## Keywords to compare values: `formatMaximum` / `formatMinimum` and `formatExclusiveMaximum` / `formatExclusiveMinimum`
These keywords allow to define minimum/maximum constraints when the format keyword defines ordering (`compare` function in format definition).
These keywords are added to ajv instance when ajv-formats is used without options or with option `keywords: true`.
These keywords apply only to strings. If the data is not a string, the validation succeeds.
The value of keywords `formatMaximum`/`formatMinimum` and `formatExclusiveMaximum`/`formatExclusiveMinimum` should be a string or [\$data reference](https://github.com/ajv-validator/ajv/blob/master/docs/validation.md#data-reference). This value is the maximum (minimum) allowed value for the data to be valid as determined by `format` keyword. If `format` keyword is not present schema compilation will throw exception.
When these keyword are added, they also add comparison functions to formats `"date"`, `"time"` and `"date-time"`. User-defined formats also can have comparison functions. See [addFormat](https://github.com/ajv-validator/ajv/blob/master/docs/api.md#api-addformat) method.
```javascript
require("ajv-formats")(ajv)
const schema = {
type: "string",
format: "date",
formatMinimum: "2016-02-06",
formatExclusiveMaximum: "2016-12-27",
}
const validDataList = ["2016-02-06", "2016-12-26"]
const invalidDataList = ["2016-02-05", "2016-12-27", "abc"]
```
## Options
Options can be passed via the second parameter. Options value can be
1. The list of format names that will be added to ajv instance:
```javascript
addFormats(ajv, ["date", "time"])
```
**Please note**: when ajv encounters an undefined format it throws exception (unless ajv instance was configured with `strict: false` option). To allow specific undefined formats they have to be passed to ajv instance via `formats` option with `true` value:
```javascript
const ajv = new Ajv((formats: {date: true, time: true})) // to ignore "date" and "time" formats in schemas.
```
2. Format validation mode (default is `"full"`) with optional list of format names and `keywords` option to add additional format comparison keywords:
```javascript
addFormats(ajv, {mode: "fast"})
```
or
```javascript
addFormats(ajv, {mode: "fast", formats: ["date", "time"], keywords: true})
```
In `"fast"` mode the following formats are simplified: `"date"`, `"time"`, `"date-time"`, `"iso-time"`, `"iso-date-time"`, `"uri"`, `"uri-reference"`, `"email"`. For example, `"date"`, `"time"` and `"date-time"` do not validate ranges in `"fast"` mode, only string structure, and other formats have simplified regular expressions.
## Tests
```bash
npm install
git submodule update --init
npm test
```
## License
[MIT](https://github.com/ajv-validator/ajv-formats/blob/master/LICENSE)

View File

@@ -0,0 +1,9 @@
import type { Format } from "ajv";
export type FormatMode = "fast" | "full";
export type FormatName = "date" | "time" | "date-time" | "iso-time" | "iso-date-time" | "duration" | "uri" | "uri-reference" | "uri-template" | "url" | "email" | "hostname" | "ipv4" | "ipv6" | "regex" | "uuid" | "json-pointer" | "json-pointer-uri-fragment" | "relative-json-pointer" | "byte" | "int32" | "int64" | "float" | "double" | "password" | "binary";
export type DefinedFormats = {
[key in FormatName]: Format;
};
export declare const fullFormats: DefinedFormats;
export declare const fastFormats: DefinedFormats;
export declare const formatNames: FormatName[];

View File

@@ -0,0 +1,208 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatNames = exports.fastFormats = exports.fullFormats = void 0;
function fmtDef(validate, compare) {
return { validate, compare };
}
exports.fullFormats = {
// date: http://tools.ietf.org/html/rfc3339#section-5.6
date: fmtDef(date, compareDate),
// date-time: http://tools.ietf.org/html/rfc3339#section-5.6
time: fmtDef(getTime(true), compareTime),
"date-time": fmtDef(getDateTime(true), compareDateTime),
"iso-time": fmtDef(getTime(), compareIsoTime),
"iso-date-time": fmtDef(getDateTime(), compareIsoDateTime),
// duration: https://tools.ietf.org/html/rfc3339#appendix-A
duration: /^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$/,
uri,
"uri-reference": /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i,
// uri-template: https://tools.ietf.org/html/rfc6570
"uri-template": /^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i,
// For the source: https://gist.github.com/dperini/729294
// For test cases: https://mathiasbynens.be/demo/url-regex
url: /^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu,
email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,
hostname: /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i,
// optimized https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html
ipv4: /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/,
ipv6: /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i,
regex,
// uuid: http://tools.ietf.org/html/rfc4122
uuid: /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i,
// JSON-pointer: https://tools.ietf.org/html/rfc6901
// uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A
"json-pointer": /^(?:\/(?:[^~/]|~0|~1)*)*$/,
"json-pointer-uri-fragment": /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i,
// relative JSON-pointer: http://tools.ietf.org/html/draft-luff-relative-json-pointer-00
"relative-json-pointer": /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/,
// the following formats are used by the openapi specification: https://spec.openapis.org/oas/v3.0.0#data-types
// byte: https://github.com/miguelmota/is-base64
byte,
// signed 32 bit integer
int32: { type: "number", validate: validateInt32 },
// signed 64 bit integer
int64: { type: "number", validate: validateInt64 },
// C-type float
float: { type: "number", validate: validateNumber },
// C-type double
double: { type: "number", validate: validateNumber },
// hint to the UI to hide input strings
password: true,
// unchecked string payload
binary: true,
};
exports.fastFormats = {
...exports.fullFormats,
date: fmtDef(/^\d\d\d\d-[0-1]\d-[0-3]\d$/, compareDate),
time: fmtDef(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, compareTime),
"date-time": fmtDef(/^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, compareDateTime),
"iso-time": fmtDef(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, compareIsoTime),
"iso-date-time": fmtDef(/^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, compareIsoDateTime),
// uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js
uri: /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/i,
"uri-reference": /^(?:(?:[a-z][a-z0-9+\-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i,
// email (sources from jsen validator):
// http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address#answer-8829363
// http://www.w3.org/TR/html5/forms.html#valid-e-mail-address (search for 'wilful violation')
email: /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i,
};
exports.formatNames = Object.keys(exports.fullFormats);
function isLeapYear(year) {
// https://tools.ietf.org/html/rfc3339#appendix-C
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
}
const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
function date(str) {
// full-date from http://tools.ietf.org/html/rfc3339#section-5.6
const matches = DATE.exec(str);
if (!matches)
return false;
const year = +matches[1];
const month = +matches[2];
const day = +matches[3];
return (month >= 1 &&
month <= 12 &&
day >= 1 &&
day <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month]));
}
function compareDate(d1, d2) {
if (!(d1 && d2))
return undefined;
if (d1 > d2)
return 1;
if (d1 < d2)
return -1;
return 0;
}
const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
function getTime(strictTimeZone) {
return function time(str) {
const matches = TIME.exec(str);
if (!matches)
return false;
const hr = +matches[1];
const min = +matches[2];
const sec = +matches[3];
const tz = matches[4];
const tzSign = matches[5] === "-" ? -1 : 1;
const tzH = +(matches[6] || 0);
const tzM = +(matches[7] || 0);
if (tzH > 23 || tzM > 59 || (strictTimeZone && !tz))
return false;
if (hr <= 23 && min <= 59 && sec < 60)
return true;
// leap second
const utcMin = min - tzM * tzSign;
const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0);
return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61;
};
}
function compareTime(s1, s2) {
if (!(s1 && s2))
return undefined;
const t1 = new Date("2020-01-01T" + s1).valueOf();
const t2 = new Date("2020-01-01T" + s2).valueOf();
if (!(t1 && t2))
return undefined;
return t1 - t2;
}
function compareIsoTime(t1, t2) {
if (!(t1 && t2))
return undefined;
const a1 = TIME.exec(t1);
const a2 = TIME.exec(t2);
if (!(a1 && a2))
return undefined;
t1 = a1[1] + a1[2] + a1[3];
t2 = a2[1] + a2[2] + a2[3];
if (t1 > t2)
return 1;
if (t1 < t2)
return -1;
return 0;
}
const DATE_TIME_SEPARATOR = /t|\s/i;
function getDateTime(strictTimeZone) {
const time = getTime(strictTimeZone);
return function date_time(str) {
// http://tools.ietf.org/html/rfc3339#section-5.6
const dateTime = str.split(DATE_TIME_SEPARATOR);
return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1]);
};
}
function compareDateTime(dt1, dt2) {
if (!(dt1 && dt2))
return undefined;
const d1 = new Date(dt1).valueOf();
const d2 = new Date(dt2).valueOf();
if (!(d1 && d2))
return undefined;
return d1 - d2;
}
function compareIsoDateTime(dt1, dt2) {
if (!(dt1 && dt2))
return undefined;
const [d1, t1] = dt1.split(DATE_TIME_SEPARATOR);
const [d2, t2] = dt2.split(DATE_TIME_SEPARATOR);
const res = compareDate(d1, d2);
if (res === undefined)
return undefined;
return res || compareTime(t1, t2);
}
const NOT_URI_FRAGMENT = /\/|:/;
const URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
function uri(str) {
// http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "."
return NOT_URI_FRAGMENT.test(str) && URI.test(str);
}
const BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
function byte(str) {
BYTE.lastIndex = 0;
return BYTE.test(str);
}
const MIN_INT32 = -(2 ** 31);
const MAX_INT32 = 2 ** 31 - 1;
function validateInt32(value) {
return Number.isInteger(value) && value <= MAX_INT32 && value >= MIN_INT32;
}
function validateInt64(value) {
// JSON and javascript max Int is 2**53, so any int that passes isInteger is valid for Int64
return Number.isInteger(value);
}
function validateNumber() {
return true;
}
const Z_ANCHOR = /[^\\]\\Z/;
function regex(str) {
if (Z_ANCHOR.test(str))
return false;
try {
new RegExp(str);
return true;
}
catch (e) {
return false;
}
}
//# sourceMappingURL=formats.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
import { FormatMode, FormatName } from "./formats";
import type { Plugin, Format } from "ajv";
export { FormatMode, FormatName } from "./formats";
export { LimitFormatError } from "./limit";
export interface FormatOptions {
mode?: FormatMode;
formats?: FormatName[];
keywords?: boolean;
}
export type FormatsPluginOptions = FormatName[] | FormatOptions;
export interface FormatsPlugin extends Plugin<FormatsPluginOptions> {
get: (format: FormatName, mode?: FormatMode) => Format;
}
declare const formatsPlugin: FormatsPlugin;
export default formatsPlugin;

View File

@@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const formats_1 = require("./formats");
const limit_1 = require("./limit");
const codegen_1 = require("ajv/dist/compile/codegen");
const fullName = new codegen_1.Name("fullFormats");
const fastName = new codegen_1.Name("fastFormats");
const formatsPlugin = (ajv, opts = { keywords: true }) => {
if (Array.isArray(opts)) {
addFormats(ajv, opts, formats_1.fullFormats, fullName);
return ajv;
}
const [formats, exportName] = opts.mode === "fast" ? [formats_1.fastFormats, fastName] : [formats_1.fullFormats, fullName];
const list = opts.formats || formats_1.formatNames;
addFormats(ajv, list, formats, exportName);
if (opts.keywords)
(0, limit_1.default)(ajv);
return ajv;
};
formatsPlugin.get = (name, mode = "full") => {
const formats = mode === "fast" ? formats_1.fastFormats : formats_1.fullFormats;
const f = formats[name];
if (!f)
throw new Error(`Unknown format "${name}"`);
return f;
};
function addFormats(ajv, list, fs, exportName) {
var _a;
var _b;
(_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : (_b.formats = (0, codegen_1._) `require("ajv-formats/dist/formats").${exportName}`);
for (const f of list)
ajv.addFormat(f, fs[f]);
}
module.exports = exports = formatsPlugin;
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = formatsPlugin;
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAAA,uCAOkB;AAClB,mCAAiC;AAGjC,sDAAgD;AAgBhD,MAAM,QAAQ,GAAG,IAAI,cAAI,CAAC,aAAa,CAAC,CAAA;AACxC,MAAM,QAAQ,GAAG,IAAI,cAAI,CAAC,aAAa,CAAC,CAAA;AAExC,MAAM,aAAa,GAAkB,CACnC,GAAQ,EACR,OAA6B,EAAC,QAAQ,EAAE,IAAI,EAAC,EACxC,EAAE;IACP,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;QACvB,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,qBAAW,EAAE,QAAQ,CAAC,CAAA;QAC5C,OAAO,GAAG,CAAA;KACX;IACD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GACzB,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,qBAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,qBAAW,EAAE,QAAQ,CAAC,CAAA;IAC1E,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,qBAAW,CAAA;IACxC,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,CAAA;IAC1C,IAAI,IAAI,CAAC,QAAQ;QAAE,IAAA,eAAW,EAAC,GAAG,CAAC,CAAA;IACnC,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAED,aAAa,CAAC,GAAG,GAAG,CAAC,IAAgB,EAAE,OAAmB,MAAM,EAAU,EAAE;IAC1E,MAAM,OAAO,GAAG,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,qBAAW,CAAC,CAAC,CAAC,qBAAW,CAAA;IAC3D,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvB,IAAI,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,GAAG,CAAC,CAAA;IACnD,OAAO,CAAC,CAAA;AACV,CAAC,CAAA;AAED,SAAS,UAAU,CAAC,GAAQ,EAAE,IAAkB,EAAE,EAAkB,EAAE,UAAgB;;;IACpF,YAAA,GAAG,CAAC,IAAI,CAAC,IAAI,EAAC,OAAO,uCAAP,OAAO,GAAK,IAAA,WAAC,EAAA,uCAAuC,UAAU,EAAE,EAAA;IAC9E,KAAK,MAAM,CAAC,IAAI,IAAI;QAAE,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;AAC/C,CAAC;AAED,MAAM,CAAC,OAAO,GAAG,OAAO,GAAG,aAAa,CAAA;AACxC,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,EAAC,KAAK,EAAE,IAAI,EAAC,CAAC,CAAA;AAE3D,kBAAe,aAAa,CAAA"}

View File

@@ -0,0 +1,10 @@
import type { Plugin, CodeKeywordDefinition, ErrorObject } from "ajv";
type Kwd = "formatMaximum" | "formatMinimum" | "formatExclusiveMaximum" | "formatExclusiveMinimum";
type Comparison = "<=" | ">=" | "<" | ">";
export type LimitFormatError = ErrorObject<Kwd, {
limit: string;
comparison: Comparison;
}>;
export declare const formatLimitDefinition: CodeKeywordDefinition;
declare const formatLimitPlugin: Plugin<undefined>;
export default formatLimitPlugin;

View File

@@ -0,0 +1,69 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatLimitDefinition = void 0;
const ajv_1 = require("ajv");
const codegen_1 = require("ajv/dist/compile/codegen");
const ops = codegen_1.operators;
const KWDs = {
formatMaximum: { okStr: "<=", ok: ops.LTE, fail: ops.GT },
formatMinimum: { okStr: ">=", ok: ops.GTE, fail: ops.LT },
formatExclusiveMaximum: { okStr: "<", ok: ops.LT, fail: ops.GTE },
formatExclusiveMinimum: { okStr: ">", ok: ops.GT, fail: ops.LTE },
};
const error = {
message: ({ keyword, schemaCode }) => (0, codegen_1.str) `should be ${KWDs[keyword].okStr} ${schemaCode}`,
params: ({ keyword, schemaCode }) => (0, codegen_1._) `{comparison: ${KWDs[keyword].okStr}, limit: ${schemaCode}}`,
};
exports.formatLimitDefinition = {
keyword: Object.keys(KWDs),
type: "string",
schemaType: "string",
$data: true,
error,
code(cxt) {
const { gen, data, schemaCode, keyword, it } = cxt;
const { opts, self } = it;
if (!opts.validateFormats)
return;
const fCxt = new ajv_1.KeywordCxt(it, self.RULES.all.format.definition, "format");
if (fCxt.$data)
validate$DataFormat();
else
validateFormat();
function validate$DataFormat() {
const fmts = gen.scopeValue("formats", {
ref: self.formats,
code: opts.code.formats,
});
const fmt = gen.const("fmt", (0, codegen_1._) `${fmts}[${fCxt.schemaCode}]`);
cxt.fail$data((0, codegen_1.or)((0, codegen_1._) `typeof ${fmt} != "object"`, (0, codegen_1._) `${fmt} instanceof RegExp`, (0, codegen_1._) `typeof ${fmt}.compare != "function"`, compareCode(fmt)));
}
function validateFormat() {
const format = fCxt.schema;
const fmtDef = self.formats[format];
if (!fmtDef || fmtDef === true)
return;
if (typeof fmtDef != "object" ||
fmtDef instanceof RegExp ||
typeof fmtDef.compare != "function") {
throw new Error(`"${keyword}": format "${format}" does not define "compare" function`);
}
const fmt = gen.scopeValue("formats", {
key: format,
ref: fmtDef,
code: opts.code.formats ? (0, codegen_1._) `${opts.code.formats}${(0, codegen_1.getProperty)(format)}` : undefined,
});
cxt.fail$data(compareCode(fmt));
}
function compareCode(fmt) {
return (0, codegen_1._) `${fmt}.compare(${data}, ${schemaCode}) ${KWDs[keyword].fail} 0`;
}
},
dependencies: ["format"],
};
const formatLimitPlugin = (ajv) => {
ajv.addKeyword(exports.formatLimitDefinition);
return ajv;
};
exports.default = formatLimitPlugin;
//# sourceMappingURL=limit.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"limit.js","sourceRoot":"","sources":["../src/limit.ts"],"names":[],"mappings":";;;AAWA,6BAA8B;AAC9B,sDAA2E;AAM3E,MAAM,GAAG,GAAG,mBAAS,CAAA;AAErB,MAAM,IAAI,GAA4D;IACpE,aAAa,EAAE,EAAC,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,EAAC;IACvD,aAAa,EAAE,EAAC,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,EAAC;IACvD,sBAAsB,EAAE,EAAC,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,EAAC;IAC/D,sBAAsB,EAAE,EAAC,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,EAAC;CAChE,CAAA;AAID,MAAM,KAAK,GAA2B;IACpC,OAAO,EAAE,CAAC,EAAC,OAAO,EAAE,UAAU,EAAC,EAAE,EAAE,CAAC,IAAA,aAAG,EAAA,aAAa,IAAI,CAAC,OAAc,CAAC,CAAC,KAAK,IAAI,UAAU,EAAE;IAC9F,MAAM,EAAE,CAAC,EAAC,OAAO,EAAE,UAAU,EAAC,EAAE,EAAE,CAChC,IAAA,WAAC,EAAA,gBAAgB,IAAI,CAAC,OAAc,CAAC,CAAC,KAAK,YAAY,UAAU,GAAG;CACvE,CAAA;AAEY,QAAA,qBAAqB,GAA0B;IAC1D,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;IAC1B,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE,QAAQ;IACpB,KAAK,EAAE,IAAI;IACX,KAAK;IACL,IAAI,CAAC,GAAG;QACN,MAAM,EAAC,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,EAAC,GAAG,GAAG,CAAA;QAChD,MAAM,EAAC,IAAI,EAAE,IAAI,EAAC,GAAG,EAAE,CAAA;QACvB,IAAI,CAAC,IAAI,CAAC,eAAe;YAAE,OAAM;QAEjC,MAAM,IAAI,GAAG,IAAI,gBAAU,CAAC,EAAE,EAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAe,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;QACrF,IAAI,IAAI,CAAC,KAAK;YAAE,mBAAmB,EAAE,CAAA;;YAChC,cAAc,EAAE,CAAA;QAErB,SAAS,mBAAmB;YAC1B,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,SAAS,EAAE;gBACrC,GAAG,EAAE,IAAI,CAAC,OAAO;gBACjB,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO;aACxB,CAAC,CAAA;YACF,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,IAAA,WAAC,EAAA,GAAG,IAAI,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC,CAAA;YAC5D,GAAG,CAAC,SAAS,CACX,IAAA,YAAE,EACA,IAAA,WAAC,EAAA,UAAU,GAAG,cAAc,EAC5B,IAAA,WAAC,EAAA,GAAG,GAAG,oBAAoB,EAC3B,IAAA,WAAC,EAAA,UAAU,GAAG,wBAAwB,EACtC,WAAW,CAAC,GAAG,CAAC,CACjB,CACF,CAAA;QACH,CAAC;QAED,SAAS,cAAc;YACrB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAgB,CAAA;YACpC,MAAM,MAAM,GAA4B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;YAC5D,IAAI,CAAC,MAAM,IAAI,MAAM,KAAK,IAAI;gBAAE,OAAM;YACtC,IACE,OAAO,MAAM,IAAI,QAAQ;gBACzB,MAAM,YAAY,MAAM;gBACxB,OAAO,MAAM,CAAC,OAAO,IAAI,UAAU,EACnC;gBACA,MAAM,IAAI,KAAK,CAAC,IAAI,OAAO,cAAc,MAAM,sCAAsC,CAAC,CAAA;aACvF;YACD,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,CAAC,SAAS,EAAE;gBACpC,GAAG,EAAE,MAAM;gBACX,GAAG,EAAE,MAAM;gBACX,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAA,WAAC,EAAA,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAA,qBAAW,EAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS;aACpF,CAAC,CAAA;YAEF,GAAG,CAAC,SAAS,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAA;QACjC,CAAC;QAED,SAAS,WAAW,CAAC,GAAS;YAC5B,OAAO,IAAA,WAAC,EAAA,GAAG,GAAG,YAAY,IAAI,KAAK,UAAU,KAAK,IAAI,CAAC,OAAc,CAAC,CAAC,IAAI,IAAI,CAAA;QACjF,CAAC;IACH,CAAC;IACD,YAAY,EAAE,CAAC,QAAQ,CAAC;CACzB,CAAA;AAED,MAAM,iBAAiB,GAAsB,CAAC,GAAQ,EAAO,EAAE;IAC7D,GAAG,CAAC,UAAU,CAAC,6BAAqB,CAAC,CAAA;IACrC,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAED,kBAAe,iBAAiB,CAAA"}

View File

@@ -0,0 +1,74 @@
{
"name": "ajv-formats",
"version": "3.0.1",
"description": "Format validation for Ajv v7+",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"src/",
"dist/"
],
"scripts": {
"build": "tsc",
"prettier:write": "prettier --write \"./**/*.{md,json,yaml,js,ts}\"",
"prettier:check": "prettier --list-different \"./**/*.{md,json,yaml,js,ts}\"",
"eslint": "eslint --ext .ts ./src/**/*",
"test-spec": "jest",
"test-cov": "jest --coverage",
"test": "npm run prettier:check && npm run build && npm run eslint && npm run test-cov",
"ci-test": "npm run test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ajv-validator/ajv-formats.git"
},
"keywords": [
"Ajv",
"JSON-Schema",
"format",
"validation"
],
"author": "Evgeny Poberezkin",
"license": "MIT",
"bugs": {
"url": "https://github.com/ajv-validator/ajv-formats/issues"
},
"homepage": "https://github.com/ajv-validator/ajv-formats#readme",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
},
"devDependencies": {
"@ajv-validator/config": "^0.3.0",
"@types/jest": "^26.0.5",
"@types/node": "^14.10.1",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"ajv": "^8.0.0",
"eslint": "^7.5.0",
"eslint-config-prettier": "^6.11.0",
"husky": "^4.2.5",
"jest": "^26.1.0",
"json-schema-test": "^2.0.0",
"lint-staged": "^10.2.11",
"prettier": "^2.3.2",
"ts-jest": "^26.1.3",
"typescript": "^4.0.0"
},
"prettier": "@ajv-validator/config/prettierrc.json",
"husky": {
"hooks": {
"pre-commit": "lint-staged && npm test"
}
},
"lint-staged": {
"*.{md,json,yaml,js,ts}": "prettier --write"
}
}

View File

@@ -0,0 +1,269 @@
import type {Format, FormatDefinition} from "ajv"
import type {FormatValidator, FormatCompare} from "ajv/dist/types"
export type FormatMode = "fast" | "full"
export type FormatName =
| "date"
| "time"
| "date-time"
| "iso-time"
| "iso-date-time"
| "duration"
| "uri"
| "uri-reference"
| "uri-template"
| "url"
| "email"
| "hostname"
| "ipv4"
| "ipv6"
| "regex"
| "uuid"
| "json-pointer"
| "json-pointer-uri-fragment"
| "relative-json-pointer"
| "byte"
| "int32"
| "int64"
| "float"
| "double"
| "password"
| "binary"
export type DefinedFormats = {
[key in FormatName]: Format
}
function fmtDef(
validate: RegExp | FormatValidator<string>,
compare: FormatCompare<string>
): FormatDefinition<string> {
return {validate, compare}
}
export const fullFormats: DefinedFormats = {
// date: http://tools.ietf.org/html/rfc3339#section-5.6
date: fmtDef(date, compareDate),
// date-time: http://tools.ietf.org/html/rfc3339#section-5.6
time: fmtDef(getTime(true), compareTime),
"date-time": fmtDef(getDateTime(true), compareDateTime),
"iso-time": fmtDef(getTime(), compareIsoTime),
"iso-date-time": fmtDef(getDateTime(), compareIsoDateTime),
// duration: https://tools.ietf.org/html/rfc3339#appendix-A
duration: /^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$/,
uri,
"uri-reference":
/^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i,
// uri-template: https://tools.ietf.org/html/rfc6570
"uri-template":
/^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i,
// For the source: https://gist.github.com/dperini/729294
// For test cases: https://mathiasbynens.be/demo/url-regex
url: /^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu,
email:
/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,
hostname:
/^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i,
// optimized https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html
ipv4: /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/,
ipv6: /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i,
regex,
// uuid: http://tools.ietf.org/html/rfc4122
uuid: /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i,
// JSON-pointer: https://tools.ietf.org/html/rfc6901
// uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A
"json-pointer": /^(?:\/(?:[^~/]|~0|~1)*)*$/,
"json-pointer-uri-fragment": /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i,
// relative JSON-pointer: http://tools.ietf.org/html/draft-luff-relative-json-pointer-00
"relative-json-pointer": /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/,
// the following formats are used by the openapi specification: https://spec.openapis.org/oas/v3.0.0#data-types
// byte: https://github.com/miguelmota/is-base64
byte,
// signed 32 bit integer
int32: {type: "number", validate: validateInt32},
// signed 64 bit integer
int64: {type: "number", validate: validateInt64},
// C-type float
float: {type: "number", validate: validateNumber},
// C-type double
double: {type: "number", validate: validateNumber},
// hint to the UI to hide input strings
password: true,
// unchecked string payload
binary: true,
}
export const fastFormats: DefinedFormats = {
...fullFormats,
date: fmtDef(/^\d\d\d\d-[0-1]\d-[0-3]\d$/, compareDate),
time: fmtDef(
/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i,
compareTime
),
"date-time": fmtDef(
/^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i,
compareDateTime
),
"iso-time": fmtDef(
/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i,
compareIsoTime
),
"iso-date-time": fmtDef(
/^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i,
compareIsoDateTime
),
// uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js
uri: /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/i,
"uri-reference": /^(?:(?:[a-z][a-z0-9+\-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i,
// email (sources from jsen validator):
// http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address#answer-8829363
// http://www.w3.org/TR/html5/forms.html#valid-e-mail-address (search for 'wilful violation')
email:
/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i,
}
export const formatNames = Object.keys(fullFormats) as FormatName[]
function isLeapYear(year: number): boolean {
// https://tools.ietf.org/html/rfc3339#appendix-C
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
}
const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
function date(str: string): boolean {
// full-date from http://tools.ietf.org/html/rfc3339#section-5.6
const matches: string[] | null = DATE.exec(str)
if (!matches) return false
const year: number = +matches[1]
const month: number = +matches[2]
const day: number = +matches[3]
return (
month >= 1 &&
month <= 12 &&
day >= 1 &&
day <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month])
)
}
function compareDate(d1: string, d2: string): number | undefined {
if (!(d1 && d2)) return undefined
if (d1 > d2) return 1
if (d1 < d2) return -1
return 0
}
const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i
function getTime(strictTimeZone?: boolean): (str: string) => boolean {
return function time(str: string): boolean {
const matches: string[] | null = TIME.exec(str)
if (!matches) return false
const hr: number = +matches[1]
const min: number = +matches[2]
const sec: number = +matches[3]
const tz: string | undefined = matches[4]
const tzSign: number = matches[5] === "-" ? -1 : 1
const tzH: number = +(matches[6] || 0)
const tzM: number = +(matches[7] || 0)
if (tzH > 23 || tzM > 59 || (strictTimeZone && !tz)) return false
if (hr <= 23 && min <= 59 && sec < 60) return true
// leap second
const utcMin = min - tzM * tzSign
const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0)
return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61
}
}
function compareTime(s1: string, s2: string): number | undefined {
if (!(s1 && s2)) return undefined
const t1 = new Date("2020-01-01T" + s1).valueOf()
const t2 = new Date("2020-01-01T" + s2).valueOf()
if (!(t1 && t2)) return undefined
return t1 - t2
}
function compareIsoTime(t1: string, t2: string): number | undefined {
if (!(t1 && t2)) return undefined
const a1 = TIME.exec(t1)
const a2 = TIME.exec(t2)
if (!(a1 && a2)) return undefined
t1 = a1[1] + a1[2] + a1[3]
t2 = a2[1] + a2[2] + a2[3]
if (t1 > t2) return 1
if (t1 < t2) return -1
return 0
}
const DATE_TIME_SEPARATOR = /t|\s/i
function getDateTime(strictTimeZone?: boolean): (str: string) => boolean {
const time = getTime(strictTimeZone)
return function date_time(str: string): boolean {
// http://tools.ietf.org/html/rfc3339#section-5.6
const dateTime: string[] = str.split(DATE_TIME_SEPARATOR)
return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1])
}
}
function compareDateTime(dt1: string, dt2: string): number | undefined {
if (!(dt1 && dt2)) return undefined
const d1 = new Date(dt1).valueOf()
const d2 = new Date(dt2).valueOf()
if (!(d1 && d2)) return undefined
return d1 - d2
}
function compareIsoDateTime(dt1: string, dt2: string): number | undefined {
if (!(dt1 && dt2)) return undefined
const [d1, t1] = dt1.split(DATE_TIME_SEPARATOR)
const [d2, t2] = dt2.split(DATE_TIME_SEPARATOR)
const res = compareDate(d1, d2)
if (res === undefined) return undefined
return res || compareTime(t1, t2)
}
const NOT_URI_FRAGMENT = /\/|:/
const URI =
/^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i
function uri(str: string): boolean {
// http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "."
return NOT_URI_FRAGMENT.test(str) && URI.test(str)
}
const BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm
function byte(str: string): boolean {
BYTE.lastIndex = 0
return BYTE.test(str)
}
const MIN_INT32 = -(2 ** 31)
const MAX_INT32 = 2 ** 31 - 1
function validateInt32(value: number): boolean {
return Number.isInteger(value) && value <= MAX_INT32 && value >= MIN_INT32
}
function validateInt64(value: number): boolean {
// JSON and javascript max Int is 2**53, so any int that passes isInteger is valid for Int64
return Number.isInteger(value)
}
function validateNumber(): boolean {
return true
}
const Z_ANCHOR = /[^\\]\\Z/
function regex(str: string): boolean {
if (Z_ANCHOR.test(str)) return false
try {
new RegExp(str)
return true
} catch (e) {
return false
}
}

View File

@@ -0,0 +1,62 @@
import {
DefinedFormats,
FormatMode,
FormatName,
formatNames,
fastFormats,
fullFormats,
} from "./formats"
import formatLimit from "./limit"
import type Ajv from "ajv"
import type {Plugin, Format} from "ajv"
import {_, Name} from "ajv/dist/compile/codegen"
export {FormatMode, FormatName} from "./formats"
export {LimitFormatError} from "./limit"
export interface FormatOptions {
mode?: FormatMode
formats?: FormatName[]
keywords?: boolean
}
export type FormatsPluginOptions = FormatName[] | FormatOptions
export interface FormatsPlugin extends Plugin<FormatsPluginOptions> {
get: (format: FormatName, mode?: FormatMode) => Format
}
const fullName = new Name("fullFormats")
const fastName = new Name("fastFormats")
const formatsPlugin: FormatsPlugin = (
ajv: Ajv,
opts: FormatsPluginOptions = {keywords: true}
): Ajv => {
if (Array.isArray(opts)) {
addFormats(ajv, opts, fullFormats, fullName)
return ajv
}
const [formats, exportName] =
opts.mode === "fast" ? [fastFormats, fastName] : [fullFormats, fullName]
const list = opts.formats || formatNames
addFormats(ajv, list, formats, exportName)
if (opts.keywords) formatLimit(ajv)
return ajv
}
formatsPlugin.get = (name: FormatName, mode: FormatMode = "full"): Format => {
const formats = mode === "fast" ? fastFormats : fullFormats
const f = formats[name]
if (!f) throw new Error(`Unknown format "${name}"`)
return f
}
function addFormats(ajv: Ajv, list: FormatName[], fs: DefinedFormats, exportName: Name): void {
ajv.opts.code.formats ??= _`require("ajv-formats/dist/formats").${exportName}`
for (const f of list) ajv.addFormat(f, fs[f])
}
module.exports = exports = formatsPlugin
Object.defineProperty(exports, "__esModule", {value: true})
export default formatsPlugin

View File

@@ -0,0 +1,99 @@
import type Ajv from "ajv"
import type {
Plugin,
CodeKeywordDefinition,
KeywordErrorDefinition,
Code,
Name,
ErrorObject,
} from "ajv"
import type {AddedFormat} from "ajv/dist/types"
import type {Rule} from "ajv/dist/compile/rules"
import {KeywordCxt} from "ajv"
import {_, str, or, getProperty, operators} from "ajv/dist/compile/codegen"
type Kwd = "formatMaximum" | "formatMinimum" | "formatExclusiveMaximum" | "formatExclusiveMinimum"
type Comparison = "<=" | ">=" | "<" | ">"
const ops = operators
const KWDs: {[K in Kwd]: {okStr: Comparison; ok: Code; fail: Code}} = {
formatMaximum: {okStr: "<=", ok: ops.LTE, fail: ops.GT},
formatMinimum: {okStr: ">=", ok: ops.GTE, fail: ops.LT},
formatExclusiveMaximum: {okStr: "<", ok: ops.LT, fail: ops.GTE},
formatExclusiveMinimum: {okStr: ">", ok: ops.GT, fail: ops.LTE},
}
export type LimitFormatError = ErrorObject<Kwd, {limit: string; comparison: Comparison}>
const error: KeywordErrorDefinition = {
message: ({keyword, schemaCode}) => str`should be ${KWDs[keyword as Kwd].okStr} ${schemaCode}`,
params: ({keyword, schemaCode}) =>
_`{comparison: ${KWDs[keyword as Kwd].okStr}, limit: ${schemaCode}}`,
}
export const formatLimitDefinition: CodeKeywordDefinition = {
keyword: Object.keys(KWDs),
type: "string",
schemaType: "string",
$data: true,
error,
code(cxt) {
const {gen, data, schemaCode, keyword, it} = cxt
const {opts, self} = it
if (!opts.validateFormats) return
const fCxt = new KeywordCxt(it, (self.RULES.all.format as Rule).definition, "format")
if (fCxt.$data) validate$DataFormat()
else validateFormat()
function validate$DataFormat(): void {
const fmts = gen.scopeValue("formats", {
ref: self.formats,
code: opts.code.formats,
})
const fmt = gen.const("fmt", _`${fmts}[${fCxt.schemaCode}]`)
cxt.fail$data(
or(
_`typeof ${fmt} != "object"`,
_`${fmt} instanceof RegExp`,
_`typeof ${fmt}.compare != "function"`,
compareCode(fmt)
)
)
}
function validateFormat(): void {
const format = fCxt.schema as string
const fmtDef: AddedFormat | undefined = self.formats[format]
if (!fmtDef || fmtDef === true) return
if (
typeof fmtDef != "object" ||
fmtDef instanceof RegExp ||
typeof fmtDef.compare != "function"
) {
throw new Error(`"${keyword}": format "${format}" does not define "compare" function`)
}
const fmt = gen.scopeValue("formats", {
key: format,
ref: fmtDef,
code: opts.code.formats ? _`${opts.code.formats}${getProperty(format)}` : undefined,
})
cxt.fail$data(compareCode(fmt))
}
function compareCode(fmt: Name): Code {
return _`${fmt}.compare(${data}, ${schemaCode}) ${KWDs[keyword as Kwd].fail} 0`
}
},
dependencies: ["format"],
}
const formatLimitPlugin: Plugin<undefined> = (ajv: Ajv): Ajv => {
ajv.addKeyword(formatLimitDefinition)
return ajv
}
export default formatLimitPlugin

68
backend/node_modules/fast-json-stringify/package.json generated vendored Normal file
View File

@@ -0,0 +1,68 @@
{
"name": "fast-json-stringify",
"version": "5.16.1",
"description": "Stringify your JSON at max speed",
"main": "index.js",
"type": "commonjs",
"types": "types/index.d.ts",
"scripts": {
"bench": "node ./benchmark/bench.js",
"bench:cmp": "node ./benchmark/bench-cmp-branch.js",
"bench:cmp:ci": "node ./benchmark/bench-cmp-branch.js --ci",
"benchmark": "node ./benchmark/bench-cmp-lib.js",
"lint": "standard",
"lint:fix": "standard --fix",
"test:typescript": "tsd",
"test:unit": "tap",
"test": "npm run test:unit && npm run test:typescript"
},
"precommit": [
"lint",
"test"
],
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/fast-json-stringify.git"
},
"keywords": [
"json",
"stringify",
"schema",
"fast"
],
"author": "Matteo Collina <hello@matteocollina.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/fast-json-stringify/issues"
},
"homepage": "https://github.com/fastify/fast-json-stringify#readme",
"devDependencies": {
"@fastify/pre-commit": "^2.0.2",
"@sinclair/typebox": "^0.32.3",
"benchmark": "^2.1.4",
"cli-select": "^1.1.2",
"compile-json-stringify": "^0.1.2",
"is-my-json-valid": "^2.20.0",
"simple-git": "^3.7.1",
"standard": "^17.0.0",
"tap": "^16.0.1",
"tsd": "^0.31.0",
"webpack": "^5.40.0",
"fast-json-stringify": "."
},
"dependencies": {
"ajv": "^8.10.0",
"ajv-formats": "^3.0.1",
"fast-deep-equal": "^3.1.3",
"fast-uri": "^2.1.0",
"rfdc": "^1.2.0",
"json-schema-ref-resolver": "^1.0.1",
"@fastify/merge-json-schemas": "^0.1.0"
},
"standard": {
"ignore": [
"schema-validator.js"
]
},
"runkitExampleFilename": "./examples/example.js"
}

View File

@@ -0,0 +1,332 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('additionalProperties', (t) => {
t.plan(1)
const stringify = build({
title: 'additionalProperties',
type: 'object',
properties: {
str: {
type: 'string'
}
},
additionalProperties: {
type: 'string'
}
})
const obj = { str: 'test', foo: 42, ofoo: true, foof: 'string', objfoo: { a: true } }
t.equal(stringify(obj), '{"str":"test","foo":"42","ofoo":"true","foof":"string","objfoo":"[object Object]"}')
})
test('additionalProperties should not change properties', (t) => {
t.plan(1)
const stringify = build({
title: 'patternProperties should not change properties',
type: 'object',
properties: {
foo: {
type: 'string'
}
},
additionalProperties: {
type: 'number'
}
})
const obj = { foo: '42', ofoo: 42 }
t.equal(stringify(obj), '{"foo":"42","ofoo":42}')
})
test('additionalProperties should not change properties and patternProperties', (t) => {
t.plan(1)
const stringify = build({
title: 'patternProperties should not change properties',
type: 'object',
properties: {
foo: {
type: 'string'
}
},
patternProperties: {
foo: {
type: 'string'
}
},
additionalProperties: {
type: 'number'
}
})
const obj = { foo: '42', ofoo: 42, test: '42' }
t.equal(stringify(obj), '{"foo":"42","ofoo":"42","test":42}')
})
test('additionalProperties set to true, use of fast-safe-stringify', (t) => {
t.plan(1)
const stringify = build({
title: 'check string coerce',
type: 'object',
properties: {},
additionalProperties: true
})
const obj = { foo: true, ofoo: 42, arrfoo: ['array', 'test'], objfoo: { a: 'world' } }
t.equal(stringify(obj), '{"foo":true,"ofoo":42,"arrfoo":["array","test"],"objfoo":{"a":"world"}}')
})
test('additionalProperties - string coerce', (t) => {
t.plan(1)
const stringify = build({
title: 'check string coerce',
type: 'object',
properties: {},
additionalProperties: {
type: 'string'
}
})
const obj = { foo: true, ofoo: 42, arrfoo: ['array', 'test'], objfoo: { a: 'world' } }
t.equal(stringify(obj), '{"foo":"true","ofoo":"42","arrfoo":"array,test","objfoo":"[object Object]"}')
})
test('additionalProperties - number skip', (t) => {
t.plan(1)
const stringify = build({
title: 'check number coerce',
type: 'object',
properties: {},
additionalProperties: {
type: 'number'
}
})
// const obj = { foo: true, ofoo: '42', xfoo: 'string', arrfoo: [1, 2], objfoo: { num: 42 } }
const obj = { foo: true, ofoo: '42' }
t.equal(stringify(obj), '{"foo":1,"ofoo":42}')
})
test('additionalProperties - boolean coerce', (t) => {
t.plan(1)
const stringify = build({
title: 'check boolean coerce',
type: 'object',
properties: {},
additionalProperties: {
type: 'boolean'
}
})
const obj = { foo: 'true', ofoo: 0, arrfoo: [1, 2], objfoo: { a: true } }
t.equal(stringify(obj), '{"foo":true,"ofoo":false,"arrfoo":true,"objfoo":true}')
})
test('additionalProperties - object coerce', (t) => {
t.plan(1)
const stringify = build({
title: 'check object coerce',
type: 'object',
properties: {},
additionalProperties: {
type: 'object',
properties: {
answer: {
type: 'number'
}
}
}
})
const obj = { objfoo: { answer: 42 } }
t.equal(stringify(obj), '{"objfoo":{"answer":42}}')
})
test('additionalProperties - array coerce', (t) => {
t.plan(2)
const stringify = build({
title: 'check array coerce',
type: 'object',
properties: {},
additionalProperties: {
type: 'array',
items: {
type: 'string'
}
}
})
const coercibleValues = { arrfoo: [1, 2] }
t.equal(stringify(coercibleValues), '{"arrfoo":["1","2"]}')
const incoercibleValues = { foo: 'true', ofoo: 0, objfoo: { tyrion: 'lannister' } }
t.throws(() => stringify(incoercibleValues))
})
test('additionalProperties with empty schema', (t) => {
t.plan(1)
const stringify = build({
type: 'object',
additionalProperties: {}
})
const obj = { a: 1, b: true, c: null }
t.equal(stringify(obj), '{"a":1,"b":true,"c":null}')
})
test('additionalProperties with nested empty schema', (t) => {
t.plan(1)
const stringify = build({
type: 'object',
properties: {
data: { type: 'object', additionalProperties: {} }
},
required: ['data']
})
const obj = { data: { a: 1, b: true, c: null } }
t.equal(stringify(obj), '{"data":{"a":1,"b":true,"c":null}}')
})
test('nested additionalProperties', (t) => {
t.plan(1)
const stringify = build({
title: 'additionalProperties',
type: 'array',
items: {
type: 'object',
properties: {
ap: {
type: 'object',
additionalProperties: { type: 'string' }
}
}
}
})
const obj = [{ ap: { value: 'string' } }]
t.equal(stringify(obj), '[{"ap":{"value":"string"}}]')
})
test('very nested additionalProperties', (t) => {
t.plan(1)
const stringify = build({
title: 'additionalProperties',
type: 'array',
items: {
type: 'object',
properties: {
ap: {
type: 'object',
properties: {
nested: {
type: 'object',
properties: {
moarNested: {
type: 'object',
properties: {
finally: {
type: 'object',
additionalProperties: {
type: 'string'
}
}
}
}
}
}
}
}
}
}
})
const obj = [{ ap: { nested: { moarNested: { finally: { value: 'str' } } } } }]
t.equal(stringify(obj), '[{"ap":{"nested":{"moarNested":{"finally":{"value":"str"}}}}}]')
})
test('nested additionalProperties set to true', (t) => {
t.plan(1)
const stringify = build({
title: 'nested additionalProperties=true',
type: 'object',
properties: {
ap: {
type: 'object',
additionalProperties: true
}
}
})
const obj = { ap: { value: 'string', someNumber: 42 } }
t.equal(stringify(obj), '{"ap":{"value":"string","someNumber":42}}')
})
test('field passed to fastSafeStringify as undefined should be removed', (t) => {
t.plan(1)
const stringify = build({
title: 'nested additionalProperties=true',
type: 'object',
properties: {
ap: {
type: 'object',
additionalProperties: true
}
}
})
const obj = { ap: { value: 'string', someNumber: undefined } }
t.equal(stringify(obj), '{"ap":{"value":"string"}}')
})
test('property without type but with enum, will acts as additionalProperties', (t) => {
t.plan(1)
const stringify = build({
title: 'automatic additionalProperties',
type: 'object',
properties: {
ap: {
enum: ['foobar', 42, ['foo', 'bar'], {}]
}
}
})
const obj = { ap: { additional: 'field' } }
t.equal(stringify(obj), '{"ap":{"additional":"field"}}')
})
test('property without type but with enum, will acts as additionalProperties without overwriting', (t) => {
t.plan(1)
const stringify = build({
title: 'automatic additionalProperties',
type: 'object',
properties: {
ap: {
additionalProperties: false,
enum: ['foobar', 42, ['foo', 'bar'], {}]
}
}
})
const obj = { ap: { additional: 'field' } }
t.equal(stringify(obj), '{"ap":{}}')
})
test('function and symbol references are not serialized as undefined', (t) => {
t.plan(1)
const stringify = build({
title: 'additionalProperties',
type: 'object',
additionalProperties: true,
properties: {
str: {
type: 'string'
}
}
})
const obj = { str: 'x', test: 'test', meth: () => 'x', sym: Symbol('x') }
t.equal(stringify(obj), '{"str":"x","test":"test"}')
})

View File

@@ -0,0 +1,755 @@
'use strict'
const test = require('tap').test
const build = require('..')
process.env.TZ = 'UTC'
test('allOf: combine type and format ', (t) => {
t.plan(1)
const schema = {
allOf: [
{ type: 'string' },
{ format: 'time' }
]
}
const stringify = build(schema)
const date = new Date(1674263005800)
const value = stringify(date)
t.equal(value, '"01:03:25"')
})
test('allOf: combine additional properties ', (t) => {
t.plan(1)
const schema = {
allOf: [
{ type: 'object' },
{
type: 'object',
additionalProperties: { type: 'boolean' }
}
]
}
const stringify = build(schema)
const data = { property: true }
const value = stringify(data)
t.equal(value, JSON.stringify(data))
})
test('allOf: combine pattern properties', (t) => {
t.plan(1)
const schema = {
allOf: [
{ type: 'object' },
{
type: 'object',
patternProperties: {
foo: {
type: 'number'
}
}
}
]
}
const stringify = build(schema)
const data = { foo: 42 }
const value = stringify(data)
t.equal(value, JSON.stringify(data))
})
test('object with allOf and multiple schema on the allOf', (t) => {
t.plan(4)
const schema = {
title: 'object with allOf and multiple schema on the allOf',
type: 'object',
allOf: [
{
type: 'object',
required: [
'name'
],
properties: {
name: {
type: 'string'
},
tag: {
type: 'string'
}
}
},
{
required: [
'id'
],
type: 'object',
properties: {
id: {
type: 'integer'
}
}
}
]
}
const stringify = build(schema)
try {
stringify({
id: 1
})
} catch (e) {
t.equal(e.message, '"name" is required!')
}
try {
stringify({
name: 'string'
})
} catch (e) {
t.equal(e.message, '"id" is required!')
}
t.equal(stringify({
id: 1,
name: 'string'
}), '{"name":"string","id":1}')
t.equal(stringify({
id: 1,
name: 'string',
tag: 'otherString'
}), '{"name":"string","id":1,"tag":"otherString"}')
})
test('object with allOf and one schema on the allOf', (t) => {
t.plan(1)
const schema = {
title: 'object with allOf and one schema on the allOf',
type: 'object',
allOf: [
{
required: [
'id'
],
type: 'object',
properties: {
id: {
type: 'integer'
}
}
}
]
}
const stringify = build(schema)
const value = stringify({
id: 1
})
t.equal(value, '{"id":1}')
})
test('object with allOf and no schema on the allOf', (t) => {
t.plan(1)
const schema = {
title: 'object with allOf and no schema on the allOf',
type: 'object',
allOf: []
}
try {
build(schema)
t.fail()
} catch (e) {
t.equal(e.message, 'schema is invalid: data/allOf must NOT have fewer than 1 items')
}
})
test('object with nested allOfs', (t) => {
t.plan(1)
const schema = {
title: 'object with nested allOfs',
type: 'object',
allOf: [
{
required: [
'id1'
],
type: 'object',
properties: {
id1: {
type: 'integer'
}
}
},
{
allOf: [
{
type: 'object',
properties: {
id2: {
type: 'integer'
}
}
},
{
type: 'object',
properties: {
id3: {
type: 'integer'
}
}
}
]
}
]
}
const stringify = build(schema)
const value = stringify({
id1: 1,
id2: 2,
id3: 3,
id4: 4 // extra prop shouldn't be in result
})
t.equal(value, '{"id1":1,"id2":2,"id3":3}')
})
test('object with anyOf nested inside allOf', (t) => {
t.plan(1)
const schema = {
title: 'object with anyOf nested inside allOf',
type: 'object',
allOf: [
{
required: ['id1', 'obj'],
type: 'object',
properties: {
id1: {
type: 'integer'
},
obj: {
type: 'object',
properties: {
nested: { type: 'string' }
}
}
}
},
{
anyOf: [
{
type: 'object',
properties: {
id2: { type: 'string' }
},
required: ['id2']
},
{
type: 'object',
properties: {
id3: {
type: 'integer'
},
nestedObj: {
type: 'object',
properties: {
nested: { type: 'string' }
}
}
},
required: ['id3']
},
{
type: 'object',
properties: {
id4: { type: 'integer' }
},
required: ['id4']
}
]
}
]
}
const stringify = build(schema)
const value = stringify({
id1: 1,
id3: 3,
id4: 4, // extra prop shouldn't be in result
obj: { nested: 'yes' },
nestedObj: { nested: 'yes' }
})
t.equal(value, '{"id1":1,"obj":{"nested":"yes"},"id3":3,"nestedObj":{"nested":"yes"}}')
})
test('object with $ref in allOf', (t) => {
t.plan(1)
const schema = {
title: 'object with $ref in allOf',
type: 'object',
definitions: {
id1: {
type: 'object',
properties: {
id1: {
type: 'integer'
}
}
}
},
allOf: [
{
$ref: '#/definitions/id1'
}
]
}
const stringify = build(schema)
const value = stringify({
id1: 1,
id2: 2 // extra prop shouldn't be in result
})
t.equal(value, '{"id1":1}')
})
test('object with $ref and other object in allOf', (t) => {
t.plan(1)
const schema = {
title: 'object with $ref in allOf',
type: 'object',
definitions: {
id1: {
type: 'object',
properties: {
id1: {
type: 'integer'
}
}
}
},
allOf: [
{
$ref: '#/definitions/id1'
},
{
type: 'object',
properties: {
id2: {
type: 'integer'
}
}
}
]
}
const stringify = build(schema)
const value = stringify({
id1: 1,
id2: 2,
id3: 3 // extra prop shouldn't be in result
})
t.equal(value, '{"id1":1,"id2":2}')
})
test('object with multiple $refs in allOf', (t) => {
t.plan(1)
const schema = {
title: 'object with $ref in allOf',
type: 'object',
definitions: {
id1: {
type: 'object',
properties: {
id1: {
type: 'integer'
}
}
},
id2: {
type: 'object',
properties: {
id2: {
type: 'integer'
}
}
}
},
allOf: [
{
$ref: '#/definitions/id1'
},
{
$ref: '#/definitions/id2'
}
]
}
const stringify = build(schema)
const value = stringify({
id1: 1,
id2: 2,
id3: 3 // extra prop shouldn't be in result
})
t.equal(value, '{"id1":1,"id2":2}')
})
test('allOf with nested allOf in $ref', (t) => {
t.plan(1)
const schema = {
title: 'allOf with nested allOf in $ref',
type: 'object',
definitions: {
group: {
type: 'object',
allOf: [{
properties: {
id2: {
type: 'integer'
}
}
}, {
properties: {
id3: {
type: 'integer'
}
}
}]
}
},
allOf: [
{
type: 'object',
properties: {
id1: {
type: 'integer'
}
},
required: [
'id1'
]
},
{
$ref: '#/definitions/group'
}
]
}
const stringify = build(schema)
const value = stringify({
id1: 1,
id2: 2,
id3: 3,
id4: 4 // extra prop shouldn't be in result
})
t.equal(value, '{"id1":1,"id2":2,"id3":3}')
})
test('object with external $refs in allOf', (t) => {
t.plan(1)
const externalSchema = {
first: {
definitions: {
id1: {
type: 'object',
properties: {
id1: {
type: 'integer'
}
}
}
}
},
second: {
definitions: {
id2: {
$id: '#id2',
type: 'object',
properties: {
id2: {
type: 'integer'
}
}
}
}
}
}
const schema = {
title: 'object with $ref in allOf',
type: 'object',
allOf: [
{
$ref: 'first#/definitions/id1'
},
{
$ref: 'second#/definitions/id2'
}
]
}
const stringify = build(schema, { schema: externalSchema })
const value = stringify({
id1: 1,
id2: 2,
id3: 3 // extra prop shouldn't be in result
})
t.equal(value, '{"id1":1,"id2":2}')
})
test('allof with local anchor reference', (t) => {
t.plan(1)
const externalSchemas = {
Test: {
$id: 'Test',
definitions: {
Problem: {
type: 'object',
properties: {
type: {
type: 'string'
}
}
},
ValidationFragment: {
type: 'string'
},
ValidationErrorProblem: {
type: 'object',
allOf: [
{
$ref: '#/definitions/Problem'
},
{
type: 'object',
properties: {
validation: {
$ref: '#/definitions/ValidationFragment'
}
}
}
]
}
}
}
}
const schema = { $ref: 'Test#/definitions/ValidationErrorProblem' }
const stringify = build(schema, { schema: externalSchemas })
const data = { type: 'foo', validation: 'bar' }
t.equal(stringify(data), JSON.stringify(data))
})
test('allOf: multiple nested $ref properties', (t) => {
t.plan(2)
const externalSchema1 = {
$id: 'externalSchema1',
oneOf: [
{ $ref: '#/definitions/id1' }
],
definitions: {
id1: {
type: 'object',
properties: {
id1: {
type: 'integer'
}
},
additionalProperties: false
}
}
}
const externalSchema2 = {
$id: 'externalSchema2',
oneOf: [
{ $ref: '#/definitions/id2' }
],
definitions: {
id2: {
type: 'object',
properties: {
id2: {
type: 'integer'
}
},
additionalProperties: false
}
}
}
const schema = {
anyOf: [
{ $ref: 'externalSchema1' },
{ $ref: 'externalSchema2' }
]
}
const stringify = build(schema, { schema: [externalSchema1, externalSchema2] })
t.equal(stringify({ id1: 1 }), JSON.stringify({ id1: 1 }))
t.equal(stringify({ id2: 2 }), JSON.stringify({ id2: 2 }))
})
test('allOf: throw Error if types mismatch ', (t) => {
t.plan(3)
const schema = {
allOf: [
{ type: 'string' },
{ type: 'number' }
]
}
try {
build(schema)
t.fail('should throw the MergeError')
} catch (error) {
t.ok(error instanceof Error)
t.equal(error.message, 'Failed to merge "type" keyword schemas.')
t.same(error.schemas, [['string'], ['number']])
}
})
test('allOf: throw Error if format mismatch ', (t) => {
t.plan(3)
const schema = {
allOf: [
{ format: 'date' },
{ format: 'time' }
]
}
try {
build(schema)
t.fail('should throw the MergeError')
} catch (error) {
t.ok(error instanceof Error)
t.equal(error.message, 'Failed to merge "format" keyword schemas.')
t.same(error.schemas, ['date', 'time'])
}
})
test('recursive nested allOfs', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
foo: {
additionalProperties: false,
allOf: [{ $ref: '#' }]
}
}
}
const data = { foo: {} }
const stringify = build(schema)
t.equal(stringify(data), JSON.stringify(data))
})
test('recursive nested allOfs', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
foo: {
additionalProperties: false,
allOf: [{ allOf: [{ $ref: '#' }] }]
}
}
}
const data = { foo: {} }
const stringify = build(schema)
t.equal(stringify(data), JSON.stringify(data))
})
test('external recursive allOfs', (t) => {
t.plan(1)
const externalSchema = {
type: 'object',
properties: {
foo: {
properties: {
bar: { type: 'string' }
},
allOf: [{ $ref: '#' }]
}
}
}
const schema = {
type: 'object',
properties: {
a: { $ref: 'externalSchema#/properties/foo' },
b: { $ref: 'externalSchema#/properties/foo' }
}
}
const data = {
a: {
foo: {},
bar: '42',
baz: 42
},
b: {
foo: {},
bar: '42',
baz: 42
}
}
const stringify = build(schema, { schema: { externalSchema } })
t.equal(stringify(data), '{"a":{"bar":"42","foo":{}},"b":{"bar":"42","foo":{}}}')
})
test('do not crash with $ref prop', (t) => {
t.plan(1)
const schema = {
title: 'object with $ref',
type: 'object',
properties: {
outside: {
$ref: '#/$defs/outside'
}
},
$defs: {
inside: {
type: 'object',
properties: {
$ref: {
type: 'string'
}
}
},
outside: {
allOf: [{
$ref: '#/$defs/inside'
}]
}
}
}
const stringify = build(schema)
const value = stringify({
outside: {
$ref: 'true'
}
})
t.equal(value, '{"outside":{"$ref":"true"}}')
})

View File

@@ -0,0 +1,231 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('object with nested random property', (t) => {
t.plan(4)
const schema = {
title: 'empty schema to allow any object',
type: 'object',
properties: {
id: { type: 'number' },
name: {}
}
}
const stringify = build(schema)
t.equal(stringify({
id: 1, name: 'string'
}), '{"id":1,"name":"string"}')
t.equal(stringify({
id: 1, name: { first: 'name', last: 'last' }
}), '{"id":1,"name":{"first":"name","last":"last"}}')
t.equal(stringify({
id: 1, name: null
}), '{"id":1,"name":null}')
t.equal(stringify({
id: 1, name: ['first', 'last']
}), '{"id":1,"name":["first","last"]}')
})
// reference: https://github.com/fastify/fast-json-stringify/issues/259
test('object with empty schema with $id: undefined set', (t) => {
t.plan(1)
const schema = {
title: 'empty schema to allow any object with $id: undefined set',
type: 'object',
properties: {
name: { $id: undefined }
}
}
const stringify = build(schema)
t.equal(stringify({
name: 'string'
}), '{"name":"string"}')
})
test('array with random items', (t) => {
t.plan(1)
const schema = {
title: 'empty schema to allow any object',
type: 'array',
items: {}
}
const stringify = build(schema)
const value = stringify([1, 'string', null])
t.equal(value, '[1,"string",null]')
})
test('empty schema', (t) => {
t.plan(7)
const schema = { }
const stringify = build(schema)
t.equal(stringify(null), 'null')
t.equal(stringify(1), '1')
t.equal(stringify(true), 'true')
t.equal(stringify('hello'), '"hello"')
t.equal(stringify({}), '{}')
t.equal(stringify({ x: 10 }), '{"x":10}')
t.equal(stringify([true, 1, 'hello']), '[true,1,"hello"]')
})
test('empty schema on nested object', (t) => {
t.plan(7)
const schema = {
type: 'object',
properties: {
x: {}
}
}
const stringify = build(schema)
t.equal(stringify({ x: null }), '{"x":null}')
t.equal(stringify({ x: 1 }), '{"x":1}')
t.equal(stringify({ x: true }), '{"x":true}')
t.equal(stringify({ x: 'hello' }), '{"x":"hello"}')
t.equal(stringify({ x: {} }), '{"x":{}}')
t.equal(stringify({ x: { x: 10 } }), '{"x":{"x":10}}')
t.equal(stringify({ x: [true, 1, 'hello'] }), '{"x":[true,1,"hello"]}')
})
test('empty schema on array', (t) => {
t.plan(1)
const schema = {
type: 'array',
items: {}
}
const stringify = build(schema)
t.equal(stringify([1, true, 'hello', [], { x: 1 }]), '[1,true,"hello",[],{"x":1}]')
})
test('empty schema on anyOf', (t) => {
t.plan(4)
// any on Foo codepath.
const schema = {
anyOf: [
{
type: 'object',
properties: {
kind: {
type: 'string',
enum: ['Foo']
},
value: {}
}
},
{
type: 'object',
properties: {
kind: {
type: 'string',
enum: ['Bar']
},
value: {
type: 'number'
}
}
}
]
}
const stringify = build(schema)
t.equal(stringify({ kind: 'Bar', value: 1 }), '{"kind":"Bar","value":1}')
t.equal(stringify({ kind: 'Foo', value: 1 }), '{"kind":"Foo","value":1}')
t.equal(stringify({ kind: 'Foo', value: true }), '{"kind":"Foo","value":true}')
t.equal(stringify({ kind: 'Foo', value: 'hello' }), '{"kind":"Foo","value":"hello"}')
})
test('should throw a TypeError with the path to the key of the invalid value /1', (t) => {
t.plan(1)
// any on Foo codepath.
const schema = {
anyOf: [
{
type: 'object',
properties: {
kind: {
type: 'string',
enum: ['Foo']
},
value: {}
}
},
{
type: 'object',
properties: {
kind: {
type: 'string',
enum: ['Bar']
},
value: {
type: 'number'
}
}
}
]
}
const stringify = build(schema)
t.throws(() => stringify({ kind: 'Baz', value: 1 }), new TypeError('The value of \'#\' does not match schema definition.'))
})
test('should throw a TypeError with the path to the key of the invalid value /2', (t) => {
t.plan(1)
// any on Foo codepath.
const schema = {
type: 'object',
properties: {
data: {
anyOf: [
{
type: 'object',
properties: {
kind: {
type: 'string',
enum: ['Foo']
},
value: {}
}
},
{
type: 'object',
properties: {
kind: {
type: 'string',
enum: ['Bar']
},
value: {
type: 'number'
}
}
}
]
}
}
}
const stringify = build(schema)
t.throws(() => stringify({ data: { kind: 'Baz', value: 1 } }), new TypeError('The value of \'#/properties/data\' does not match schema definition.'))
})

View File

@@ -0,0 +1,794 @@
'use strict'
const { test } = require('tap')
const build = require('..')
process.env.TZ = 'UTC'
test('object with multiple types field', (t) => {
t.plan(2)
const schema = {
title: 'object with multiple types field',
type: 'object',
properties: {
str: {
anyOf: [{
type: 'string'
}, {
type: 'boolean'
}]
}
}
}
const stringify = build(schema)
t.equal(stringify({
str: 'string'
}), '{"str":"string"}')
t.equal(stringify({
str: true
}), '{"str":true}')
})
test('object with field of type object or null', (t) => {
t.plan(2)
const schema = {
title: 'object with field of type object or null',
type: 'object',
properties: {
prop: {
anyOf: [{
type: 'object',
properties: {
str: {
type: 'string'
}
}
}, {
type: 'null'
}]
}
}
}
const stringify = build(schema)
t.equal(stringify({
prop: null
}), '{"prop":null}')
t.equal(stringify({
prop: {
str: 'string'
}
}), '{"prop":{"str":"string"}}')
})
test('object with field of type object or array', (t) => {
t.plan(2)
const schema = {
title: 'object with field of type object or array',
type: 'object',
properties: {
prop: {
anyOf: [{
type: 'object',
properties: {},
additionalProperties: true
}, {
type: 'array',
items: {
type: 'string'
}
}]
}
}
}
const stringify = build(schema)
t.equal(stringify({
prop: {
str: 'string'
}
}), '{"prop":{"str":"string"}}')
t.equal(stringify({
prop: ['string']
}), '{"prop":["string"]}')
})
test('object with field of type string and coercion disable ', (t) => {
t.plan(1)
const schema = {
title: 'object with field of type string',
type: 'object',
properties: {
str: {
anyOf: [{
type: 'string'
}]
}
}
}
const stringify = build(schema)
t.throws(() => stringify({ str: 1 }))
})
test('object with field of type string and coercion enable ', (t) => {
t.plan(1)
const schema = {
title: 'object with field of type string',
type: 'object',
properties: {
str: {
anyOf: [{
type: 'string'
}]
}
}
}
const options = {
ajv: {
coerceTypes: true
}
}
const stringify = build(schema, options)
const value = stringify({
str: 1
})
t.equal(value, '{"str":"1"}')
})
test('object with field with type union of multiple objects', (t) => {
t.plan(2)
const schema = {
title: 'object with anyOf property value containing objects',
type: 'object',
properties: {
anyOfSchema: {
anyOf: [
{
type: 'object',
properties: {
baz: { type: 'number' }
},
required: ['baz']
},
{
type: 'object',
properties: {
bar: { type: 'string' }
},
required: ['bar']
}
]
}
},
required: ['anyOfSchema']
}
const stringify = build(schema)
t.equal(stringify({ anyOfSchema: { baz: 5 } }), '{"anyOfSchema":{"baz":5}}')
t.equal(stringify({ anyOfSchema: { bar: 'foo' } }), '{"anyOfSchema":{"bar":"foo"}}')
})
test('null value in schema', (t) => {
t.plan(0)
const schema = {
title: 'schema with null child',
type: 'string',
nullable: true,
enum: [null]
}
build(schema)
})
test('symbol value in schema', (t) => {
t.plan(4)
const ObjectKind = Symbol('LiteralKind')
const UnionKind = Symbol('UnionKind')
const LiteralKind = Symbol('LiteralKind')
const schema = {
kind: ObjectKind,
type: 'object',
properties: {
value: {
kind: UnionKind,
anyOf: [
{ kind: LiteralKind, type: 'string', enum: ['foo'] },
{ kind: LiteralKind, type: 'string', enum: ['bar'] },
{ kind: LiteralKind, type: 'string', enum: ['baz'] }
]
}
},
required: ['value']
}
const stringify = build(schema)
t.equal(stringify({ value: 'foo' }), '{"value":"foo"}')
t.equal(stringify({ value: 'bar' }), '{"value":"bar"}')
t.equal(stringify({ value: 'baz' }), '{"value":"baz"}')
t.throws(() => stringify({ value: 'qux' }))
})
test('anyOf and $ref together', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
cs: {
anyOf: [
{
$ref: '#/definitions/Option'
},
{
type: 'boolean'
}
]
}
},
definitions: {
Option: {
type: 'string'
}
}
}
const stringify = build(schema)
t.equal(stringify({ cs: 'franco' }), '{"cs":"franco"}')
t.equal(stringify({ cs: true }), '{"cs":true}')
})
test('anyOf and $ref: 2 levels are fine', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
cs: {
anyOf: [
{
$ref: '#/definitions/Option'
},
{
type: 'boolean'
}
]
}
},
definitions: {
Option: {
anyOf: [
{
type: 'number'
},
{
type: 'boolean'
}
]
}
}
}
const stringify = build(schema)
const value = stringify({ cs: 3 })
t.equal(value, '{"cs":3}')
})
test('anyOf and $ref: multiple levels should throw at build.', (t) => {
t.plan(3)
const schema = {
type: 'object',
properties: {
cs: {
anyOf: [
{
$ref: '#/definitions/Option'
},
{
type: 'boolean'
}
]
}
},
definitions: {
Option: {
anyOf: [
{
$ref: '#/definitions/Option2'
},
{
type: 'string'
}
]
},
Option2: {
type: 'number'
}
}
}
const stringify = build(schema)
t.equal(stringify({ cs: 3 }), '{"cs":3}')
t.equal(stringify({ cs: true }), '{"cs":true}')
t.equal(stringify({ cs: 'pippo' }), '{"cs":"pippo"}')
})
test('anyOf and $ref - multiple external $ref', (t) => {
t.plan(2)
const externalSchema = {
external: {
definitions: {
def: {
type: 'object',
properties: {
prop: { anyOf: [{ $ref: 'external2#/definitions/other' }] }
}
}
}
},
external2: {
definitions: {
internal: {
type: 'string'
},
other: {
type: 'object',
properties: {
prop2: { $ref: '#/definitions/internal' }
}
}
}
}
}
const schema = {
title: 'object with $ref',
type: 'object',
properties: {
obj: {
$ref: 'external#/definitions/def'
}
}
}
const object = {
obj: {
prop: {
prop2: 'test'
}
}
}
const stringify = build(schema, { schema: externalSchema })
const output = stringify(object)
JSON.parse(output)
t.pass()
t.equal(output, '{"obj":{"prop":{"prop2":"test"}}}')
})
test('anyOf looks for all of the array items', (t) => {
t.plan(1)
const schema = {
title: 'type array that may have any of declared items',
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
savedId: {
type: 'string'
}
},
required: ['savedId']
},
{
type: 'object',
properties: {
error: {
type: 'string'
}
},
required: ['error']
}
]
}
}
const stringify = build(schema)
const value = stringify([{ savedId: 'great' }, { error: 'oops' }])
t.equal(value, '[{"savedId":"great"},{"error":"oops"}]')
})
test('anyOf with enum with more than 100 entries', (t) => {
t.plan(1)
const schema = {
title: 'type array that may have any of declared items',
type: 'array',
items: {
anyOf: [
{
type: 'string',
enum: ['EUR', 'USD', ...(new Set([...new Array(200)].map(() => Math.random().toString(36).substr(2, 3)))).values()]
},
{ type: 'null' }
]
}
}
const stringify = build(schema)
const value = stringify(['EUR', 'USD', null])
t.equal(value, '["EUR","USD",null]')
})
test('anyOf object with field date-time of type string with format or null', (t) => {
t.plan(1)
const toStringify = new Date()
const withOneOfSchema = {
type: 'object',
properties: {
prop: {
anyOf: [{
type: 'string',
format: 'date-time'
}, {
type: 'null'
}]
}
}
}
const withOneOfStringify = build(withOneOfSchema)
t.equal(withOneOfStringify({
prop: toStringify
}), `{"prop":"${toStringify.toISOString()}"}`)
})
test('anyOf object with nested field date-time of type string with format or null', (t) => {
t.plan(1)
const withOneOfSchema = {
type: 'object',
properties: {
prop: {
anyOf: [{
type: 'object',
properties: {
nestedProp: {
type: 'string',
format: 'date-time'
}
}
}]
}
}
}
const withOneOfStringify = build(withOneOfSchema)
const data = {
prop: { nestedProp: new Date() }
}
t.equal(withOneOfStringify(data), JSON.stringify(data))
})
test('anyOf object with nested field date of type string with format or null', (t) => {
t.plan(1)
const withOneOfSchema = {
type: 'object',
properties: {
prop: {
anyOf: [{
type: 'object',
properties: {
nestedProp: {
type: 'string',
format: 'date'
}
}
}]
}
}
}
const withOneOfStringify = build(withOneOfSchema)
const data = {
prop: { nestedProp: new Date(1674263005800) }
}
t.equal(withOneOfStringify(data), '{"prop":{"nestedProp":"2023-01-21"}}')
})
test('anyOf object with nested field time of type string with format or null', (t) => {
t.plan(1)
const withOneOfSchema = {
type: 'object',
properties: {
prop: {
anyOf: [{
type: 'object',
properties: {
nestedProp: {
type: 'string',
format: 'time'
}
}
}]
}
}
}
const withOneOfStringify = build(withOneOfSchema)
const data = {
prop: { nestedProp: new Date(1674263005800) }
}
t.equal(withOneOfStringify(data), '{"prop":{"nestedProp":"01:03:25"}}')
})
test('anyOf object with field date of type string with format or null', (t) => {
t.plan(1)
const toStringify = '2011-01-01'
const withOneOfSchema = {
type: 'object',
properties: {
prop: {
anyOf: [{
type: 'string',
format: 'date'
}, {
type: 'null'
}]
}
}
}
const withOneOfStringify = build(withOneOfSchema)
t.equal(withOneOfStringify({
prop: toStringify
}), '{"prop":"2011-01-01"}')
})
test('anyOf object with invalid field date of type string with format or null', (t) => {
t.plan(1)
const toStringify = 'foo bar'
const withOneOfSchema = {
type: 'object',
properties: {
prop: {
anyOf: [{
type: 'string',
format: 'date'
}, {
type: 'null'
}]
}
}
}
const withOneOfStringify = build(withOneOfSchema)
t.throws(() => withOneOfStringify({ prop: toStringify }))
})
test('anyOf with a nested external schema', (t) => {
t.plan(1)
const externalSchemas = {
schema1: {
definitions: {
def1: {
$id: 'external',
type: 'string'
}
},
type: 'number'
}
}
const schema = { anyOf: [{ $ref: 'external' }] }
const stringify = build(schema, { schema: externalSchemas })
t.equal(stringify('foo'), '"foo"')
})
test('object with ref and validated properties', (t) => {
t.plan(1)
const externalSchemas = {
RefSchema: {
$id: 'RefSchema',
type: 'string'
}
}
const schema = {
$id: 'root',
type: 'object',
properties: {
id: {
anyOf: [
{ type: 'string' },
{ type: 'number' }
]
},
reference: { $ref: 'RefSchema' }
}
}
const stringify = build(schema, { schema: externalSchemas })
t.equal(stringify({ id: 1, reference: 'hi' }), '{"id":1,"reference":"hi"}')
})
test('anyOf required props', (t) => {
t.plan(3)
const schema = {
type: 'object',
properties: {
prop1: { type: 'string' },
prop2: { type: 'string' },
prop3: { type: 'string' }
},
required: ['prop1'],
anyOf: [{ required: ['prop2'] }, { required: ['prop3'] }]
}
const stringify = build(schema)
t.equal(stringify({ prop1: 'test', prop2: 'test2' }), '{"prop1":"test","prop2":"test2"}')
t.equal(stringify({ prop1: 'test', prop3: 'test3' }), '{"prop1":"test","prop3":"test3"}')
t.equal(stringify({ prop1: 'test', prop2: 'test2', prop3: 'test3' }), '{"prop1":"test","prop2":"test2","prop3":"test3"}')
})
test('anyOf required props', (t) => {
t.plan(3)
const schema = {
type: 'object',
properties: {
prop1: { type: 'string' }
},
anyOf: [
{
properties: {
prop2: { type: 'string' }
}
},
{
properties: {
prop3: { type: 'string' }
}
}
]
}
const stringify = build(schema)
t.equal(stringify({ prop1: 'test1' }), '{"prop1":"test1"}')
t.equal(stringify({ prop2: 'test2' }), '{"prop2":"test2"}')
t.equal(stringify({ prop1: 'test1', prop2: 'test2' }), '{"prop1":"test1","prop2":"test2"}')
})
test('recursive nested anyOfs', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
foo: {
additionalProperties: false,
anyOf: [{ $ref: '#' }]
}
}
}
const data = { foo: {} }
const stringify = build(schema)
t.equal(stringify(data), JSON.stringify(data))
})
test('recursive nested anyOfs', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
foo: {
additionalProperties: false,
anyOf: [{ anyOf: [{ $ref: '#' }] }]
}
}
}
const data = { foo: {} }
const stringify = build(schema)
t.equal(stringify(data), JSON.stringify(data))
})
test('external recursive anyOfs', (t) => {
t.plan(1)
const externalSchema = {
type: 'object',
properties: {
foo: {
properties: {
bar: { type: 'string' }
},
anyOf: [{ $ref: '#' }]
}
}
}
const schema = {
type: 'object',
properties: {
a: { $ref: 'externalSchema#/properties/foo' },
b: { $ref: 'externalSchema#/properties/foo' }
}
}
const data = {
a: {
foo: {},
bar: '42',
baz: 42
},
b: {
foo: {},
bar: '42',
baz: 42
}
}
const stringify = build(schema, { schema: { externalSchema } })
t.equal(stringify(data), '{"a":{"bar":"42","foo":{}},"b":{"bar":"42","foo":{}}}')
})
test('should build merged schemas twice', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
enums: {
type: 'string',
anyOf: [
{ type: 'string', const: 'FOO' },
{ type: 'string', const: 'BAR' }
]
}
}
}
{
const stringify = build(schema)
t.equal(stringify({ enums: 'FOO' }), '{"enums":"FOO"}')
}
{
const stringify = build(schema)
t.equal(stringify({ enums: 'BAR' }), '{"enums":"BAR"}')
}
})

View File

@@ -0,0 +1,638 @@
'use strict'
const test = require('tap').test
const validator = require('is-my-json-valid')
const build = require('..')
const Ajv = require('ajv')
test('error on invalid largeArrayMechanism', (t) => {
t.plan(1)
t.throws(() => build({
title: 'large array of null values with default mechanism',
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'null' }
}
}
}, {
largeArraySize: 2e4,
largeArrayMechanism: 'invalid'
}), Error('Unsupported large array mechanism invalid'))
})
function buildTest (schema, toStringify, options) {
test(`render a ${schema.title} as JSON`, (t) => {
t.plan(3)
const validate = validator(schema)
const stringify = build(schema, options)
const output = stringify(toStringify)
t.same(JSON.parse(output), JSON.parse(JSON.stringify(toStringify)))
t.equal(output, JSON.stringify(toStringify))
t.ok(validate(JSON.parse(output)), 'valid schema')
})
}
buildTest({
title: 'dates tuple',
type: 'object',
properties: {
dates: {
type: 'array',
minItems: 2,
maxItems: 2,
items: [
{
type: 'string',
format: 'date-time'
},
{
type: 'string',
format: 'date-time'
}
]
}
}
}, {
dates: [new Date(1), new Date(2)]
})
buildTest({
title: 'string array',
type: 'object',
properties: {
ids: {
type: 'array',
items: {
type: 'string'
}
}
}
}, {
ids: ['test']
})
buildTest({
title: 'number array',
type: 'object',
properties: {
ids: {
type: 'array',
items: {
type: 'number'
}
}
}
}, {
ids: [1]
})
buildTest({
title: 'mixed array',
type: 'object',
properties: {
ids: {
type: 'array',
items: [
{
type: 'null'
},
{
type: 'string'
},
{
type: 'integer'
},
{
type: 'number'
},
{
type: 'boolean'
},
{
type: 'object',
properties: {
a: {
type: 'string'
}
}
},
{
type: 'array',
items: {
type: 'string'
}
}
]
}
}
}, {
ids: [null, 'test', 1, 1.1, true, { a: 'test' }, ['test']]
})
buildTest({
title: 'repeated types',
type: 'object',
properties: {
ids: {
type: 'array',
items: [
{
type: 'number'
},
{
type: 'number'
}
]
}
}
}, { ids: [1, 2] })
buildTest({
title: 'pattern properties array',
type: 'object',
properties: {
args: {
type: 'array',
items: [
{
type: 'object',
patternProperties: {
'.*': {
type: 'string'
}
}
},
{
type: 'object',
patternProperties: {
'.*': {
type: 'number'
}
}
}
]
}
}
}, { args: [{ a: 'test' }, { b: 1 }] })
buildTest({
title: 'array with weird key',
type: 'object',
properties: {
'@data': {
type: 'array',
items: {
type: 'string'
}
}
}
}, {
'@data': ['test']
})
test('invalid items throw', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
args: {
type: 'array',
items: [
{
type: 'object',
patternProperties: {
'.*': {
type: 'string'
}
}
}
]
}
}
}
const stringify = build(schema)
t.throws(() => stringify({ args: ['invalid'] }))
})
buildTest({
title: 'item types in array default to any',
type: 'object',
properties: {
foo: {
type: 'array'
}
}
}, {
foo: [1, 'string', {}, null]
})
test('array items is a list of schema and additionalItems is true, just the described item is validated', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
foo: {
type: 'array',
items: [
{
type: 'string'
}
],
additionalItems: true
}
}
}
const stringify = build(schema)
const result = stringify({
foo: [
'foo',
'bar',
1
]
})
t.equal(result, '{"foo":["foo","bar",1]}')
})
test('array items is a list of schema and additionalItems is true, just the described item is validated', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
foo: {
type: 'array',
items: [
{
type: 'string'
},
{
type: 'number'
}
],
additionalItems: true
}
}
}
const stringify = build(schema)
const result = stringify({
foo: ['foo']
})
t.equal(result, '{"foo":["foo"]}')
})
test('array items is a list of schema and additionalItems is false /1', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
foo: {
type: 'array',
items: [
{ type: 'string' }
],
additionalItems: false
}
}
}
const stringify = build(schema)
t.throws(() => stringify({ foo: ['foo', 'bar'] }), new Error('Item at 1 does not match schema definition.'))
})
test('array items is a list of schema and additionalItems is false /2', (t) => {
t.plan(3)
const schema = {
type: 'object',
properties: {
foo: {
type: 'array',
items: [
{ type: 'string' },
{ type: 'string' }
],
additionalItems: false
}
}
}
const stringify = build(schema)
t.throws(() => stringify({ foo: [1, 'bar'] }), new Error('Item at 0 does not match schema definition.'))
t.throws(() => stringify({ foo: ['foo', 1] }), new Error('Item at 1 does not match schema definition.'))
t.throws(() => stringify({ foo: ['foo', 'bar', 'baz'] }), new Error('Item at 2 does not match schema definition.'))
})
test('array items is a schema and additionalItems is false', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: {
type: 'array',
items: { type: 'string' },
additionalItems: false
}
}
}
const stringify = build(schema)
// ajv ignores additionalItems if items is not an Array
const ajv = new Ajv({ allErrors: true, strict: false })
const validate = ajv.compile(schema)
t.same(stringify({ foo: ['foo', 'bar'] }), '{"foo":["foo","bar"]}')
t.equal(validate({ foo: ['foo', 'bar'] }), true)
})
// https://github.com/fastify/fast-json-stringify/issues/279
test('object array with anyOf and symbol', (t) => {
t.plan(1)
const ArrayKind = Symbol('ArrayKind')
const ObjectKind = Symbol('LiteralKind')
const UnionKind = Symbol('UnionKind')
const LiteralKind = Symbol('LiteralKind')
const StringKind = Symbol('StringKind')
const schema = {
kind: ArrayKind,
type: 'array',
items: {
kind: ObjectKind,
type: 'object',
properties: {
name: {
kind: StringKind,
type: 'string'
},
option: {
kind: UnionKind,
anyOf: [
{
kind: LiteralKind,
type: 'string',
enum: ['Foo']
},
{
kind: LiteralKind,
type: 'string',
enum: ['Bar']
}
]
}
},
required: ['name', 'option']
}
}
const stringify = build(schema)
const value = stringify([
{ name: 'name-0', option: 'Foo' },
{ name: 'name-1', option: 'Bar' }
])
t.equal(value, '[{"name":"name-0","option":"Foo"},{"name":"name-1","option":"Bar"}]')
})
test('different arrays with same item schemas', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
array1: {
type: 'array',
items: [{ type: 'string' }],
additionalItems: false
},
array2: {
type: 'array',
items: { $ref: '#/properties/array1/items' },
additionalItems: true
}
}
}
const stringify = build(schema)
const data = { array1: ['bar'], array2: ['foo', 'bar'] }
t.equal(stringify(data), '{"array1":["bar"],"array2":["foo","bar"]}')
})
const largeArray = new Array(2e4).fill({ a: 'test', b: 1 })
buildTest({
title: 'large array with default mechanism',
type: 'object',
properties: {
ids: {
type: 'array',
items: {
type: 'object',
properties: {
a: { type: 'string' },
b: { type: 'number' }
}
}
}
}
}, {
ids: largeArray
}, {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
})
buildTest({
title: 'large array of objects with json-stringify mechanism',
type: 'object',
properties: {
ids: {
type: 'array',
items: {
type: 'object',
properties: {
a: { type: 'string' },
b: { type: 'number' }
}
}
}
}
}, {
ids: largeArray
}, {
largeArrayMechanism: 'json-stringify'
})
buildTest({
title: 'large array of strings with default mechanism',
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'string' }
}
}
}, {
ids: new Array(2e4).fill('string')
}, {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
})
buildTest({
title: 'large array of numbers with default mechanism',
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'number' }
}
}
}, {
ids: new Array(2e4).fill(42)
}, {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
})
buildTest({
title: 'large array of integers with default mechanism',
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'integer' }
}
}
}, {
ids: new Array(2e4).fill(42)
}, {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
})
buildTest({
title: 'large array of booleans with default mechanism',
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'boolean' }
}
}
}, {
ids: new Array(2e4).fill(true)
}, {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
})
buildTest({
title: 'large array of null values with default mechanism',
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'null' }
}
}
}, {
ids: new Array(2e4).fill(null)
}, {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
})
test('error on invalid value for largeArraySize /1', (t) => {
t.plan(1)
t.throws(() => build({
title: 'large array of null values with default mechanism',
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'null' }
}
}
}, {
largeArraySize: 'invalid'
}), Error('Unsupported large array size. Expected integer-like, got string with value invalid'))
})
test('error on invalid value for largeArraySize /2', (t) => {
t.plan(1)
t.throws(() => build({
title: 'large array of null values with default mechanism',
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'null' }
}
}
}, {
largeArraySize: Infinity
}), Error('Unsupported large array size. Expected integer-like, got number with value Infinity'))
})
test('error on invalid value for largeArraySize /3', (t) => {
t.plan(1)
t.throws(() => build({
title: 'large array of null values with default mechanism',
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'null' }
}
}
}, {
largeArraySize: [200]
}), Error('Unsupported large array size. Expected integer-like, got object with value 200'))
})
buildTest({
title: 'large array of integers with largeArraySize is bigint',
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'integer' }
}
}
}, {
ids: new Array(2e4).fill(42)
}, {
largeArraySize: 20000n,
largeArrayMechanism: 'default'
})
buildTest({
title: 'large array of integers with largeArraySize is valid string',
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'integer' }
}
}
}, {
ids: new Array(1e4).fill(42)
}, {
largeArraySize: '10000',
largeArrayMechanism: 'default'
})

View File

@@ -0,0 +1,13 @@
'use strict'
const test = require('tap').test
test('asNumber should convert BigInt', (t) => {
t.plan(1)
const Serializer = require('../lib/serializer')
const serializer = new Serializer()
const number = serializer.asNumber(11753021440n)
t.equal(number, '11753021440')
})

View File

@@ -0,0 +1,412 @@
'use strict'
const test = require('tap').test
const validator = require('is-my-json-valid')
const build = require('..')
function buildTest (schema, toStringify) {
test(`render a ${schema.title} as JSON`, (t) => {
t.plan(3)
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.same(JSON.parse(output), toStringify)
t.equal(output, JSON.stringify(toStringify))
t.ok(validate(JSON.parse(output)), 'valid schema')
})
}
buildTest({
title: 'string',
type: 'string',
format: 'unsafe'
}, 'hello world')
buildTest({
title: 'basic',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 0
},
magic: {
type: 'number'
}
},
required: ['firstName', 'lastName']
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
magic: 42.42
})
buildTest({
title: 'string',
type: 'string'
}, 'hello world')
buildTest({
title: 'string',
type: 'string'
}, 'hello\nworld')
buildTest({
title: 'string with quotes',
type: 'string'
}, 'hello """" world')
buildTest({
title: 'boolean true',
type: 'boolean'
}, true)
buildTest({
title: 'boolean false',
type: 'boolean'
}, false)
buildTest({
title: 'an integer',
type: 'integer'
}, 42)
buildTest({
title: 'a number',
type: 'number'
}, 42.42)
buildTest({
title: 'deep',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
more: {
description: 'more properties',
type: 'object',
properties: {
something: {
type: 'string'
}
}
}
}
}, {
firstName: 'Matteo',
lastName: 'Collina',
more: {
something: 'else'
}
})
buildTest({
title: 'null',
type: 'null'
}, null)
buildTest({
title: 'deep object with weird keys',
type: 'object',
properties: {
'@version': {
type: 'integer'
}
}
}, {
'@version': 1
})
buildTest({
title: 'deep object with weird keys of type object',
type: 'object',
properties: {
'@data': {
type: 'object',
properties: {
id: {
type: 'string'
}
}
}
}
}, {
'@data': {
id: 'string'
}
})
buildTest({
title: 'deep object with spaces in key',
type: 'object',
properties: {
'spaces in key': {
type: 'object',
properties: {
something: {
type: 'integer'
}
}
}
}
}, {
'spaces in key': {
something: 1
}
})
buildTest({
title: 'with null',
type: 'object',
properties: {
firstName: {
type: 'null'
}
}
}, {
firstName: null
})
buildTest({
title: 'array with objects',
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
}, [{
name: 'Matteo'
}, {
name: 'Dave'
}])
buildTest({
title: 'array with strings',
type: 'array',
items: {
type: 'string'
}
}, [
'Matteo',
'Dave'
])
buildTest({
title: 'array with numbers',
type: 'array',
items: {
type: 'number'
}
}, [
42.42,
24
])
buildTest({
title: 'array with integers',
type: 'array',
items: {
type: 'number'
}
}, [
42,
24
])
buildTest({
title: 'nested array with objects',
type: 'object',
properties: {
data: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
}
}
}, {
data: [{
name: 'Matteo'
}, {
name: 'Dave'
}]
})
buildTest({
title: 'object with boolean',
type: 'object',
properties: {
readonly: {
type: 'boolean'
}
}
}, {
readonly: true
})
test('throw an error or coerce numbers and integers that are not numbers', (t) => {
const stringify = build({
title: 'basic',
type: 'object',
properties: {
age: {
type: 'number'
},
distance: {
type: 'integer'
}
}
})
try {
stringify({ age: 'hello ', distance: 'long' })
t.fail('should throw an error')
} catch (err) {
t.ok(err)
}
const result = stringify({
age: '42',
distance: true
})
t.same(JSON.parse(result), { age: 42, distance: 1 })
t.end()
})
test('Should throw on invalid schema', t => {
t.plan(1)
try {
build({
type: 'Dinosaur',
properties: {
claws: { type: 'sharp' }
}
})
t.fail('should be an invalid schema')
} catch (err) {
t.ok(err)
}
})
test('additionalProperties - throw on unknown type', (t) => {
t.plan(1)
try {
build({
title: 'check array coerce',
type: 'object',
properties: {},
additionalProperties: {
type: 'strangetype'
}
})
t.fail('should be an invalid schema')
} catch (err) {
t.ok(err)
}
})
test('patternProperties - throw on unknown type', (t) => {
t.plan(1)
try {
build({
title: 'check array coerce',
type: 'object',
properties: {},
patternProperties: {
foo: {
type: 'strangetype'
}
}
})
t.fail('should be an invalid schema')
} catch (err) {
t.ok(err)
}
})
test('render a double quote as JSON /1', (t) => {
t.plan(2)
const schema = {
type: 'string'
}
const toStringify = '" double quote'
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, JSON.stringify(toStringify))
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a double quote as JSON /2', (t) => {
t.plan(2)
const schema = {
type: 'string'
}
const toStringify = 'double quote " 2'
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, JSON.stringify(toStringify))
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a long string', (t) => {
t.plan(2)
const schema = {
type: 'string'
}
const toStringify = 'the Ultimate Question of Life, the Universe, and Everything.'
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, JSON.stringify(toStringify))
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('returns JSON.stringify if schema type is boolean', t => {
t.plan(1)
const schema = {
type: 'array',
items: true
}
const array = [1, true, 'test']
const stringify = build(schema)
t.equal(stringify(array), JSON.stringify(array))
})

View File

@@ -0,0 +1,76 @@
'use strict'
const t = require('tap')
const test = t.test
const build = require('..')
test('render a bigint as JSON', (t) => {
t.plan(1)
const schema = {
title: 'bigint',
type: 'integer'
}
const stringify = build(schema)
const output = stringify(1615n)
t.equal(output, '1615')
})
test('render an object with a bigint as JSON', (t) => {
t.plan(1)
const schema = {
title: 'object with bigint',
type: 'object',
properties: {
id: {
type: 'integer'
}
}
}
const stringify = build(schema)
const output = stringify({
id: 1615n
})
t.equal(output, '{"id":1615}')
})
test('render an array with a bigint as JSON', (t) => {
t.plan(1)
const schema = {
title: 'array with bigint',
type: 'array',
items: {
type: 'integer'
}
}
const stringify = build(schema)
const output = stringify([1615n])
t.equal(output, '[1615]')
})
test('render an object with an additionalProperty of type bigint as JSON', (t) => {
t.plan(1)
const schema = {
title: 'object with bigint',
type: 'object',
additionalProperties: {
type: 'integer'
}
}
const stringify = build(schema)
const output = stringify({
num: 1615n
})
t.equal(output, '{"num":1615}')
})

View File

@@ -0,0 +1,45 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('Should clean the cache', (t) => {
t.plan(1)
const schema = {
$id: 'test',
type: 'string'
}
build(schema)
build(schema)
t.pass()
})
test('Should clean the cache with external schemas', (t) => {
t.plan(1)
const schema = {
$id: 'test',
definitions: {
def: {
type: 'object',
properties: {
str: {
type: 'string'
}
}
}
},
type: 'object',
properties: {
obj: {
$ref: '#/definitions/def'
}
}
}
build(schema)
build(schema)
t.pass()
})

View File

@@ -0,0 +1,314 @@
'use strict'
const test = require('tap').test
const validator = require('is-my-json-valid')
const build = require('..')
test('schema with const string', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: 'bar' }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: 'bar'
})
t.equal(output, '{"foo":"bar"}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('schema with const string and different input', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: 'bar' }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: 'baz'
})
t.equal(output, '{"foo":"bar"}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('schema with const string and different type input', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: 'bar' }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: 1
})
t.equal(output, '{"foo":"bar"}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('schema with const string and no input', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: 'bar' }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({})
t.equal(output, '{}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('schema with const string that contains \'', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: "'bar'" }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: "'bar'"
})
t.equal(output, '{"foo":"\'bar\'"}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('schema with const number', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: 1 }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: 1
})
t.equal(output, '{"foo":1}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('schema with const number and different input', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: 1 }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: 2
})
t.equal(output, '{"foo":1}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('schema with const bool', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: true }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: true
})
t.equal(output, '{"foo":true}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('schema with const number', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: 1 }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: 1
})
t.equal(output, '{"foo":1}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('schema with const null', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: null }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: null
})
t.equal(output, '{"foo":null}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('schema with const array', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: [1, 2, 3] }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: [1, 2, 3]
})
t.equal(output, '{"foo":[1,2,3]}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('schema with const object', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: { bar: 'baz' } }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: { bar: 'baz' }
})
t.equal(output, '{"foo":{"bar":"baz"}}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('schema with const and null as type', (t) => {
t.plan(4)
const schema = {
type: 'object',
properties: {
foo: { type: ['string', 'null'], const: 'baz' }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: null
})
t.equal(output, '{"foo":null}')
t.ok(validate(JSON.parse(output)), 'valid schema')
const output2 = stringify({ foo: 'baz' })
t.equal(output2, '{"foo":"baz"}')
t.ok(validate(JSON.parse(output2)), 'valid schema')
})
test('schema with const as nullable', (t) => {
t.plan(4)
const schema = {
type: 'object',
properties: {
foo: { nullable: true, const: 'baz' }
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
foo: null
})
t.equal(output, '{"foo":null}')
t.ok(validate(JSON.parse(output)), 'valid schema')
const output2 = stringify({
foo: 'baz'
})
t.equal(output2, '{"foo":"baz"}')
t.ok(validate(JSON.parse(output2)), 'valid schema')
})
test('schema with const and invalid object', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
foo: { const: { foo: 'bar' } }
},
required: ['foo']
}
const validate = validator(schema)
const stringify = build(schema)
const result = stringify({
foo: { foo: 'baz' }
})
t.equal(result, '{"foo":{"foo":"bar"}}')
t.ok(validate(JSON.parse(result)), 'valid schema')
})

View File

@@ -0,0 +1,639 @@
'use strict'
const test = require('tap').test
const validator = require('is-my-json-valid')
const build = require('..')
process.env.TZ = 'UTC'
test('render a date in a string as JSON', (t) => {
t.plan(2)
const schema = {
title: 'a date in a string',
type: 'string'
}
const toStringify = new Date(1674263005800)
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, JSON.stringify(toStringify))
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a date in a string when format is date-format as ISOString', (t) => {
t.plan(2)
const schema = {
title: 'a date in a string',
type: 'string',
format: 'date-time'
}
const toStringify = new Date(1674263005800)
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, JSON.stringify(toStringify))
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a nullable date in a string when format is date-format as ISOString', (t) => {
t.plan(2)
const schema = {
title: 'a date in a string',
type: 'string',
format: 'date-time',
nullable: true
}
const toStringify = new Date(1674263005800)
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, JSON.stringify(toStringify))
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a date in a string when format is date as YYYY-MM-DD', (t) => {
t.plan(2)
const schema = {
title: 'a date in a string',
type: 'string',
format: 'date'
}
const toStringify = new Date(1674263005800)
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, '"2023-01-21"')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a nullable date in a string when format is date as YYYY-MM-DD', (t) => {
t.plan(2)
const schema = {
title: 'a date in a string',
type: 'string',
format: 'date',
nullable: true
}
const toStringify = new Date(1674263005800)
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, '"2023-01-21"')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('verify padding for rendered date in a string when format is date', (t) => {
t.plan(2)
const schema = {
title: 'a date in a string',
type: 'string',
format: 'date'
}
const toStringify = new Date(2020, 0, 1, 0, 0, 0, 0)
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, '"2020-01-01"')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a date in a string when format is time as kk:mm:ss', (t) => {
t.plan(3)
const schema = {
title: 'a date in a string',
type: 'string',
format: 'time'
}
const toStringify = new Date(1674263005800)
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
validate(JSON.parse(output))
t.equal(validate.errors, null)
t.equal(output, '"01:03:25"')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a nullable date in a string when format is time as kk:mm:ss', (t) => {
t.plan(3)
const schema = {
title: 'a date in a string',
type: 'string',
format: 'time',
nullable: true
}
const toStringify = new Date(1674263005800)
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
validate(JSON.parse(output))
t.equal(validate.errors, null)
t.equal(output, '"01:03:25"')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a midnight time', (t) => {
t.plan(3)
const schema = {
title: 'a date in a string',
type: 'string',
format: 'time'
}
const midnight = new Date(new Date(1674263005800).setHours(24))
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(midnight)
validate(JSON.parse(output))
t.equal(validate.errors, null)
t.equal(output, '"00:03:25"')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('verify padding for rendered date in a string when format is time', (t) => {
t.plan(3)
const schema = {
title: 'a date in a string',
type: 'string',
format: 'time'
}
const toStringify = new Date(2020, 0, 1, 1, 1, 1, 1)
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
validate(JSON.parse(output))
t.equal(validate.errors, null)
t.equal(output, '"01:01:01"')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a nested object in a string when type is date-format as ISOString', (t) => {
t.plan(2)
const schema = {
title: 'an object in a string',
type: 'object',
properties: {
date: {
type: 'string',
format: 'date-time'
}
}
}
const toStringify = { date: new Date(1674263005800) }
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, JSON.stringify(toStringify))
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('serializing null value', t => {
const input = { updatedAt: null }
function createSchema (properties) {
return {
title: 'an object in a string',
type: 'object',
properties
}
}
function serialize (schema, input) {
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(input)
return {
validate,
output
}
}
t.plan(3)
t.test('type::string', t => {
t.plan(3)
t.test('format::date-time', t => {
t.plan(2)
const prop = {
updatedAt: {
type: 'string',
format: 'date-time'
}
}
const {
output,
validate
} = serialize(createSchema(prop), input)
t.equal(output, '{"updatedAt":""}')
t.notOk(validate(JSON.parse(output)), 'an empty string is not a date-time format')
})
t.test('format::date', t => {
t.plan(2)
const prop = {
updatedAt: {
type: 'string',
format: 'date'
}
}
const {
output,
validate
} = serialize(createSchema(prop), input)
t.equal(output, '{"updatedAt":""}')
t.notOk(validate(JSON.parse(output)), 'an empty string is not a date format')
})
t.test('format::time', t => {
t.plan(2)
const prop = {
updatedAt: {
type: 'string',
format: 'time'
}
}
const {
output,
validate
} = serialize(createSchema(prop), input)
t.equal(output, '{"updatedAt":""}')
t.notOk(validate(JSON.parse(output)), 'an empty string is not a time format')
})
})
t.test('type::array', t => {
t.plan(6)
t.test('format::date-time', t => {
t.plan(2)
const prop = {
updatedAt: {
type: ['string'],
format: 'date-time'
}
}
const {
output,
validate
} = serialize(createSchema(prop), input)
t.equal(output, '{"updatedAt":""}')
t.notOk(validate(JSON.parse(output)), 'an empty string is not a date-time format')
})
t.test('format::date', t => {
t.plan(2)
const prop = {
updatedAt: {
type: ['string'],
format: 'date'
}
}
const {
output,
validate
} = serialize(createSchema(prop), input)
t.equal(output, '{"updatedAt":""}')
t.notOk(validate(JSON.parse(output)), 'an empty string is not a date format')
})
t.test('format::date', t => {
t.plan(2)
const prop = {
updatedAt: {
type: ['string'],
format: 'date'
}
}
const {
output,
validate
} = serialize(createSchema(prop), input)
t.equal(output, '{"updatedAt":""}')
t.notOk(validate(JSON.parse(output)), 'an empty string is not a date format')
})
t.test('format::time, Date object', t => {
t.plan(1)
const schema = {
oneOf: [
{
type: 'object',
properties: {
updatedAt: {
type: ['string', 'number'],
format: 'time'
}
}
}
]
}
const date = new Date(1674263005800)
const input = { updatedAt: date }
const { output } = serialize(schema, input)
t.equal(output, JSON.stringify({ updatedAt: '01:03:25' }))
})
t.test('format::time, Date object', t => {
t.plan(1)
const schema = {
oneOf: [
{
type: ['string', 'number'],
format: 'time'
}
]
}
const date = new Date(1674263005800)
const { output } = serialize(schema, date)
t.equal(output, '"01:03:25"')
})
t.test('format::time, Date object', t => {
t.plan(1)
const schema = {
oneOf: [
{
type: ['string', 'number'],
format: 'time'
}
]
}
const { output } = serialize(schema, 42)
t.equal(output, JSON.stringify(42))
})
})
t.test('type::array::nullable', t => {
t.plan(3)
t.test('format::date-time', t => {
t.plan(2)
const prop = {
updatedAt: {
type: ['string', 'null'],
format: 'date-time'
}
}
const {
output,
validate
} = serialize(createSchema(prop), input)
t.equal(output, '{"updatedAt":null}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
t.test('format::date', t => {
t.plan(2)
const prop = {
updatedAt: {
type: ['string', 'null'],
format: 'date'
}
}
const {
output,
validate
} = serialize(createSchema(prop), input)
t.equal(output, '{"updatedAt":null}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
t.test('format::time', t => {
t.plan(2)
const prop = {
updatedAt: {
type: ['string', 'null'],
format: 'time'
}
}
const {
output,
validate
} = serialize(createSchema(prop), input)
t.equal(output, '{"updatedAt":null}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
})
})
test('Validate Date object as string type', (t) => {
t.plan(1)
const schema = {
oneOf: [
{ type: 'string' }
]
}
const toStringify = new Date(1674263005800)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, JSON.stringify(toStringify))
})
test('nullable date', (t) => {
t.plan(1)
const schema = {
anyOf: [
{
format: 'date',
type: 'string',
nullable: true
}
]
}
const stringify = build(schema)
const data = new Date(1674263005800)
const result = stringify(data)
t.same(result, '"2023-01-21"')
})
test('non-date format should not affect data serialization (issue #491)', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
hello: {
type: 'string',
format: 'int64',
pattern: '^[0-9]*$'
}
}
}
const stringify = build(schema)
const data = { hello: 123n }
t.equal(stringify(data), '{"hello":"123"}')
})
test('should serialize also an invalid string value, even if it is not a valid date', (t) => {
t.plan(2)
const schema = {
title: 'a date in a string',
type: 'string',
format: 'date-time',
nullable: true
}
const toStringify = 'invalid'
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, JSON.stringify(toStringify))
t.not(validate(JSON.parse(output)), 'valid schema')
})
test('should throw an error if value can not be transformed to date-time', (t) => {
t.plan(2)
const schema = {
title: 'a date in a string',
type: 'string',
format: 'date-time',
nullable: true
}
const toStringify = true
const validate = validator(schema)
const stringify = build(schema)
t.throws(() => stringify(toStringify), new Error('The value "true" cannot be converted to a date-time.'))
t.not(validate(toStringify))
})
test('should throw an error if value can not be transformed to date', (t) => {
t.plan(2)
const schema = {
title: 'a date in a string',
type: 'string',
format: 'date',
nullable: true
}
const toStringify = true
const validate = validator(schema)
const stringify = build(schema)
t.throws(() => stringify(toStringify), new Error('The value "true" cannot be converted to a date.'))
t.not(validate(toStringify))
})
test('should throw an error if value can not be transformed to time', (t) => {
t.plan(2)
const schema = {
title: 'a time in a string',
type: 'string',
format: 'time',
nullable: true
}
const toStringify = true
const validate = validator(schema)
const stringify = build(schema)
t.throws(() => stringify(toStringify), new Error('The value "true" cannot be converted to a time.'))
t.not(validate(toStringify))
})
test('should serialize also an invalid string value, even if it is not a valid time', (t) => {
t.plan(2)
const schema = {
title: 'a time in a string',
type: 'string',
format: 'time',
nullable: true
}
const toStringify = 'invalid'
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, JSON.stringify(toStringify))
t.not(validate(JSON.parse(output)), 'valid schema')
})

View File

@@ -0,0 +1,121 @@
'use strict'
const test = require('tap').test
const fjs = require('..')
const Ajv = require('ajv').default
const Validator = require('../lib/validator')
const Serializer = require('../lib/serializer')
function build (opts) {
return fjs({
title: 'default string',
type: 'object',
properties: {
firstName: {
type: 'string'
}
},
required: ['firstName']
}, opts)
}
test('activate debug mode', t => {
t.plan(5)
const debugMode = build({ debugMode: true })
t.type(debugMode, 'object')
t.ok(debugMode.ajv instanceof Ajv)
t.ok(debugMode.validator instanceof Validator)
t.ok(debugMode.serializer instanceof Serializer)
t.type(debugMode.code, 'string')
})
test('activate debug mode truthy', t => {
t.plan(5)
const debugMode = build({ debugMode: 'yes' })
t.type(debugMode, 'object')
t.type(debugMode.code, 'string')
t.ok(debugMode.ajv instanceof Ajv)
t.ok(debugMode.validator instanceof Validator)
t.ok(debugMode.serializer instanceof Serializer)
})
test('to string auto-consistent', t => {
t.plan(6)
const debugMode = build({ debugMode: 1 })
t.type(debugMode, 'object')
t.type(debugMode.code, 'string')
t.ok(debugMode.ajv instanceof Ajv)
t.ok(debugMode.serializer instanceof Serializer)
t.ok(debugMode.validator instanceof Validator)
const compiled = fjs.restore(debugMode)
const tobe = JSON.stringify({ firstName: 'Foo' })
t.same(compiled({ firstName: 'Foo', surname: 'bar' }), tobe, 'surname evicted')
})
test('to string auto-consistent with ajv', t => {
t.plan(6)
const debugMode = fjs({
title: 'object with multiple types field',
type: 'object',
properties: {
str: {
anyOf: [{
type: 'string'
}, {
type: 'boolean'
}]
}
}
}, { debugMode: 1 })
t.type(debugMode, 'object')
t.type(debugMode.code, 'string')
t.ok(debugMode.ajv instanceof Ajv)
t.ok(debugMode.validator instanceof Validator)
t.ok(debugMode.serializer instanceof Serializer)
const compiled = fjs.restore(debugMode)
const tobe = JSON.stringify({ str: 'Foo' })
t.same(compiled({ str: 'Foo', void: 'me' }), tobe)
})
test('to string auto-consistent with ajv-formats', t => {
t.plan(3)
const debugMode = fjs({
title: 'object with multiple types field and format keyword',
type: 'object',
properties: {
str: {
anyOf: [{
type: 'string',
format: 'email'
}, {
type: 'boolean'
}]
}
}
}, { debugMode: 1 })
t.type(debugMode, 'object')
const compiled = fjs.restore(debugMode)
const tobe = JSON.stringify({ str: 'foo@bar.com' })
t.same(compiled({ str: 'foo@bar.com' }), tobe)
t.throws(() => compiled({ str: 'foo' }))
})
test('debug should restore the same serializer instance', t => {
t.plan(1)
const debugMode = fjs({ type: 'integer' }, { debugMode: 1, rounding: 'ceil' })
const compiled = fjs.restore(debugMode)
t.same(compiled(3.95), 4)
})

View File

@@ -0,0 +1,376 @@
'use strict'
const test = require('tap').test
const build = require('..')
function buildTest (schema, toStringify, expected) {
test(`render a ${schema.title} with default as JSON`, (t) => {
t.plan(1)
const stringify = build(schema)
const output = stringify(toStringify)
t.equal(output, JSON.stringify(expected))
})
}
buildTest({
title: 'default string',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string',
default: 'Collina'
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 0
},
magic: {
type: 'number'
}
},
required: ['firstName', 'lastName']
}, {
firstName: 'Matteo',
magic: 42,
age: 32
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
magic: 42
})
buildTest({
title: 'default string with value',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string',
default: 'Collina'
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 0
},
magic: {
type: 'number'
}
},
required: ['firstName', 'lastName']
}, {
firstName: 'Matteo',
lastName: 'collina',
magic: 42,
age: 32
}, {
firstName: 'Matteo',
lastName: 'collina',
age: 32,
magic: 42
})
buildTest({
title: 'default number',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 0
},
magic: {
type: 'number',
default: 42
}
},
required: ['firstName', 'lastName']
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
magic: 42
})
buildTest({
title: 'default number with value',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 0
},
magic: {
type: 'number',
default: 42
}
},
required: ['firstName', 'lastName']
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
magic: 66
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
magic: 66
})
buildTest({
title: 'default object',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 0
},
otherProps: {
type: 'object',
default: { foo: 'bar' }
}
},
required: ['firstName', 'lastName']
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
otherProps: { foo: 'bar' }
})
buildTest({
title: 'default object with value',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 0
},
otherProps: {
type: 'object',
additionalProperties: true,
default: { foo: 'bar' }
}
},
required: ['firstName', 'lastName']
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
otherProps: { hello: 'world' }
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
otherProps: { hello: 'world' }
})
buildTest({
title: 'default array',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 0
},
otherProps: {
type: 'array',
items: { type: 'string' },
default: ['FOO']
}
},
required: ['firstName', 'lastName']
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
otherProps: ['FOO']
})
buildTest({
title: 'default array with value',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
age: {
description: 'Age in years',
type: 'integer',
minimum: 0
},
otherProps: {
type: 'array',
items: { type: 'string' },
default: ['FOO']
}
},
required: ['firstName', 'lastName']
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
otherProps: ['BAR']
}, {
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
otherProps: ['BAR']
})
buildTest({
title: 'default deeper value',
type: 'object',
properties: {
level1: {
type: 'object',
properties: {
level2: {
type: 'object',
properties: {
level3: {
type: 'object',
properties: {
level4: {
type: 'object',
default: { foo: 'bar' }
}
}
}
}
}
}
}
}
}, {
level1: { level2: { level3: { } } }
}, {
level1: { level2: { level3: { level4: { foo: 'bar' } } } }
})
buildTest({
title: 'default deeper value with value',
type: 'object',
properties: {
level1: {
type: 'object',
properties: {
level2: {
type: 'object',
properties: {
level3: {
type: 'object',
properties: {
level4: {
type: 'object',
default: { foo: 'bar' }
}
}
}
}
}
}
}
}
}, {
level1: { level2: { level3: { level4: { } } } }
}, {
level1: { level2: { level3: { level4: { } } } }
})
buildTest({
type: 'object',
properties: {
name: {
type: 'string',
default: 'foo'
},
dev: {
type: 'boolean',
default: false
}
},
required: [
'name', 'dev'
]
}, {}, { name: 'foo', dev: false })
buildTest({
type: 'object',
properties: {
name: {
type: 'string',
default: 'foo'
},
dev: {
type: 'boolean'
},
job: {
type: 'string',
default: 'awesome'
}
},
required: [
'name', 'dev'
]
}, { dev: true }, { name: 'foo', dev: true, job: 'awesome' })

View File

@@ -0,0 +1,37 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('use enum without type', (t) => {
t.plan(1)
const stringify = build({
title: 'Example Schema',
type: 'object',
properties: {
order: {
type: 'string',
enum: ['asc', 'desc']
}
}
})
const obj = { order: 'asc' }
t.equal('{"order":"asc"}', stringify(obj))
})
test('use enum without type', (t) => {
t.plan(1)
const stringify = build({
title: 'Example Schema',
type: 'object',
properties: {
order: {
enum: ['asc', 'desc']
}
}
})
const obj = { order: 'asc' }
t.equal('{"order":"asc"}', stringify(obj))
})

View File

@@ -0,0 +1,24 @@
'use strict'
const t = require('tap')
const fjs = require('..')
const schema = {
type: 'object',
properties: {
fullName: { type: 'string' },
phone: { type: 'number' }
}
}
const input = {
fullName: 'Jone',
phone: 'phone'
}
const render = fjs(schema)
try {
render(input)
} catch (err) {
t.equal(err.message, 'The value "phone" cannot be converted to a number.')
}

View File

View File

@@ -0,0 +1,470 @@
'use strict'
const t = require('tap')
const build = require('..')
process.env.TZ = 'UTC'
const schema = {
type: 'object',
properties: {
},
if: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['foobar'] }
}
},
then: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['foobar'] },
foo: { type: 'string' },
bar: { type: 'number' },
list: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'string' }
}
}
}
}
},
else: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['greeting'] },
hi: { type: 'string' },
hello: { type: 'number' },
list: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'string' }
}
}
}
}
}
}
const nestedIfSchema = {
type: 'object',
properties: { },
if: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['foobar', 'greeting'] }
}
},
then: {
if: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['foobar'] }
}
},
then: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['foobar'] },
foo: { type: 'string' },
bar: { type: 'number' },
list: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'string' }
}
}
}
}
},
else: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['greeting'] },
hi: { type: 'string' },
hello: { type: 'number' }
}
}
},
else: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['alphabet'] },
a: { type: 'string' },
b: { type: 'number' }
}
}
}
const nestedElseSchema = {
type: 'object',
properties: { },
if: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['foobar'] }
}
},
then: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['foobar'] },
foo: { type: 'string' },
bar: { type: 'number' },
list: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'string' }
}
}
}
}
},
else: {
if: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['greeting'] }
}
},
then: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['greeting'] },
hi: { type: 'string' },
hello: { type: 'number' }
}
},
else: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['alphabet'] },
a: { type: 'string' },
b: { type: 'number' }
}
}
}
}
const nestedDeepElseSchema = {
type: 'object',
additionalProperties: schema
}
const noElseSchema = {
type: 'object',
properties: {
},
if: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['foobar'] }
}
},
then: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['foobar'] },
foo: { type: 'string' },
bar: { type: 'number' },
list: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'string' }
}
}
}
}
}
}
const fooBarInput = {
kind: 'foobar',
foo: 'FOO',
list: [{
name: 'name',
value: 'foo'
}],
bar: 42,
hi: 'HI',
hello: 45,
a: 'A',
b: 35
}
const greetingInput = {
kind: 'greeting',
foo: 'FOO',
bar: 42,
hi: 'HI',
hello: 45,
a: 'A',
b: 35
}
const alphabetInput = {
kind: 'alphabet',
foo: 'FOO',
bar: 42,
hi: 'HI',
hello: 45,
a: 'A',
b: 35
}
const deepFoobarInput = {
foobar: fooBarInput
}
const foobarOutput = JSON.stringify({
kind: 'foobar',
foo: 'FOO',
bar: 42,
list: [{
name: 'name',
value: 'foo'
}]
})
const greetingOutput = JSON.stringify({
kind: 'greeting',
hi: 'HI',
hello: 45
})
const alphabetOutput = JSON.stringify({
kind: 'alphabet',
a: 'A',
b: 35
})
const deepFoobarOutput = JSON.stringify({
foobar: JSON.parse(foobarOutput)
})
const noElseGreetingOutput = JSON.stringify({})
t.test('if-then-else', t => {
const tests = [
{
name: 'foobar',
schema,
input: fooBarInput,
expected: foobarOutput
},
{
name: 'greeting',
schema,
input: greetingInput,
expected: greetingOutput
},
{
name: 'if nested - then then',
schema: nestedIfSchema,
input: fooBarInput,
expected: foobarOutput
},
{
name: 'if nested - then else',
schema: nestedIfSchema,
input: greetingInput,
expected: greetingOutput
},
{
name: 'if nested - else',
schema: nestedIfSchema,
input: alphabetInput,
expected: alphabetOutput
},
{
name: 'else nested - then',
schema: nestedElseSchema,
input: fooBarInput,
expected: foobarOutput
},
{
name: 'else nested - else then',
schema: nestedElseSchema,
input: greetingInput,
expected: greetingOutput
},
{
name: 'else nested - else else',
schema: nestedElseSchema,
input: alphabetInput,
expected: alphabetOutput
},
{
name: 'deep then - else',
schema: nestedDeepElseSchema,
input: deepFoobarInput,
expected: deepFoobarOutput
},
{
name: 'no else',
schema: noElseSchema,
input: greetingInput,
expected: noElseGreetingOutput
}
]
tests.forEach(test => {
t.test(test.name + ' - normal', t => {
t.plan(1)
const stringify = build(JSON.parse(JSON.stringify(test.schema)), { ajv: { strictTypes: false } })
const serialized = stringify(test.input)
t.equal(serialized, test.expected)
})
})
t.end()
})
t.test('nested if/then', t => {
t.plan(2)
const schema = {
type: 'object',
properties: { a: { type: 'string' } },
if: {
type: 'object',
properties: { foo: { type: 'string' } }
},
then: {
properties: { bar: { type: 'string' } },
if: {
type: 'object',
properties: { foo1: { type: 'string' } }
},
then: {
properties: { bar1: { type: 'string' } }
}
}
}
const stringify = build(schema)
t.equal(
stringify({ a: 'A', foo: 'foo', bar: 'bar' }),
JSON.stringify({ a: 'A', bar: 'bar' })
)
t.equal(
stringify({ a: 'A', foo: 'foo', bar: 'bar', foo1: 'foo1', bar1: 'bar1' }),
JSON.stringify({ a: 'A', bar: 'bar', bar1: 'bar1' })
)
})
t.test('if/else with string format', (t) => {
t.plan(2)
const schema = {
if: { type: 'string' },
then: { type: 'string', format: 'date' },
else: { const: 'Invalid' }
}
const stringify = build(schema)
const date = new Date(1674263005800)
t.equal(stringify(date), '"2023-01-21"')
t.equal(stringify('Invalid'), '"Invalid"')
})
t.test('if/else with const integers', (t) => {
t.plan(2)
const schema = {
type: 'number',
if: { type: 'number', minimum: 42 },
then: { const: 66 },
else: { const: 33 }
}
const stringify = build(schema)
t.equal(stringify(100.32), '66')
t.equal(stringify(10.12), '33')
})
t.test('if/else with array', (t) => {
t.plan(2)
const schema = {
type: 'array',
if: { type: 'array', maxItems: 1 },
then: { items: { type: 'string' } },
else: { items: { type: 'number' } }
}
const stringify = build(schema)
t.equal(stringify(['1']), JSON.stringify(['1']))
t.equal(stringify(['1', '2']), JSON.stringify([1, 2]))
})
t.test('external recursive if/then/else', (t) => {
t.plan(1)
const externalSchema = {
type: 'object',
properties: {
base: { type: 'string' },
self: { $ref: 'externalSchema#' }
},
if: {
type: 'object',
properties: {
foo: { type: 'string', const: '41' }
}
},
then: {
type: 'object',
properties: {
bar: { type: 'string', const: '42' }
}
},
else: {
type: 'object',
properties: {
baz: { type: 'string', const: '43' }
}
}
}
const schema = {
type: 'object',
properties: {
a: { $ref: 'externalSchema#/properties/self' },
b: { $ref: 'externalSchema#/properties/self' }
}
}
const data = {
a: {
base: 'a',
foo: '41',
bar: '42',
baz: '43',
ignore: 'ignored'
},
b: {
base: 'b',
foo: 'not-41',
bar: '42',
baz: '43',
ignore: 'ignored'
}
}
const stringify = build(schema, { schema: { externalSchema } })
t.equal(stringify(data), '{"a":{"base":"a","bar":"42"},"b":{"base":"b","baz":"43"}}')
})

View File

@@ -0,0 +1,92 @@
'use strict'
const test = require('tap').test
const validator = require('is-my-json-valid')
const build = require('..')
function buildTest (schema, toStringify) {
test(`render a ${schema.title} as JSON`, (t) => {
t.plan(3)
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(toStringify)
t.same(JSON.parse(output), toStringify)
t.equal(output, JSON.stringify(toStringify))
t.ok(validate(JSON.parse(output)), 'valid schema')
})
}
buildTest({
title: 'infer type object by keyword',
// 'type': 'object',
properties: {
name: {
type: 'string'
}
}
}, {
name: 'foo'
})
buildTest({
title: 'infer type of nested object by keyword',
// 'type': 'object',
properties: {
more: {
description: 'more properties',
// 'type': 'object',
properties: {
something: {
type: 'string'
}
}
}
}
}, {
more: {
something: 'else'
}
})
buildTest({
title: 'infer type array by keyword',
type: 'object',
properties: {
ids: {
// 'type': 'array',
items: {
type: 'string'
}
}
}
}, {
ids: ['test']
})
buildTest({
title: 'infer type string by keyword',
type: 'object',
properties: {
name: {
// 'type': 'string',
maxLength: 3
}
}
}, {
name: 'foo'
})
buildTest({
title: 'infer type number by keyword',
type: 'object',
properties: {
age: {
// 'type': 'number',
maximum: 18
}
}
}, {
age: 18
})

View File

@@ -0,0 +1,55 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('Finite numbers', t => {
const values = [-5, 0, -0, 1.33, 99, 100.0,
Math.E, Number.EPSILON,
Number.MAX_SAFE_INTEGER, Number.MAX_VALUE,
Number.MIN_SAFE_INTEGER, Number.MIN_VALUE]
t.plan(values.length)
const schema = {
type: 'number'
}
const stringify = build(schema)
values.forEach(v => t.equal(stringify(v), JSON.stringify(v)))
})
test('Infinite integers', t => {
const values = [Infinity, -Infinity]
t.plan(values.length)
const schema = {
type: 'integer'
}
const stringify = build(schema)
values.forEach(v => {
try {
stringify(v)
} catch (err) {
t.equal(err.message, `The value "${v}" cannot be converted to an integer.`)
}
})
})
test('Infinite numbers', t => {
const values = [Infinity, -Infinity]
t.plan(values.length)
const schema = {
type: 'number'
}
const stringify = build(schema)
values.forEach(v => t.equal(stringify(v), JSON.stringify(v)))
})

View File

@@ -0,0 +1,194 @@
'use strict'
const t = require('tap')
const test = t.test
const validator = require('is-my-json-valid')
const build = require('..')
const ROUNDING_TYPES = ['ceil', 'floor', 'round']
test('render an integer as JSON', (t) => {
t.plan(2)
const schema = {
title: 'integer',
type: 'integer'
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify(1615)
t.equal(output, '1615')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a float as an integer', (t) => {
t.plan(2)
try {
build({
title: 'float as integer',
type: 'integer'
}, { rounding: 'foobar' })
} catch (error) {
t.ok(error)
t.equal(error.message, 'Unsupported integer rounding method foobar')
}
})
test('throws on NaN', (t) => {
t.plan(1)
const schema = {
title: 'integer',
type: 'integer'
}
const stringify = build(schema)
t.throws(() => stringify(NaN), new Error('The value "NaN" cannot be converted to an integer.'))
})
test('render a float as an integer', (t) => {
const cases = [
{ input: Math.PI, output: '3' },
{ input: 5.0, output: '5' },
{ input: null, output: '0' },
{ input: 0, output: '0' },
{ input: 0.0, output: '0' },
{ input: 42, output: '42' },
{ input: 1.99999, output: '1' },
{ input: -45.05, output: '-45' },
{ input: 3333333333333333, output: '3333333333333333' },
{ input: Math.PI, output: '3', rounding: 'trunc' },
{ input: 5.0, output: '5', rounding: 'trunc' },
{ input: null, output: '0', rounding: 'trunc' },
{ input: 0, output: '0', rounding: 'trunc' },
{ input: 0.0, output: '0', rounding: 'trunc' },
{ input: 42, output: '42', rounding: 'trunc' },
{ input: 1.99999, output: '1', rounding: 'trunc' },
{ input: -45.05, output: '-45', rounding: 'trunc' },
{ input: 0.95, output: '1', rounding: 'ceil' },
{ input: 0.2, output: '1', rounding: 'ceil' },
{ input: 45.95, output: '45', rounding: 'floor' },
{ input: -45.05, output: '-46', rounding: 'floor' },
{ input: 45.44, output: '45', rounding: 'round' },
{ input: 45.95, output: '46', rounding: 'round' }
]
t.plan(cases.length * 2)
cases.forEach(checkInteger)
function checkInteger ({ input, output, rounding }) {
const schema = {
title: 'float as integer',
type: 'integer'
}
const validate = validator(schema)
const stringify = build(schema, { rounding })
const str = stringify(input)
t.equal(str, output)
t.ok(validate(JSON.parse(str)), 'valid schema')
}
})
test('render an object with an integer as JSON', (t) => {
t.plan(2)
const schema = {
title: 'object with integer',
type: 'object',
properties: {
id: {
type: 'integer'
}
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
id: 1615
})
t.equal(output, '{"id":1615}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render an array with an integer as JSON', (t) => {
t.plan(2)
const schema = {
title: 'array with integer',
type: 'array',
items: {
type: 'integer'
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify([1615])
t.equal(output, '[1615]')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render an object with an additionalProperty of type integer as JSON', (t) => {
t.plan(2)
const schema = {
title: 'object with integer',
type: 'object',
additionalProperties: {
type: 'integer'
}
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({
num: 1615
})
t.equal(output, '{"num":1615}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('should round integer object parameter', t => {
t.plan(2)
const schema = { type: 'object', properties: { magic: { type: 'integer' } } }
const validate = validator(schema)
const stringify = build(schema, { rounding: 'ceil' })
const output = stringify({ magic: 4.2 })
t.equal(output, '{"magic":5}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('should not stringify a property if it does not exist', t => {
t.plan(2)
const schema = { title: 'Example Schema', type: 'object', properties: { age: { type: 'integer' } } }
const validate = validator(schema)
const stringify = build(schema)
const output = stringify({})
t.equal(output, '{}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
ROUNDING_TYPES.forEach((rounding) => {
test(`should not stringify a property if it does not exist (rounding: ${rounding})`, t => {
t.plan(2)
const schema = { type: 'object', properties: { magic: { type: 'integer' } } }
const validate = validator(schema)
const stringify = build(schema, { rounding })
const output = stringify({})
t.equal(output, '{}')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
})

View File

@@ -0,0 +1,22 @@
'use strict'
const test = require('tap').test
const build = require('..')
// Covers issue #139
test('Should throw on invalid schema', t => {
t.plan(2)
try {
build({}, {
schema: {
invalid: {
type: 'Dinosaur'
}
}
})
t.fail('should be an invalid schema')
} catch (err) {
t.match(err.message, /^"invalid" schema is invalid:.*/, 'Schema contains invalid key')
t.ok(err)
}
})

View File

@@ -0,0 +1,57 @@
'use strict'
const { test } = require('tap')
const build = require('..')
test('should validate anyOf after allOf merge', (t) => {
t.plan(1)
const schema = {
$id: 'schema',
type: 'object',
allOf: [
{
$id: 'base',
type: 'object',
properties: {
name: {
type: 'string'
}
},
required: [
'name'
]
},
{
$id: 'inner_schema',
type: 'object',
properties: {
union: {
$id: '#id',
anyOf: [
{
$id: 'guid',
type: 'string'
},
{
$id: 'email',
type: 'string'
}
]
}
},
required: [
'union'
]
}
]
}
const stringify = build(schema)
t.equal(
stringify({ name: 'foo', union: 'a8f1cc50-5530-5c62-9109-5ba9589a6ae1' }),
'{"name":"foo","union":"a8f1cc50-5530-5c62-9109-5ba9589a6ae1"}')
})

View File

@@ -0,0 +1,10 @@
# JSON-Schema-Test-Suite
You can find all test cases [here](https://github.com/json-schema-org/JSON-Schema-Test-Suite).
It contains a set of JSON objects that implementors of JSON Schema validation libraries can use to test their validators.
# How to add another test case?
1. Navigate to [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite/tree/master/tests)
2. Choose a draft `draft4`, `draft6` or `draft7`
3. Copy & paste the `test-case.json` to the project and add a test like in the `draft4.test.js`

View File

@@ -0,0 +1,12 @@
'use strict'
const test = require('tap').test
const { counTests, runTests } = require('./util')
const requiredTestSuite = require('./draft4/required.json')
test('required', (t) => {
const skippedTests = ['ignores arrays', 'ignores strings', 'ignores other non-objects']
t.plan(counTests(requiredTestSuite, skippedTests))
runTests(t, requiredTestSuite, skippedTests)
})

View File

@@ -0,0 +1,54 @@
[
{
"description": "required validation",
"schema": {
"properties": {
"foo": {},
"bar": {}
},
"required": ["foo"]
},
"tests": [
{
"description": "present required property is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "non-present required property is invalid",
"data": {"bar": 1},
"valid": false
},
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores strings",
"data": "",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "required default validation",
"schema": {
"properties": {
"foo": {}
}
},
"tests": [
{
"description": "not required by default",
"data": {},
"valid": true
}
]
}
]

View File

@@ -0,0 +1,12 @@
'use strict'
const test = require('tap').test
const { counTests, runTests } = require('./util')
const requiredTestSuite = require('./draft6/required.json')
test('required', (t) => {
const skippedTests = ['ignores arrays', 'ignores strings', 'ignores other non-objects']
t.plan(counTests(requiredTestSuite, skippedTests))
runTests(t, requiredTestSuite, skippedTests)
})

View File

@@ -0,0 +1,70 @@
[
{
"description": "required validation",
"schema": {
"properties": {
"foo": {},
"bar": {}
},
"required": ["foo"]
},
"tests": [
{
"description": "present required property is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "non-present required property is invalid",
"data": {"bar": 1},
"valid": false
},
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores strings",
"data": "",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "required default validation",
"schema": {
"properties": {
"foo": {}
}
},
"tests": [
{
"description": "not required by default",
"data": {},
"valid": true
}
]
},
{
"description": "required with empty array",
"schema": {
"properties": {
"foo": {}
},
"required": []
},
"tests": [
{
"description": "property not required",
"data": {},
"valid": true
}
]
}
]

View File

@@ -0,0 +1,12 @@
'use strict'
const test = require('tap').test
const { counTests, runTests } = require('./util')
const requiredTestSuite = require('./draft7/required.json')
test('required', (t) => {
const skippedTests = ['ignores arrays', 'ignores strings', 'ignores other non-objects']
t.plan(counTests(requiredTestSuite, skippedTests))
runTests(t, requiredTestSuite, skippedTests)
})

View File

@@ -0,0 +1,70 @@
[
{
"description": "required validation",
"schema": {
"properties": {
"foo": {},
"bar": {}
},
"required": ["foo"]
},
"tests": [
{
"description": "present required property is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "non-present required property is invalid",
"data": {"bar": 1},
"valid": false
},
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores strings",
"data": "",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "required default validation",
"schema": {
"properties": {
"foo": {}
}
},
"tests": [
{
"description": "not required by default",
"data": {},
"valid": true
}
]
},
{
"description": "required with empty array",
"schema": {
"properties": {
"foo": {}
},
"required": []
},
"tests": [
{
"description": "property not required",
"data": {},
"valid": true
}
]
}
]

View File

@@ -0,0 +1,34 @@
'use strict'
const build = require('../..')
function runTests (t, testsuite, skippedTests) {
for (const scenario of testsuite) {
const stringify = build(scenario.schema)
for (const test of scenario.tests) {
if (skippedTests.indexOf(test.description) !== -1) {
t.comment('skip %s', test.description)
continue
}
t.test(test.description, (t) => {
t.plan(1)
try {
const output = stringify(test.data)
t.equal(output, JSON.stringify(test.data), 'compare payloads')
} catch (err) {
if (test.valid === false) {
t.pass('payload is invalid')
} else {
t.fail('payload should be valid: ' + err.message)
}
}
})
}
}
}
function counTests (ts, skippedTests) {
return ts.reduce((a, b) => a + b.tests.length, 0) - skippedTests.length
}
module.exports = { runTests, counTests }

View File

@@ -0,0 +1,88 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('missing values', (t) => {
t.plan(3)
const stringify = build({
title: 'object with missing values',
type: 'object',
properties: {
str: {
type: 'string'
},
num: {
type: 'number'
},
val: {
type: 'string'
}
}
})
t.equal('{"val":"value"}', stringify({ val: 'value' }))
t.equal('{"str":"string","val":"value"}', stringify({ str: 'string', val: 'value' }))
t.equal('{"str":"string","num":42,"val":"value"}', stringify({ str: 'string', num: 42, val: 'value' }))
})
test('handle null when value should be string', (t) => {
t.plan(1)
const stringify = build({
type: 'object',
properties: {
str: {
type: 'string'
}
}
})
t.equal('{"str":""}', stringify({ str: null }))
})
test('handle null when value should be integer', (t) => {
t.plan(1)
const stringify = build({
type: 'object',
properties: {
int: {
type: 'integer'
}
}
})
t.equal('{"int":0}', stringify({ int: null }))
})
test('handle null when value should be number', (t) => {
t.plan(1)
const stringify = build({
type: 'object',
properties: {
num: {
type: 'number'
}
}
})
t.equal('{"num":0}', stringify({ num: null }))
})
test('handle null when value should be boolean', (t) => {
t.plan(1)
const stringify = build({
type: 'object',
properties: {
bool: {
type: 'boolean'
}
}
})
t.equal('{"bool":false}', stringify({ bool: null }))
})

View File

@@ -0,0 +1,19 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('should throw a TypeError with the path to the key of the invalid value', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
num: {
type: ['number']
}
}
}
const stringify = build(schema)
t.throws(() => stringify({ num: { bla: 123 } }), new TypeError('The value of \'#/properties/num\' does not match schema definition.'))
})

View File

@@ -0,0 +1,63 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('nested objects with same properties', (t) => {
t.plan(1)
const schema = {
title: 'nested objects with same properties',
type: 'object',
properties: {
stringProperty: {
type: 'string'
},
objectProperty: {
type: 'object',
additionalProperties: true
}
}
}
const stringify = build(schema)
const value = stringify({
stringProperty: 'string1',
objectProperty: {
stringProperty: 'string2',
numberProperty: 42
}
})
t.equal(value, '{"stringProperty":"string1","objectProperty":{"stringProperty":"string2","numberProperty":42}}')
})
test('names collision', (t) => {
t.plan(1)
const schema = {
title: 'nested objects with same properties',
type: 'object',
properties: {
test: {
type: 'object',
properties: {
a: { type: 'string' }
}
},
tes: {
type: 'object',
properties: {
b: { type: 'string' },
t: { type: 'object' }
}
}
}
}
const stringify = build(schema)
const data = {
test: { a: 'a' },
tes: { b: 'b', t: {} }
}
t.equal(stringify(data), JSON.stringify(data))
})

View File

@@ -0,0 +1,543 @@
'use strict'
const test = require('tap').test
const build = require('..')
const nullable = true
const complexObject = {
type: 'object',
properties: {
nullableString: { type: 'string', nullable },
nullableNumber: { type: 'number', nullable },
nullableInteger: { type: 'integer', nullable },
nullableBoolean: { type: 'boolean', nullable },
nullableNull: { type: 'null', nullable },
nullableArray: {
type: 'array',
nullable: true,
items: {}
},
nullableObject: { type: 'object', nullable: true },
objectWithNullableProps: {
type: 'object',
nullable: false,
additionalProperties: true,
properties: {
nullableString: { type: 'string', nullable },
nullableNumber: { type: 'number', nullable },
nullableInteger: { type: 'integer', nullable },
nullableBoolean: { type: 'boolean', nullable },
nullableNull: { type: 'null', nullable },
nullableArray: {
type: 'array',
nullable: true,
items: {}
}
}
},
arrayWithNullableItems: {
type: 'array',
nullable: true,
items: { type: ['integer', 'string'], nullable: true }
}
}
}
const complexData = {
nullableString: null,
nullableNumber: null,
nullableInteger: null,
nullableBoolean: null,
nullableNull: null,
nullableArray: null,
nullableObject: null,
objectWithNullableProps: {
additionalProp: null,
nullableString: null,
nullableNumber: null,
nullableInteger: null,
nullableBoolean: null,
nullableNull: null,
nullableArray: null
},
arrayWithNullableItems: [1, 2, null]
}
const complexExpectedResult = {
nullableString: null,
nullableNumber: null,
nullableInteger: null,
nullableBoolean: null,
nullableNull: null,
nullableArray: null,
nullableObject: null,
objectWithNullableProps: {
additionalProp: null,
nullableString: null,
nullableNumber: null,
nullableInteger: null,
nullableBoolean: null,
nullableNull: null,
nullableArray: null
},
arrayWithNullableItems: [1, 2, null]
}
const testSet = {
nullableString: [{ type: 'string', nullable }, null, null],
nullableNumber: [{ type: 'number', nullable }, null, null],
nullableInteger: [{ type: 'integer', nullable }, null, null],
nullableBoolean: [{ type: 'boolean', nullable }, null, null],
nullableNull: [{ type: 'null', nullable }, null, null],
nullableArray: [{
type: 'array',
nullable: true,
items: {}
}, null, null],
nullableObject: [{ type: 'object', nullable: true }, null, null],
complexObject: [complexObject, complexData, complexExpectedResult, { ajv: { allowUnionTypes: true } }]
}
Object.keys(testSet).forEach(key => {
test(`handle nullable:true in ${key} correctly`, (t) => {
t.plan(1)
const [
schema,
data,
expected,
extraOptions
] = testSet[key]
const stringifier = build(schema, extraOptions)
const result = stringifier(data)
t.same(JSON.parse(result), expected)
})
})
test('handle nullable number correctly', (t) => {
t.plan(2)
const schema = {
type: 'number',
nullable: true
}
const stringify = build(schema)
const data = null
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('handle nullable integer correctly', (t) => {
t.plan(2)
const schema = {
type: 'integer',
nullable: true
}
const stringify = build(schema)
const data = null
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('handle nullable boolean correctly', (t) => {
t.plan(2)
const schema = {
type: 'boolean',
nullable: true
}
const stringify = build(schema)
const data = null
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('handle nullable string correctly', (t) => {
t.plan(2)
const schema = {
type: 'string',
nullable: true
}
const stringify = build(schema)
const data = null
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('handle nullable date-time correctly', (t) => {
t.plan(2)
const schema = {
type: 'string',
format: 'date-time',
nullable: true
}
const stringify = build(schema)
const data = null
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('handle nullable date correctly', (t) => {
t.plan(2)
const schema = {
type: 'string',
format: 'date',
nullable: true
}
const stringify = build(schema)
const data = null
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('handle nullable time correctly', (t) => {
t.plan(2)
const schema = {
type: 'string',
format: 'time',
nullable: true
}
const stringify = build(schema)
const data = null
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('large array of nullable strings with default mechanism', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
ids: {
type: 'array',
items: {
type: 'string',
nullable: true
}
}
}
}
const options = {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
}
const stringify = build(schema, options)
const data = { ids: new Array(2e4).fill(null) }
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('large array of nullable date-time strings with default mechanism', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
ids: {
type: 'array',
items: {
type: 'string',
format: 'date-time',
nullable: true
}
}
}
}
const options = {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
}
const stringify = build(schema, options)
const data = { ids: new Array(2e4).fill(null) }
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('large array of nullable date-time strings with default mechanism', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
ids: {
type: 'array',
items: {
type: 'string',
format: 'date',
nullable: true
}
}
}
}
const options = {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
}
const stringify = build(schema, options)
const data = { ids: new Array(2e4).fill(null) }
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('large array of nullable date-time strings with default mechanism', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
ids: {
type: 'array',
items: {
type: 'string',
format: 'time',
nullable: true
}
}
}
}
const options = {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
}
const stringify = build(schema, options)
const data = { ids: new Array(2e4).fill(null) }
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('large array of nullable numbers with default mechanism', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
ids: {
type: 'array',
items: {
type: 'number',
nullable: true
}
}
}
}
const options = {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
}
const stringify = build(schema, options)
const data = { ids: new Array(2e4).fill(null) }
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('large array of nullable integers with default mechanism', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
ids: {
type: 'array',
items: {
type: 'integer',
nullable: true
}
}
}
}
const options = {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
}
const stringify = build(schema, options)
const data = { ids: new Array(2e4).fill(null) }
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('large array of nullable booleans with default mechanism', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
ids: {
type: 'array',
items: {
type: 'boolean',
nullable: true
}
}
}
}
const options = {
largeArraySize: 2e4,
largeArrayMechanism: 'default'
}
const stringify = build(schema, options)
const data = { ids: new Array(2e4).fill(null) }
const result = stringify(data)
t.same(result, JSON.stringify(data))
t.same(JSON.parse(result), data)
})
test('nullable type in the schema', (t) => {
t.plan(2)
const schema = {
type: ['object', 'null'],
properties: {
foo: {
type: 'string'
}
}
}
const stringify = build(schema)
const data = { foo: 'bar' }
t.same(stringify(data), JSON.stringify(data))
t.same(stringify(null), JSON.stringify(null))
})
test('throw an error if the value doesn\'t match the type', (t) => {
t.plan(2)
const schema = {
type: 'object',
additionalProperties: false,
required: ['data'],
properties: {
data: {
type: 'array',
minItems: 1,
items: {
oneOf: [
{
type: 'string'
},
{
type: 'number'
}
]
}
}
}
}
const stringify = build(schema)
const validData = { data: [1, 'testing'] }
t.equal(stringify(validData), JSON.stringify(validData))
const invalidData = { data: [false, 'testing'] }
t.throws(() => stringify(invalidData))
})
test('nullable value in oneOf', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
data: {
oneOf: [
{
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer', minimum: 1 }
},
additionalProperties: false,
required: ['id']
}
},
{
type: 'array',
items: {
type: 'object',
properties: {
job: { type: 'string', nullable: true }
},
additionalProperties: false,
required: ['job']
}
}
]
}
},
required: ['data'],
additionalProperties: false
}
const stringify = build(schema)
const data = { data: [{ job: null }] }
t.equal(stringify(data), JSON.stringify(data))
})

View File

@@ -0,0 +1,492 @@
'use strict'
const { test } = require('tap')
const build = require('..')
test('object with multiple types field', (t) => {
t.plan(2)
const schema = {
title: 'object with multiple types field',
type: 'object',
properties: {
str: {
oneOf: [{
type: 'string'
}, {
type: 'boolean'
}]
}
}
}
const stringify = build(schema)
t.equal(stringify({ str: 'string' }), '{"str":"string"}')
t.equal(stringify({ str: true }), '{"str":true}')
})
test('object with field of type object or null', (t) => {
t.plan(2)
const schema = {
title: 'object with field of type object or null',
type: 'object',
properties: {
prop: {
oneOf: [{
type: 'object',
properties: {
str: {
type: 'string'
}
}
}, {
type: 'null'
}]
}
}
}
const stringify = build(schema)
t.equal(stringify({ prop: null }), '{"prop":null}')
t.equal(stringify({
prop: {
str: 'string', remove: 'this'
}
}), '{"prop":{"str":"string"}}')
})
test('object with field of type object or array', (t) => {
t.plan(2)
const schema = {
title: 'object with field of type object or array',
type: 'object',
properties: {
prop: {
oneOf: [{
type: 'object',
properties: {},
additionalProperties: true
}, {
type: 'array',
items: {
type: 'string'
}
}]
}
}
}
const stringify = build(schema)
t.equal(stringify({
prop: { str: 'string' }
}), '{"prop":{"str":"string"}}')
t.equal(stringify({
prop: ['string']
}), '{"prop":["string"]}')
})
test('object with field of type string and coercion disable ', (t) => {
t.plan(1)
const schema = {
title: 'object with field of type string',
type: 'object',
properties: {
str: {
oneOf: [{
type: 'string'
}]
}
}
}
const stringify = build(schema)
t.throws(() => stringify({ str: 1 }))
})
test('object with field of type string and coercion enable ', (t) => {
t.plan(1)
const schema = {
title: 'object with field of type string',
type: 'object',
properties: {
str: {
oneOf: [{
type: 'string'
}]
}
}
}
const options = {
ajv: {
coerceTypes: true
}
}
const stringify = build(schema, options)
const value = stringify({
str: 1
})
t.equal(value, '{"str":"1"}')
})
test('object with field with type union of multiple objects', (t) => {
t.plan(2)
const schema = {
title: 'object with oneOf property value containing objects',
type: 'object',
properties: {
oneOfSchema: {
oneOf: [
{
type: 'object',
properties: {
baz: { type: 'number' }
},
required: ['baz']
},
{
type: 'object',
properties: {
bar: { type: 'string' }
},
required: ['bar']
}
]
}
},
required: ['oneOfSchema']
}
const stringify = build(schema)
t.equal(stringify({ oneOfSchema: { baz: 5 } }), '{"oneOfSchema":{"baz":5}}')
t.equal(stringify({ oneOfSchema: { bar: 'foo' } }), '{"oneOfSchema":{"bar":"foo"}}')
})
test('null value in schema', (t) => {
t.plan(0)
const schema = {
title: 'schema with null child',
type: 'string',
nullable: true,
enum: [null]
}
build(schema)
})
test('oneOf and $ref together', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
cs: {
oneOf: [
{
$ref: '#/definitions/Option'
},
{
type: 'boolean'
}
]
}
},
definitions: {
Option: {
type: 'string'
}
}
}
const stringify = build(schema)
t.equal(stringify({ cs: 'franco' }), '{"cs":"franco"}')
t.equal(stringify({ cs: true }), '{"cs":true}')
})
test('oneOf and $ref: 2 levels are fine', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
cs: {
oneOf: [
{
$ref: '#/definitions/Option'
},
{
type: 'boolean'
}
]
}
},
definitions: {
Option: {
oneOf: [
{
type: 'number'
},
{
type: 'boolean'
}
]
}
}
}
const stringify = build(schema)
const value = stringify({
cs: 3
})
t.equal(value, '{"cs":3}')
})
test('oneOf and $ref: multiple levels should throw at build.', (t) => {
t.plan(3)
const schema = {
type: 'object',
properties: {
cs: {
oneOf: [
{
$ref: '#/definitions/Option'
},
{
type: 'boolean'
}
]
}
},
definitions: {
Option: {
oneOf: [
{
$ref: '#/definitions/Option2'
},
{
type: 'string'
}
]
},
Option2: {
type: 'number'
}
}
}
const stringify = build(schema)
t.equal(stringify({ cs: 3 }), '{"cs":3}')
t.equal(stringify({ cs: true }), '{"cs":true}')
t.equal(stringify({ cs: 'pippo' }), '{"cs":"pippo"}')
})
test('oneOf and $ref - multiple external $ref', (t) => {
t.plan(2)
const externalSchema = {
external: {
definitions: {
def: {
type: 'object',
properties: {
prop: { oneOf: [{ $ref: 'external2#/definitions/other' }] }
}
}
}
},
external2: {
definitions: {
internal: {
type: 'string'
},
other: {
type: 'object',
properties: {
prop2: { $ref: '#/definitions/internal' }
}
}
}
}
}
const schema = {
title: 'object with $ref',
type: 'object',
properties: {
obj: {
$ref: 'external#/definitions/def'
}
}
}
const object = {
obj: {
prop: {
prop2: 'test'
}
}
}
const stringify = build(schema, { schema: externalSchema })
const output = stringify(object)
JSON.parse(output)
t.pass()
t.equal(output, '{"obj":{"prop":{"prop2":"test"}}}')
})
test('oneOf with enum with more than 100 entries', (t) => {
t.plan(1)
const schema = {
title: 'type array that may have one of declared items',
type: 'array',
items: {
oneOf: [
{
type: 'string',
enum: ['EUR', 'USD', ...(new Set([...new Array(200)].map(() => Math.random().toString(36).substr(2, 3)))).values()]
},
{ type: 'null' }
]
}
}
const stringify = build(schema)
const value = stringify(['EUR', 'USD', null])
t.equal(value, '["EUR","USD",null]')
})
test('oneOf object with field of type string with format or null', (t) => {
t.plan(1)
const toStringify = new Date()
const withOneOfSchema = {
type: 'object',
properties: {
prop: {
oneOf: [{
type: 'string',
format: 'date-time'
}, {
type: 'null'
}]
}
}
}
const withOneOfStringify = build(withOneOfSchema)
t.equal(withOneOfStringify({
prop: toStringify
}), `{"prop":"${toStringify.toISOString()}"}`)
})
test('one array item match oneOf types', (t) => {
t.plan(3)
const schema = {
type: 'object',
additionalProperties: false,
required: ['data'],
properties: {
data: {
type: 'array',
minItems: 1,
items: {
oneOf: [
{
type: 'string'
},
{
type: 'number'
}
]
}
}
}
}
const stringify = build(schema)
t.equal(stringify({ data: ['foo'] }), '{"data":["foo"]}')
t.equal(stringify({ data: [1] }), '{"data":[1]}')
t.throws(() => stringify({ data: [false, 'foo'] }))
})
test('some array items match oneOf types', (t) => {
t.plan(2)
const schema = {
type: 'object',
additionalProperties: false,
required: ['data'],
properties: {
data: {
type: 'array',
minItems: 1,
items: {
oneOf: [
{
type: 'string'
},
{
type: 'number'
}
]
}
}
}
}
const stringify = build(schema)
t.equal(stringify({ data: ['foo', 5] }), '{"data":["foo",5]}')
t.throws(() => stringify({ data: [false, 'foo', true, 5] }))
})
test('all array items does not match oneOf types', (t) => {
t.plan(1)
const schema = {
type: 'object',
additionalProperties: false,
required: ['data'],
properties: {
data: {
type: 'array',
minItems: 1,
items: {
oneOf: [
{
type: 'string'
},
{
type: 'number'
}
]
}
}
}
}
const stringify = build(schema)
t.throws(() => stringify({ data: [null, false, true, undefined, [], {}] }))
})

View File

@@ -0,0 +1,168 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('patternProperties', (t) => {
t.plan(1)
const stringify = build({
title: 'patternProperties',
type: 'object',
properties: {
str: {
type: 'string'
}
},
patternProperties: {
foo: {
type: 'string'
}
}
})
const obj = { str: 'test', foo: 42, ofoo: true, foof: 'string', objfoo: { a: true }, notMe: false }
t.equal(stringify(obj), '{"str":"test","foo":"42","ofoo":"true","foof":"string","objfoo":"[object Object]"}')
})
test('patternProperties should not change properties', (t) => {
t.plan(1)
const stringify = build({
title: 'patternProperties should not change properties',
type: 'object',
properties: {
foo: {
type: 'string'
}
},
patternProperties: {
foo: {
type: 'number'
}
}
})
const obj = { foo: '42', ofoo: 42 }
t.equal(stringify(obj), '{"foo":"42","ofoo":42}')
})
test('patternProperties - string coerce', (t) => {
t.plan(1)
const stringify = build({
title: 'check string coerce',
type: 'object',
properties: {},
patternProperties: {
foo: {
type: 'string'
}
}
})
const obj = { foo: true, ofoo: 42, arrfoo: ['array', 'test'], objfoo: { a: 'world' } }
t.equal(stringify(obj), '{"foo":"true","ofoo":"42","arrfoo":"array,test","objfoo":"[object Object]"}')
})
test('patternProperties - number coerce', (t) => {
t.plan(2)
const stringify = build({
title: 'check number coerce',
type: 'object',
properties: {},
patternProperties: {
foo: {
type: 'number'
}
}
})
const coercibleValues = { foo: true, ofoo: '42' }
t.equal(stringify(coercibleValues), '{"foo":1,"ofoo":42}')
const incoercibleValues = { xfoo: 'string', arrfoo: [1, 2], objfoo: { num: 42 } }
try {
stringify(incoercibleValues)
t.fail('should throw an error')
} catch (err) {
t.ok(err)
}
})
test('patternProperties - boolean coerce', (t) => {
t.plan(1)
const stringify = build({
title: 'check boolean coerce',
type: 'object',
properties: {},
patternProperties: {
foo: {
type: 'boolean'
}
}
})
const obj = { foo: 'true', ofoo: 0, arrfoo: [1, 2], objfoo: { a: true } }
t.equal(stringify(obj), '{"foo":true,"ofoo":false,"arrfoo":true,"objfoo":true}')
})
test('patternProperties - object coerce', (t) => {
t.plan(1)
const stringify = build({
title: 'check object coerce',
type: 'object',
properties: {},
patternProperties: {
foo: {
type: 'object',
properties: {
answer: {
type: 'number'
}
}
}
}
})
const obj = { objfoo: { answer: 42 } }
t.equal(stringify(obj), '{"objfoo":{"answer":42}}')
})
test('patternProperties - array coerce', (t) => {
t.plan(2)
const stringify = build({
title: 'check array coerce',
type: 'object',
properties: {},
patternProperties: {
foo: {
type: 'array',
items: {
type: 'string'
}
}
}
})
const coercibleValues = { arrfoo: [1, 2] }
t.equal(stringify(coercibleValues), '{"arrfoo":["1","2"]}')
const incoercibleValues = { foo: 'true', ofoo: 0, objfoo: { tyrion: 'lannister' } }
t.throws(() => stringify(incoercibleValues))
})
test('patternProperties - fail on invalid regex, handled by ajv', (t) => {
t.plan(1)
t.throws(() => build({
title: 'check array coerce',
type: 'object',
properties: {},
patternProperties: {
'foo/\\': {
type: 'array',
items: {
type: 'string'
}
}
}
}), new Error('schema is invalid: data/patternProperties must match format "regex"'))
})

View File

@@ -0,0 +1,245 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('can stringify recursive directory tree (issue #181)', (t) => {
t.plan(1)
const schema = {
definitions: {
directory: {
type: 'object',
properties: {
name: { type: 'string' },
subDirectories: {
type: 'array',
items: { $ref: '#/definitions/directory' },
default: []
}
}
}
},
type: 'array',
items: { $ref: '#/definitions/directory' }
}
const stringify = build(schema)
t.equal(stringify([
{ name: 'directory 1', subDirectories: [] },
{
name: 'directory 2',
subDirectories: [
{ name: 'directory 2.1', subDirectories: [] },
{ name: 'directory 2.2', subDirectories: [] }
]
}
]), '[{"name":"directory 1","subDirectories":[]},{"name":"directory 2","subDirectories":[{"name":"directory 2.1","subDirectories":[]},{"name":"directory 2.2","subDirectories":[]}]}]')
})
test('can stringify when recursion in external schema', t => {
t.plan(1)
const referenceSchema = {
$id: 'person',
type: 'object',
properties: {
name: { type: 'string' },
children: {
type: 'array',
items: { $ref: '#' }
}
}
}
const schema = {
$id: 'mainSchema',
type: 'object',
properties: {
people: {
$ref: 'person'
}
}
}
const stringify = build(schema, {
schema: {
[referenceSchema.$id]: referenceSchema
}
})
const value = stringify({ people: { name: 'Elizabeth', children: [{ name: 'Charles' }] } })
t.equal(value, '{"people":{"name":"Elizabeth","children":[{"name":"Charles"}]}}')
})
test('use proper serialize function', t => {
t.plan(1)
const personSchema = {
$id: 'person',
type: 'object',
properties: {
name: { type: 'string' },
children: {
type: 'array',
items: { $ref: '#' }
}
}
}
const directorySchema = {
$id: 'directory',
type: 'object',
properties: {
name: { type: 'string' },
subDirectories: {
type: 'array',
items: { $ref: '#' },
default: []
}
}
}
const schema = {
$id: 'mainSchema',
type: 'object',
properties: {
people: { $ref: 'person' },
directory: { $ref: 'directory' }
}
}
const stringify = build(schema, {
schema: {
[personSchema.$id]: personSchema,
[directorySchema.$id]: directorySchema
}
})
const value = stringify({
people: {
name: 'Elizabeth',
children: [{
name: 'Charles',
children: [{ name: 'William', children: [{ name: 'George' }, { name: 'Charlotte' }] }, { name: 'Harry' }]
}]
},
directory: {
name: 'directory 1',
subDirectories: [
{ name: 'directory 1.1', subDirectories: [] },
{
name: 'directory 1.2',
subDirectories: [{ name: 'directory 1.2.1' }, { name: 'directory 1.2.2' }]
}
]
}
})
t.equal(value, '{"people":{"name":"Elizabeth","children":[{"name":"Charles","children":[{"name":"William","children":[{"name":"George"},{"name":"Charlotte"}]},{"name":"Harry"}]}]},"directory":{"name":"directory 1","subDirectories":[{"name":"directory 1.1","subDirectories":[]},{"name":"directory 1.2","subDirectories":[{"name":"directory 1.2.1","subDirectories":[]},{"name":"directory 1.2.2","subDirectories":[]}]}]}}')
})
test('can stringify recursive references in object types (issue #365)', t => {
t.plan(1)
const schema = {
type: 'object',
definitions: {
parentCategory: {
type: 'object',
properties: {
parent: {
$ref: '#/definitions/parentCategory'
}
}
}
},
properties: {
category: {
type: 'object',
properties: {
parent: {
$ref: '#/definitions/parentCategory'
}
}
}
}
}
const stringify = build(schema)
const data = {
category: {
parent: {
parent: {
parent: {
parent: {}
}
}
}
}
}
const value = stringify(data)
t.equal(value, '{"category":{"parent":{"parent":{"parent":{"parent":{}}}}}}')
})
test('can stringify recursive inline $id references (issue #410)', t => {
t.plan(1)
const schema = {
$id: 'Node',
type: 'object',
properties: {
id: {
type: 'string'
},
nodes: {
type: 'array',
items: {
$ref: 'Node'
}
}
},
required: [
'id',
'nodes'
]
}
const stringify = build(schema)
const data = {
id: '0',
nodes: [
{
id: '1',
nodes: [{
id: '2',
nodes: [
{ id: '3', nodes: [] },
{ id: '4', nodes: [] },
{ id: '5', nodes: [] }
]
}]
},
{
id: '6',
nodes: [{
id: '7',
nodes: [
{ id: '8', nodes: [] },
{ id: '9', nodes: [] },
{ id: '10', nodes: [] }
]
}]
},
{
id: '11',
nodes: [{
id: '12',
nodes: [
{ id: '13', nodes: [] },
{ id: '14', nodes: [] },
{ id: '15', nodes: [] }
]
}]
}
]
}
const value = stringify(data)
t.equal(value, '{"id":"0","nodes":[{"id":"1","nodes":[{"id":"2","nodes":[{"id":"3","nodes":[]},{"id":"4","nodes":[]},{"id":"5","nodes":[]}]}]},{"id":"6","nodes":[{"id":"7","nodes":[{"id":"8","nodes":[]},{"id":"9","nodes":[]},{"id":"10","nodes":[]}]}]},{"id":"11","nodes":[{"id":"12","nodes":[{"id":"13","nodes":[]},{"id":"14","nodes":[]},{"id":"15","nodes":[]}]}]}]}')
})

12
backend/node_modules/fast-json-stringify/test/ref.json generated vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"definitions": {
"def": {
"type": "object",
"properties": {
"str": {
"type": "string"
}
}
}
}
}

2130
backend/node_modules/fast-json-stringify/test/ref.test.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
'use strict'
const test = require('tap').test
const validator = require('is-my-json-valid')
const build = require('..')
test('object with RexExp', (t) => {
t.plan(3)
const schema = {
title: 'object with RegExp',
type: 'object',
properties: {
reg: {
type: 'string'
}
}
}
const obj = {
reg: /"([^"]|\\")*"/
}
const stringify = build(schema)
const validate = validator(schema)
const output = stringify(obj)
JSON.parse(output)
t.pass()
t.equal(obj.reg.source, new RegExp(JSON.parse(output).reg).source)
t.ok(validate(JSON.parse(output)), 'valid schema')
})

View File

@@ -0,0 +1,239 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('object with required field', (t) => {
t.plan(3)
const schema = {
title: 'object with required field',
type: 'object',
properties: {
str: {
type: 'string'
},
num: {
type: 'integer'
}
},
required: ['str']
}
const stringify = build(schema)
stringify({
str: 'string'
})
t.pass()
try {
stringify({
num: 42
})
t.fail()
} catch (e) {
t.equal(e.message, '"str" is required!')
t.pass()
}
})
test('object with required field not in properties schema', (t) => {
t.plan(4)
const schema = {
title: 'object with required field',
type: 'object',
properties: {
num: {
type: 'integer'
}
},
required: ['str']
}
const stringify = build(schema)
try {
stringify({})
t.fail()
} catch (e) {
t.equal(e.message, '"str" is required!')
t.pass()
}
try {
stringify({
num: 42
})
t.fail()
} catch (e) {
t.equal(e.message, '"str" is required!')
t.pass()
}
})
test('object with required field not in properties schema with additional properties true', (t) => {
t.plan(4)
const schema = {
title: 'object with required field',
type: 'object',
properties: {
num: {
type: 'integer'
}
},
additionalProperties: true,
required: ['str']
}
const stringify = build(schema)
try {
stringify({})
t.fail()
} catch (e) {
t.equal(e.message, '"str" is required!')
t.pass()
}
try {
stringify({
num: 42
})
t.fail()
} catch (e) {
t.equal(e.message, '"str" is required!')
t.pass()
}
})
test('object with multiple required field not in properties schema', (t) => {
t.plan(6)
const schema = {
title: 'object with required field',
type: 'object',
properties: {
num: {
type: 'integer'
}
},
additionalProperties: true,
required: ['num', 'key1', 'key2']
}
const stringify = build(schema)
try {
stringify({})
t.fail()
} catch (e) {
t.equal(e.message, '"key1" is required!')
t.pass()
}
try {
stringify({
key1: 42,
key2: 42
})
t.fail()
} catch (e) {
t.equal(e.message, '"num" is required!')
t.pass()
}
try {
stringify({
num: 42,
key1: 'some'
})
t.fail()
} catch (e) {
t.equal(e.message, '"key2" is required!')
t.pass()
}
})
test('object with required bool', (t) => {
t.plan(2)
const schema = {
title: 'object with required field',
type: 'object',
properties: {
num: {
type: 'integer'
}
},
additionalProperties: true,
required: ['bool']
}
const stringify = build(schema)
try {
stringify({})
t.fail()
} catch (e) {
t.equal(e.message, '"bool" is required!')
t.pass()
}
stringify({
bool: false
})
})
test('required nullable', (t) => {
t.plan(1)
const schema = {
title: 'object with required field',
type: 'object',
properties: {
num: {
type: ['integer']
}
},
additionalProperties: true,
required: ['null']
}
const stringify = build(schema)
stringify({
null: null
})
t.pass()
})
test('required numbers', (t) => {
t.plan(3)
const schema = {
title: 'object with required field',
type: 'object',
properties: {
str: {
type: 'string'
},
num: {
type: 'integer'
}
},
required: ['num']
}
const stringify = build(schema)
stringify({
num: 42
})
t.pass()
try {
stringify({
num: 'aaa'
})
t.fail()
} catch (e) {
t.equal(e.message, 'The value "aaa" cannot be converted to an integer.')
t.pass()
}
})

View File

@@ -0,0 +1,48 @@
'use strict'
const t = require('tap')
const build = require('..')
t.test('nested ref requires ajv', async t => {
const schemaA = {
$id: 'urn:schema:a',
definitions: {
foo: { anyOf: [{ type: 'string' }, { type: 'null' }] }
}
}
const schemaB = {
$id: 'urn:schema:b',
type: 'object',
properties: {
results: {
type: 'object',
properties: {
items: {
type: 'object',
properties: {
bar: {
type: 'array',
items: { $ref: 'urn:schema:a#/definitions/foo' }
}
}
}
}
}
}
}
const stringify = build(schemaB, {
schema: {
[schemaA.$id]: schemaA
}
})
const result = stringify({
results: {
items: {
bar: ['baz']
}
}
})
t.same(result, '{"results":{"items":{"bar":["baz"]}}}')
})

View File

@@ -0,0 +1,142 @@
'use strict'
const t = require('tap')
const build = require('..')
const stringify = build({
title: 'Example Schema',
type: 'object',
properties: {
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
age: {
description: 'Age in years"',
type: 'integer'
},
[(() => "phra'&& process.exit(1) ||'phra")()]: {},
now: {
type: 'string'
},
reg: {
type: 'string',
default: 'a\'&& process.exit(1) ||\''
},
obj: {
type: 'object',
properties: {
bool: {
type: 'boolean'
}
}
},
'"\'w00t': {
type: 'string',
default: '"\'w00t'
},
arr: {
type: 'array',
items: {
type: 'object',
properties: {
'phra\' && process.exit(1)//': {
type: 'number'
},
str: {
type: 'string'
}
}
}
}
},
required: ['now'],
patternProperties: {
'.*foo$': {
type: 'string'
},
test: {
type: 'number'
},
'phra\'/ && process.exit(1) && /\'': {
type: 'number'
},
'"\'w00t.*////': {
type: 'number'
}
},
additionalProperties: {
type: 'string'
}
})
const obj = {
firstName: 'Matteo',
lastName: 'Collina',
age: 32,
now: new Date(),
foo: 'hello"',
bar: "world'",
'fuzz"': 42,
"me'": 42,
numfoo: 42,
test: 42,
strtest: '23',
arr: [{ 'phra\' && process.exit(1)//': 42 }],
obj: { bool: true },
notmatch: 'valar morghulis',
notmatchobj: { a: true },
notmatchnum: 42
}
// pass if it does not crash
const json = stringify(obj)
JSON.parse(json)
const stringify2 = build({
title: 'Example Schema',
type: 'object',
patternProperties: {
'"\'w00t.*////': {
type: 'number'
}
}
})
t.same(JSON.parse(stringify2({
'"\'phra////': 42,
asd: 42
})), {
})
const stringify3 = build({
title: 'Example Schema',
type: 'object',
properties: {
"\"phra\\'&&(console.log(42))//||'phra": {}
}
})
// this verifies the escaping
JSON.parse(stringify3({
'"phra\'&&(console.log(42))//||\'phra': 42
}))
const stringify4 = build({
title: 'Example Schema',
type: 'object',
properties: {
'"\\\\\\\\\'w00t': {
type: 'string',
default: '"\'w00t'
}
}
})
t.same(JSON.parse(stringify4({})), {
'"\\\\\\\\\'w00t': '"\'w00t'
})
t.pass('no crashes')

View File

@@ -0,0 +1,18 @@
'use strict'
const t = require('tap')
const build = require('..')
const payload = '(throw "pwoned")'
const stringify = build({
properties: {
[`*///\\\\\\']);${payload};{/*`]: {
type: 'number'
}
}
})
stringify({})
t.pass('no crashes')

View File

@@ -0,0 +1,15 @@
'use strict'
const t = require('tap')
const build = require('..')
t.throws(() => {
build({
$defs: {
type: 'foooo"bar'
},
patternProperties: {
x: { $ref: '#/$defs' }
}
})
}, 'foooo"bar unsupported')

View File

@@ -0,0 +1,14 @@
'use strict'
const t = require('tap')
const build = require('..')
const payload = '(throw "pwoned")'
const stringify = build({
required: [`"];${payload}//`]
})
t.throws(() => {
stringify({})
}, 'Error: ""];(throw "pwoned")//" is required!')

View File

@@ -0,0 +1,16 @@
'use strict'
const t = require('tap')
const build = require('..')
const payload = '(throw "pwoned")'
const expected = 'Error: Invalid regular expression: /*/: Nothing to repeat. Found at * matching {"type":"*/(throw \\"pwoned\\")){//"}'
t.throws(() => {
build({
patternProperties: {
'*': { type: `*/${payload}){//` }
}
})
}, expected)

View File

@@ -0,0 +1,22 @@
'use strict'
const t = require('tap')
const build = require('..')
const payload = '(throw "pwoned")'
const stringify = build({
type: 'object',
properties: {
'/*': { type: 'object' },
x: {
type: 'object',
properties: {
a: { type: 'string', default: `*/}${payload};{//` }
}
}
}
})
stringify({})
t.pass('no crashes')

View File

@@ -0,0 +1,68 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('required property containing single quote, contains property', (t) => {
t.plan(1)
const stringify = build({
type: 'object',
properties: {
'\'': { type: 'string' }
},
required: [
'\''
]
})
t.throws(() => stringify({}), new Error('"\'" is required!'))
})
test('required property containing double quote, contains property', (t) => {
t.plan(1)
const stringify = build({
type: 'object',
properties: {
'"': { type: 'string' }
},
required: [
'"'
]
})
t.throws(() => stringify({}), new Error('""" is required!'))
})
test('required property containing single quote, does not contain property', (t) => {
t.plan(1)
const stringify = build({
type: 'object',
properties: {
a: { type: 'string' }
},
required: [
'\''
]
})
t.throws(() => stringify({}), new Error('"\'" is required!'))
})
test('required property containing double quote, does not contain property', (t) => {
t.plan(1)
const stringify = build({
type: 'object',
properties: {
a: { type: 'string' }
},
required: [
'"'
]
})
t.throws(() => stringify({}), new Error('""" is required!'))
})

View File

@@ -0,0 +1,196 @@
'use strict'
const { test } = require('tap')
const clone = require('rfdc/default')
const build = require('..')
test('oneOf with $ref should not change the input schema', t => {
t.plan(2)
const referenceSchema = {
$id: 'externalId',
type: 'object',
properties: {
name: { type: 'string' }
}
}
const schema = {
$id: 'mainSchema',
type: 'object',
properties: {
people: {
oneOf: [{ $ref: 'externalId' }]
}
}
}
const clonedSchema = clone(schema)
const stringify = build(schema, {
schema: {
[referenceSchema.$id]: referenceSchema
}
})
const value = stringify({ people: { name: 'hello', foo: 'bar' } })
t.equal(value, '{"people":{"name":"hello"}}')
t.same(schema, clonedSchema)
})
test('oneOf and anyOf with $ref should not change the input schema', t => {
t.plan(3)
const referenceSchema = {
$id: 'externalSchema',
type: 'object',
properties: {
name: { type: 'string' }
}
}
const schema = {
$id: 'rootSchema',
type: 'object',
properties: {
people: {
oneOf: [{ $ref: 'externalSchema' }]
},
love: {
anyOf: [
{ $ref: '#/definitions/foo' },
{ type: 'boolean' }
]
}
},
definitions: {
foo: { type: 'string' }
}
}
const clonedSchema = clone(schema)
const stringify = build(schema, {
schema: {
[referenceSchema.$id]: referenceSchema
}
})
const valueAny1 = stringify({ people: { name: 'hello', foo: 'bar' }, love: 'music' })
const valueAny2 = stringify({ people: { name: 'hello', foo: 'bar' }, love: true })
t.equal(valueAny1, '{"people":{"name":"hello"},"love":"music"}')
t.equal(valueAny2, '{"people":{"name":"hello"},"love":true}')
t.same(schema, clonedSchema)
})
test('multiple $ref tree', t => {
t.plan(2)
const referenceDeepSchema = {
$id: 'deepId',
type: 'number'
}
const referenceSchema = {
$id: 'externalId',
type: 'object',
properties: {
name: { $ref: '#/definitions/foo' },
age: { $ref: 'deepId' }
},
definitions: {
foo: { type: 'string' }
}
}
const schema = {
$id: 'mainSchema',
type: 'object',
properties: {
people: {
oneOf: [{ $ref: 'externalId' }]
}
}
}
const clonedSchema = clone(schema)
const stringify = build(schema, {
schema: {
[referenceDeepSchema.$id]: referenceDeepSchema,
[referenceSchema.$id]: referenceSchema
}
})
const value = stringify({ people: { name: 'hello', foo: 'bar', age: 42 } })
t.equal(value, '{"people":{"name":"hello","age":42}}')
t.same(schema, clonedSchema)
})
test('must not mutate items $ref', t => {
t.plan(2)
const referenceSchema = {
$id: 'ShowSchema',
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
const schema = {
$id: 'ListSchema',
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'array',
items: {
$ref: 'ShowSchema#'
}
}
const clonedSchema = clone(schema)
const stringify = build(schema, {
schema: {
[referenceSchema.$id]: referenceSchema
}
})
const value = stringify([{ name: 'foo' }])
t.equal(value, '[{"name":"foo"}]')
t.same(schema, clonedSchema)
})
test('must not mutate items referred by $ref', t => {
t.plan(2)
const firstSchema = {
$id: 'example1',
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
const reusedSchema = {
$id: 'example2',
type: 'object',
properties: {
name: {
oneOf: [
{
$ref: 'example1'
}
]
}
}
}
const clonedSchema = clone(firstSchema)
const stringify = build(reusedSchema, {
schema: {
[firstSchema.$id]: firstSchema
}
})
const value = stringify({ name: { name: 'foo' } })
t.equal(value, '{"name":{"name":"foo"}}')
t.same(firstSchema, clonedSchema)
})

View File

@@ -0,0 +1,216 @@
'use strict'
const test = require('tap').test
const fjs = require('..')
const fs = require('fs')
const path = require('path')
function build (opts, schema) {
return fjs(schema || {
title: 'default string',
type: 'object',
properties: {
firstName: {
type: 'string'
}
},
required: ['firstName']
}, opts)
}
const tmpDir = 'test/fixtures'
test('activate standalone mode', async (t) => {
t.plan(3)
const code = build({ mode: 'standalone' })
t.type(code, 'string')
t.equal(code.indexOf('ajv'), -1)
const destination = path.resolve(tmpDir, 'standalone.js')
t.teardown(async () => {
await fs.promises.rm(destination, { force: true })
})
await fs.promises.writeFile(destination, code)
const standalone = require(destination)
t.same(standalone({ firstName: 'Foo', surname: 'bar' }), JSON.stringify({ firstName: 'Foo' }), 'surname evicted')
})
test('test ajv schema', async (t) => {
t.plan(3)
const code = build({ mode: 'standalone' }, {
type: 'object',
properties: {
},
if: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['foobar'] }
}
},
then: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['foobar'] },
foo: { type: 'string' },
bar: { type: 'number' },
list: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'string' }
}
}
}
}
},
else: {
type: 'object',
properties: {
kind: { type: 'string', enum: ['greeting'] },
hi: { type: 'string' },
hello: { type: 'number' },
list: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'string' }
}
}
}
}
}
})
t.type(code, 'string')
t.equal(code.indexOf('ajv') > 0, true)
const destination = path.resolve(tmpDir, 'standalone2.js')
t.teardown(async () => {
await fs.promises.rm(destination, { force: true })
})
await fs.promises.writeFile(destination, code)
const standalone = require(destination)
t.same(standalone({
kind: 'foobar',
foo: 'FOO',
list: [{
name: 'name',
value: 'foo'
}],
bar: 42,
hi: 'HI',
hello: 45,
a: 'A',
b: 35
}), JSON.stringify({
kind: 'foobar',
foo: 'FOO',
bar: 42,
list: [{
name: 'name',
value: 'foo'
}]
}))
})
test('no need to keep external schemas once compiled', async (t) => {
t.plan(1)
const externalSchema = {
first: {
definitions: {
id1: {
type: 'object',
properties: {
id1: {
type: 'integer'
}
}
}
}
}
}
const code = fjs({
$ref: 'first#/definitions/id1'
}, {
mode: 'standalone',
schema: externalSchema
})
const destination = path.resolve(tmpDir, 'standalone3.js')
t.teardown(async () => {
await fs.promises.rm(destination, { force: true })
})
await fs.promises.writeFile(destination, code)
const standalone = require(destination)
t.same(standalone({ id1: 5 }), JSON.stringify({ id1: 5 }), 'serialization works with external schemas')
})
test('no need to keep external schemas once compiled - with oneOf validator', async (t) => {
t.plan(2)
const externalSchema = {
ext: {
definitions: {
oBaz: {
type: 'object',
properties: {
baz: { type: 'number' }
},
required: ['baz']
},
oBar: {
type: 'object',
properties: {
bar: { type: 'string' }
},
required: ['bar']
},
other: {
type: 'string',
const: 'other'
}
}
}
}
const schema = {
title: 'object with oneOf property value containing refs to external schema',
type: 'object',
properties: {
oneOfSchema: {
oneOf: [
{ $ref: 'ext#/definitions/oBaz' },
{ $ref: 'ext#/definitions/oBar' }
]
}
},
required: ['oneOfSchema']
}
const code = fjs(schema, {
mode: 'standalone',
schema: externalSchema
})
const destination = path.resolve(tmpDir, 'standalone-oneOf-ref.js')
t.teardown(async () => {
await fs.promises.rm(destination, { force: true })
})
await fs.promises.writeFile(destination, code)
const stringify = require(destination)
t.equal(stringify({ oneOfSchema: { baz: 5 } }), '{"oneOfSchema":{"baz":5}}')
t.equal(stringify({ oneOfSchema: { bar: 'foo' } }), '{"oneOfSchema":{"bar":"foo"}}')
})

View File

@@ -0,0 +1,84 @@
'use strict'
const t = require('tap')
const test = t.test
const build = require('..')
test('serialize short string', (t) => {
t.plan(2)
const schema = {
type: 'string'
}
const input = 'abcd'
const stringify = build(schema)
const output = stringify(input)
t.equal(output, '"abcd"')
t.equal(JSON.parse(output), input)
})
test('serialize short string', (t) => {
t.plan(2)
const schema = {
type: 'string'
}
const input = '\x00'
const stringify = build(schema)
const output = stringify(input)
t.equal(output, '"\\u0000"')
t.equal(JSON.parse(output), input)
})
test('serialize long string', (t) => {
t.plan(2)
const schema = {
type: 'string'
}
const input = new Array(2e4).fill('\x00').join('')
const stringify = build(schema)
const output = stringify(input)
t.equal(output, `"${new Array(2e4).fill('\\u0000').join('')}"`)
t.equal(JSON.parse(output), input)
})
test('unsafe string', (t) => {
t.plan(2)
const schema = {
type: 'string',
format: 'unsafe'
}
const input = 'abcd'
const stringify = build(schema)
const output = stringify(input)
t.equal(output, `"${input}"`)
t.equal(JSON.parse(output), input)
})
test('unsafe unescaped string', (t) => {
t.plan(2)
const schema = {
type: 'string',
format: 'unsafe'
}
const input = 'abcd "abcd"'
const stringify = build(schema)
const output = stringify(input)
t.equal(output, `"${input}"`)
t.throws(function () {
JSON.parse(output)
})
})

View File

@@ -0,0 +1,67 @@
'use strict'
const test = require('tap').test
const validator = require('is-my-json-valid')
const build = require('..')
test('render a string with surrogate pairs as JSON:test 1', (t) => {
t.plan(2)
const schema = {
title: 'surrogate',
type: 'string'
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify('𝌆')
t.equal(output, '"𝌆"')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a string with surrogate pairs as JSON: test 2', (t) => {
t.plan(2)
const schema = {
title: 'long',
type: 'string'
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify('\uD834\uDF06')
t.equal(output, '"𝌆"')
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a string with Unpaired surrogate code as JSON', (t) => {
t.plan(2)
const schema = {
title: 'surrogate',
type: 'string'
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify('\uDF06\uD834')
t.equal(output, JSON.stringify('\uDF06\uD834'))
t.ok(validate(JSON.parse(output)), 'valid schema')
})
test('render a string with lone surrogate code as JSON', (t) => {
t.plan(2)
const schema = {
title: 'surrogate',
type: 'string'
}
const validate = validator(schema)
const stringify = build(schema)
const output = stringify('\uDEAD')
t.equal(output, JSON.stringify('\uDEAD'))
t.ok(validate(JSON.parse(output)), 'valid schema')
})

View File

@@ -0,0 +1,203 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('use toJSON method on object types', (t) => {
t.plan(1)
const stringify = build({
title: 'simple object',
type: 'object',
properties: {
productName: {
type: 'string'
}
}
})
const object = {
product: { name: 'cola' },
toJSON: function () {
return { productName: this.product.name }
}
}
t.equal('{"productName":"cola"}', stringify(object))
})
test('use toJSON method on nested object types', (t) => {
t.plan(1)
const stringify = build({
title: 'simple array',
type: 'array',
items: {
type: 'object',
properties: {
productName: {
type: 'string'
}
}
}
})
const array = [
{
product: { name: 'cola' },
toJSON: function () {
return { productName: this.product.name }
}
},
{
product: { name: 'sprite' },
toJSON: function () {
return { productName: this.product.name }
}
}
]
t.equal('[{"productName":"cola"},{"productName":"sprite"}]', stringify(array))
})
test('not use toJSON if does not exist', (t) => {
t.plan(1)
const stringify = build({
title: 'simple object',
type: 'object',
properties: {
product: {
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
}
})
const object = {
product: { name: 'cola' }
}
t.equal('{"product":{"name":"cola"}}', stringify(object))
})
test('not fail on null object declared nullable', (t) => {
t.plan(1)
const stringify = build({
title: 'simple object',
type: 'object',
nullable: true,
properties: {
product: {
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
}
})
t.equal('null', stringify(null))
})
test('not fail on null sub-object declared nullable', (t) => {
t.plan(1)
const stringify = build({
title: 'simple object',
type: 'object',
properties: {
product: {
nullable: true,
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
}
})
const object = {
product: null
}
t.equal('{"product":null}', stringify(object))
})
test('on non nullable null sub-object it should coerce to {}', (t) => {
t.plan(1)
const stringify = build({
title: 'simple object',
type: 'object',
properties: {
product: {
nullable: false,
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
}
})
const object = {
product: null
}
const result = stringify(object)
t.equal(result, JSON.stringify({ product: {} }))
})
test('on non nullable null object it should coerce to {}', (t) => {
t.plan(1)
const stringify = build({
title: 'simple object',
nullable: false,
type: 'object',
properties: {
product: {
nullable: false,
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
}
})
const result = stringify(null)
t.equal(result, '{}')
})
test('on non-nullable null object it should skip rendering, skipping required fields checks', (t) => {
t.plan(1)
const stringify = build({
title: 'simple object',
nullable: false,
type: 'object',
properties: {
product: {
nullable: false,
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
},
required: ['product']
})
const result = stringify(null)
t.equal(result, '{}')
})

View File

@@ -0,0 +1,36 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('nested object in pattern properties for typebox', (t) => {
const { Type } = require('@sinclair/typebox')
t.plan(1)
const nestedSchema = Type.Object({
nestedKey1: Type.String()
})
const RootSchema = Type.Object({
key1: Type.Record(Type.String(), nestedSchema),
key2: Type.Record(Type.String(), nestedSchema)
})
const schema = RootSchema
const stringify = build(schema)
const value = stringify({
key1: {
nestedKey: {
nestedKey1: 'value1'
}
},
key2: {
nestedKey: {
nestedKey1: 'value2'
}
}
})
t.equal(value, '{"key1":{"nestedKey":{"nestedKey1":"value1"}},"key2":{"nestedKey":{"nestedKey1":"value2"}}}')
})

View File

@@ -0,0 +1,550 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('possibly nullable integer primitive alternative', (t) => {
t.plan(1)
const schema = {
title: 'simple object with multi-type nullable primitive',
type: 'object',
properties: {
data: {
type: ['integer']
}
}
}
const stringify = build(schema, { ajv: { allowUnionTypes: true } })
const value = stringify({
data: 4
})
t.equal(value, '{"data":4}')
})
test('possibly nullable number primitive alternative', (t) => {
t.plan(1)
const schema = {
title: 'simple object with multi-type nullable primitive',
type: 'object',
properties: {
data: {
type: ['number']
}
}
}
const stringify = build(schema)
const value = stringify({
data: 4
})
t.equal(value, '{"data":4}')
})
test('possibly nullable integer primitive alternative with null value', (t) => {
t.plan(1)
const schema = {
title: 'simple object with multi-type nullable primitive',
type: 'object',
properties: {
data: {
type: ['integer']
}
}
}
const stringify = build(schema)
const value = stringify({
data: null
})
t.equal(value, '{"data":0}')
})
test('possibly nullable number primitive alternative with null value', (t) => {
t.plan(1)
const schema = {
title: 'simple object with multi-type nullable primitive',
type: 'object',
properties: {
data: {
type: ['number']
}
}
}
const stringify = build(schema)
const value = stringify({
data: null
})
t.equal(value, '{"data":0}')
})
test('possibly nullable number primitive alternative with null value', (t) => {
t.plan(1)
const schema = {
title: 'simple object with multi-type nullable primitive',
type: 'object',
properties: {
data: {
type: ['boolean']
}
}
}
const stringify = build(schema)
const value = stringify({
data: null
})
t.equal(value, '{"data":false}')
})
test('nullable integer primitive', (t) => {
t.plan(1)
const schema = {
title: 'simple object with nullable primitive',
type: 'object',
properties: {
data: {
type: ['integer', 'null']
}
}
}
const stringify = build(schema)
const value = stringify({
data: 4
})
t.equal(value, '{"data":4}')
})
test('nullable number primitive', (t) => {
t.plan(1)
const schema = {
title: 'simple object with nullable primitive',
type: 'object',
properties: {
data: {
type: ['number', 'null']
}
}
}
const stringify = build(schema)
const value = stringify({
data: 4
})
t.equal(value, '{"data":4}')
})
test('nullable primitive with null value', (t) => {
t.plan(1)
const schema = {
title: 'simple object with nullable primitive',
type: 'object',
properties: {
data: {
type: ['integer', 'null']
}
}
}
const stringify = build(schema)
const value = stringify({
data: null
})
t.equal(value, '{"data":null}')
})
test('nullable number primitive with null value', (t) => {
t.plan(1)
const schema = {
title: 'simple object with nullable primitive',
type: 'object',
properties: {
data: {
type: ['number', 'null']
}
}
}
const stringify = build(schema)
const value = stringify({
data: null
})
t.equal(value, '{"data":null}')
})
test('possibly null object with multi-type property', (t) => {
t.plan(3)
const schema = {
title: 'simple object with multi-type property',
type: 'object',
properties: {
objectOrNull: {
type: ['object', 'null'],
properties: {
stringOrNumber: {
type: ['string', 'number']
}
}
}
}
}
const stringify = build(schema)
t.equal(stringify({
objectOrNull: {
stringOrNumber: 'string'
}
}), '{"objectOrNull":{"stringOrNumber":"string"}}')
t.equal(stringify({
objectOrNull: {
stringOrNumber: 42
}
}), '{"objectOrNull":{"stringOrNumber":42}}')
t.equal(stringify({
objectOrNull: null
}), '{"objectOrNull":null}')
})
test('object with possibly null array of multiple types', (t) => {
t.plan(5)
const schema = {
title: 'object with array of multiple types',
type: 'object',
properties: {
arrayOfStringsAndNumbers: {
type: ['array', 'null'],
items: {
type: ['string', 'number', 'null']
}
}
}
}
const stringify = build(schema)
try {
const value = stringify({
arrayOfStringsAndNumbers: null
})
t.equal(value, '{"arrayOfStringsAndNumbers":null}')
} catch (e) {
console.log(e)
t.fail()
}
try {
const value = stringify({
arrayOfStringsAndNumbers: ['string1', 'string2']
})
t.equal(value, '{"arrayOfStringsAndNumbers":["string1","string2"]}')
} catch (e) {
console.log(e)
t.fail()
}
t.equal(stringify({
arrayOfStringsAndNumbers: [42, 7]
}), '{"arrayOfStringsAndNumbers":[42,7]}')
t.equal(stringify({
arrayOfStringsAndNumbers: ['string1', 42, 7, 'string2']
}), '{"arrayOfStringsAndNumbers":["string1",42,7,"string2"]}')
t.equal(stringify({
arrayOfStringsAndNumbers: ['string1', null, 42, 7, 'string2', null]
}), '{"arrayOfStringsAndNumbers":["string1",null,42,7,"string2",null]}')
})
test('object with tuple of multiple types', (t) => {
t.plan(2)
const schema = {
title: 'object with array of multiple types',
type: 'object',
properties: {
fixedTupleOfStringsAndNumbers: {
type: 'array',
items: [
{
type: 'string'
},
{
type: 'number'
},
{
type: ['string', 'number']
}
]
}
}
}
const stringify = build(schema)
try {
const value = stringify({
fixedTupleOfStringsAndNumbers: ['string1', 42, 7]
})
t.equal(value, '{"fixedTupleOfStringsAndNumbers":["string1",42,7]}')
} catch (e) {
console.log(e)
t.fail()
}
try {
const value = stringify({
fixedTupleOfStringsAndNumbers: ['string1', 42, 'string2']
})
t.equal(value, '{"fixedTupleOfStringsAndNumbers":["string1",42,"string2"]}')
} catch (e) {
console.log(e)
t.fail()
}
})
test('object with anyOf and multiple types', (t) => {
t.plan(3)
const schema = {
title: 'object with anyOf and multiple types',
type: 'object',
properties: {
objectOrBoolean: {
anyOf: [
{
type: 'object',
properties: {
stringOrNumber: {
type: ['string', 'number']
}
}
},
{
type: 'boolean'
}
]
}
}
}
const stringify = build(schema, { ajv: { allowUnionTypes: true } })
try {
const value = stringify({
objectOrBoolean: { stringOrNumber: 'string' }
})
t.equal(value, '{"objectOrBoolean":{"stringOrNumber":"string"}}')
} catch (e) {
console.log(e)
t.fail()
}
t.equal(stringify({
objectOrBoolean: { stringOrNumber: 42 }
}), '{"objectOrBoolean":{"stringOrNumber":42}}')
t.equal(stringify({
objectOrBoolean: true
}), '{"objectOrBoolean":true}')
})
test('string type array can handle dates', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
date: { type: ['string'] },
dateObject: { type: ['string'], format: 'date-time' }
}
}
const stringify = build(schema)
const value = stringify({
date: new Date('2018-04-20T07:52:31.017Z'),
dateObject: new Date('2018-04-21T07:52:31.017Z')
})
t.equal(value, '{"date":"2018-04-20T07:52:31.017Z","dateObject":"2018-04-21T07:52:31.017Z"}')
})
test('object that is simultaneously a string and a json', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
simultaneously: {
type: ['string', 'object'],
properties: {
foo: { type: 'string' }
}
}
}
}
const likeObjectId = {
toString () { return 'hello' }
}
const stringify = build(schema)
const valueStr = stringify({ simultaneously: likeObjectId })
t.equal(valueStr, '{"simultaneously":"hello"}')
const valueObj = stringify({ simultaneously: { foo: likeObjectId } })
t.equal(valueObj, '{"simultaneously":{"foo":"hello"}}')
})
test('object that is simultaneously a string and a json switched', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
simultaneously: {
type: ['object', 'string'],
properties: {
foo: { type: 'string' }
}
}
}
}
const likeObjectId = {
toString () { return 'hello' }
}
const stringify = build(schema)
const valueStr = stringify({ simultaneously: likeObjectId })
t.equal(valueStr, '{"simultaneously":{}}')
const valueObj = stringify({ simultaneously: { foo: likeObjectId } })
t.equal(valueObj, '{"simultaneously":{"foo":"hello"}}')
})
test('class instance that is simultaneously a string and a json', (t) => {
t.plan(2)
const schema = {
type: 'object',
properties: {
simultaneously: {
type: ['string', 'object'],
properties: {
foo: { type: 'string' }
}
}
}
}
class Test {
toString () { return 'hello' }
}
const likeObjectId = new Test()
const stringify = build(schema)
const valueStr = stringify({ simultaneously: likeObjectId })
t.equal(valueStr, '{"simultaneously":"hello"}')
const valueObj = stringify({ simultaneously: { foo: likeObjectId } })
t.equal(valueObj, '{"simultaneously":{"foo":"hello"}}')
})
test('should not throw an error when type is array and object is null, it should instead coerce to []', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
arr: {
type: 'array',
items: {
type: 'number'
}
}
}
}
const stringify = build(schema)
const result = stringify({ arr: null })
t.equal(result, JSON.stringify({ arr: [] }))
})
test('should throw an error when type is array and object is not an array', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
arr: {
type: 'array',
items: {
type: 'number'
}
}
}
}
const stringify = build(schema)
t.throws(() => stringify({ arr: { foo: 'hello' } }), new TypeError('The value of \'#/properties/arr\' does not match schema definition.'))
})
test('should throw an error when type is array and object is not an array with external schema', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
arr: {
$ref: 'arrayOfNumbers#/definitions/arr'
}
}
}
const externalSchema = {
arrayOfNumbers: {
definitions: {
arr: {
type: 'array',
items: {
type: 'number'
}
}
}
}
}
const stringify = build(schema, { schema: externalSchema })
t.throws(() => stringify({ arr: { foo: 'hello' } }), new TypeError('The value of \'arrayOfNumbers#/definitions/arr\' does not match schema definition.'))
})
test('throw an error if none of types matches', (t) => {
t.plan(1)
const schema = {
title: 'simple object with multi-type nullable primitive',
type: 'object',
properties: {
data: {
type: ['number', 'boolean']
}
}
}
const stringify = build(schema)
t.throws(() => stringify({ data: 'string' }), 'The value "string" does not match schema definition.')
})

View File

@@ -0,0 +1,27 @@
'use strict'
const test = require('tap').test
const build = require('..')
test('object with custom format field', (t) => {
t.plan(1)
const schema = {
title: 'object with custom format field',
type: 'object',
properties: {
str: {
type: 'string',
format: 'test-format'
}
}
}
const stringify = build(schema)
stringify({
str: 'string'
})
t.pass()
})

View File

@@ -0,0 +1,50 @@
'use strict'
const test = require('tap').test
const webpack = require('webpack')
const path = require('path')
test('the library should work with webpack', async (t) => {
t.plan(1)
const targetdir = path.resolve(__dirname, '..', '.cache')
const targetname = path.join(targetdir, 'webpacktest.js')
const wopts = {
entry: path.resolve(__dirname, '..', 'index.js'),
mode: 'production',
target: 'node',
output: {
path: targetdir,
filename: 'webpacktest.js',
library: {
name: 'fastJsonStringify',
type: 'umd'
}
}
}
await new Promise((resolve, reject) => {
webpack(wopts, (err, stats) => {
if (err) { reject(err) } else { resolve(stats) };
})
})
const build = require(targetname)
const stringify = build({
title: 'webpack should not rename code to be executed',
type: 'object',
properties: {
foo: {
type: 'string'
},
bar: {
type: 'boolean'
}
},
patternProperties: {
foo: {
type: 'number'
}
}
})
const obj = { foo: '42', bar: true }
t.equal(stringify(obj), '{"foo":"42","bar":true}')
})

View File

@@ -0,0 +1,231 @@
import Ajv, { Options as AjvOptions } from "ajv"
type Build = typeof build
declare namespace build {
interface BaseSchema {
/**
* Schema id
*/
$id?: string
/**
* Schema title
*/
title?: string;
/**
* Schema description
*/
description?: string;
/**
* A comment to be added to the schema
*/
$comment?: string;
/**
* Default value to be assigned when no value is given in the document
*/
default?: any;
/**
* A list of example values that match this schema
*/
examples?: any[];
/**
* Additional schema definition to reference from within the schema
*/
definitions?: Record<string, Schema>
/**
* A set of schemas of which at least one must match
*/
anyOf?: Partial<Schema>[];
/**
* A set of schemas which must all match
*/
allOf?: Partial<Schema>[];
/**
* A conditional schema to check, controls schemas defined in `then` and `else`
*/
if?: Partial<Schema>;
/**
* A schema to apply if the conditional schema from `if` passes
*/
then?: Partial<Schema>;
/**
* A schema to apply if the conditional schema from `if` fails
*/
else?: Partial<Schema>;
/**
* Open API 3.0 spec states that any value that can be null must be declared `nullable`
* @default false
*/
nullable?: boolean;
}
export interface RefSchema {
/**
* A json-pointer to a schema to use as a reference
*/
$ref: string;
}
export interface AnySchema extends BaseSchema {
}
export interface StringSchema extends BaseSchema {
type: "string";
format?: string;
}
export interface IntegerSchema extends BaseSchema {
type: "integer";
}
export interface NumberSchema extends BaseSchema {
type: "number";
}
export interface NullSchema extends BaseSchema {
type: "null";
}
export interface BooleanSchema extends BaseSchema {
type: "boolean";
}
export interface ArraySchema extends BaseSchema {
type: "array";
/**
* The schema for the items in the array
*/
items: Schema | {}
}
export interface TupleSchema extends BaseSchema {
type: "array";
/**
* The schemas for the items in the tuple
*/
items: Schema[];
}
type ObjectProperties = Record<string, Partial<Schema>> & {
anyOf?: ObjectProperties[];
allOf?: ObjectProperties[];
if?: ObjectProperties;
then?: ObjectProperties;
else?: ObjectProperties;
}
export interface ObjectSchema extends BaseSchema {
type: "object";
/**
* Describe the properties of the object
*/
properties?: ObjectProperties;
/**
* The required properties of the object
*/
required?: string[];
/**
* Describe properties that have keys following a given pattern
*/
patternProperties?: ObjectProperties;
/**
* Specifies whether additional properties on the object are allowed, and optionally what schema they should
* adhere to
* @default false
*/
additionalProperties?: Schema | boolean;
}
export type Schema =
| RefSchema
| StringSchema
| IntegerSchema
| NumberSchema
| NullSchema
| BooleanSchema
| ArraySchema
| TupleSchema
| ObjectSchema;
export interface Options {
/**
* Optionally add an external definition to reference from your schema
*/
schema?: Record<string, Schema>
/**
* Configure Ajv, which is used to evaluate conditional schemas and combined (anyOf) schemas
*/
ajv?: AjvOptions
/**
* Optionally configure how the integer will be rounded
*
* @default 'trunc'
*/
rounding?: 'ceil' | 'floor' | 'round' | 'trunc'
/**
* @deprecated
* Enable debug mode. Please use `mode: "debug"` instead
*/
debugMode?: boolean
/**
* Running mode of fast-json-stringify
*/
mode?: 'debug' | 'standalone'
/**
* Large arrays are defined as arrays containing, by default, `20000`
* elements or more. That value can be adjusted via the option parameter
* `largeArraySize`.
*
* @default 20000
*/
largeArraySize?: number | string | BigInt
/**
* Specify the function on how large Arrays should be stringified.
*
* @default 'default'
*/
largeArrayMechanism?: 'default' | 'json-stringify'
}
export const validLargeArrayMechanisms: string[]
export function restore (value: <TDoc extends object = object>(doc: TDoc) => string): ReturnType<Build>
export const build: Build
export { build as default }
}
interface DebugOption extends build.Options {
mode: 'debug'
}
interface DeprecateDebugOption extends build.Options {
debugMode: true
}
interface StandaloneOption extends build.Options {
mode: 'standalone'
}
type StringCoercible = string | Date | RegExp;
type IntegerCoercible = number | BigInt;
/**
* Build a stringify function using a schema of the documents that should be stringified
* @param schema The schema used to stringify values
* @param options The options to use (optional)
*/
declare function build(schema: build.AnySchema, options: DebugOption): { code: string, ajv: Ajv };
declare function build(schema: build.AnySchema, options: DeprecateDebugOption): { code: string, ajv: Ajv };
declare function build(schema: build.AnySchema, options: StandaloneOption): string;
declare function build(schema: build.AnySchema, options?: build.Options): <TDoc = any>(doc: TDoc) => any;
declare function build(schema: build.StringSchema, options?: build.Options): <TDoc extends StringCoercible = StringCoercible>(doc: TDoc) => string;
declare function build(schema: build.IntegerSchema | build.NumberSchema, options?: build.Options): <TDoc extends IntegerCoercible = IntegerCoercible>(doc: TDoc) => string;
declare function build(schema: build.NullSchema, options?: build.Options): <TDoc extends null = null>(doc: TDoc) => "null";
declare function build(schema: build.BooleanSchema, options?: build.Options): <TDoc extends boolean = boolean>(doc: TDoc) => string;
declare function build(schema: build.ArraySchema | build.TupleSchema, options?: build.Options): <TDoc extends any[]= any[]>(doc: TDoc) => string;
declare function build(schema: build.ObjectSchema, options?: build.Options): <TDoc extends object = object>(doc: TDoc) => string;
declare function build(schema: build.Schema, options?: build.Options): <TDoc = object | any[] | string | number | boolean | null> (doc: TDoc) => string;
export = build;

View File

@@ -0,0 +1,258 @@
import Ajv from 'ajv'
import build, { restore, Schema, validLargeArrayMechanisms } from '..'
import { expectError, expectType } from 'tsd'
// Number schemas
build({
type: 'number'
})(25)
build({
type: 'integer'
})(-5)
build({
type: 'integer'
})(5n)
build({
type: 'number'
}, { rounding: 'ceil' })
build({
type: 'number'
}, { rounding: 'floor' })
build({
type: 'number'
}, { rounding: 'round' })
build({
type: 'number'
}, { rounding: 'trunc' })
expectError(build({
type: 'number'
}, { rounding: 'invalid' }))
// String schema
build({
type: 'string'
})('foobar')
// Boolean schema
build({
type: 'boolean'
})(true)
// Null schema
build({
type: 'null'
})(null)
// Array schemas
build({
type: 'array',
items: { type: 'number' }
})([25])
build({
type: 'array',
items: [{ type: 'string'}, {type: 'integer'}]
})(['hello', 42])
// Object schemas
build({
type: 'object'
})({})
build({
type: 'object',
properties: {
foo: { type: 'string' },
bar: { type: 'integer' }
},
required: ['foo'],
patternProperties: {
'baz*': { type: 'null' }
},
additionalProperties: {
type: 'boolean'
}
})({ foo: 'bar' })
build({
type: 'object',
properties: {
foo: { type: 'string' },
bar: { type: 'integer' }
},
required: ['foo'],
patternProperties: {
'baz*': { type: 'null' }
},
additionalProperties: {
type: 'boolean'
}
}, { rounding: 'floor' })({ foo: 'bar' })
// Reference schemas
build({
title: 'Example Schema',
definitions: {
num: {
type: 'object',
properties: {
int: {
type: 'integer'
}
}
},
str: {
type: 'string'
},
def: {
type: 'null'
}
},
type: 'object',
properties: {
nickname: {
$ref: '#/definitions/str'
}
},
patternProperties: {
'num': {
$ref: '#/definitions/num'
}
},
additionalProperties: {
$ref: '#/definitions/def'
}
})({ nickname: '', num: { int: 5 }, other: null })
// Conditional/Combined schemas
build({
title: 'Conditional/Combined Schema',
type: 'object',
properties: {
something: {
anyOf: [
{ type: 'string' },
{ type: 'boolean' }
]
}
},
if: {
properties: {
something: { type: 'string' }
}
},
then: {
properties: {
somethingElse: { type: 'number' }
}
},
else: {
properties: {
somethingElse: { type: 'null' }
}
}
})({ something: 'a string', somethingElse: 42 })
// String schema with format
build({
type: 'string',
format: 'date-time'
})(new Date())
/*
This overload doesn't work yet -
TypeScript chooses the generic for the schema
before it chooses the overload for the options
parameter.
let str: string, ajv: Ajv
str = build({
type: 'number'
}, { debugMode: true }).code
ajv = build({
type: 'number'
}, { debugMode: true }).ajv
str = build({
type: 'number'
}, { mode: 'debug' }).code
ajv = build({
type: 'number'
}, { mode: 'debug' }).ajv
str = build({
type: 'number'
}, { mode: 'standalone' })
*/
const debugCompiled = build({
title: 'default string',
type: 'object',
properties: {
firstName: {
type: 'string'
}
}
}, { mode: 'debug' })
expectType<ReturnType<typeof build>>(build.restore(debugCompiled))
expectType<ReturnType<typeof build>>(restore(debugCompiled))
expectType<string[]>(build.validLargeArrayMechanisms)
expectType<string[]>(validLargeArrayMechanisms)
/**
* Schema inference
*/
// With inference
interface InferenceSchema {
id: string;
a?: number;
}
const stringify3 = build({
type: "object",
properties: { a: { type: "string" } },
});
stringify3<InferenceSchema>({ id: "123" });
stringify3<InferenceSchema>({ a: 123, id: "123" });
expectError(stringify3<InferenceSchema>({ anotherOne: "bar" }));
expectError(stringify3<Schema>({ a: "bar" }));
// Without inference
const stringify4 = build({
type: "object",
properties: { a: { type: "string" } },
});
stringify4({ id: "123" });
stringify4({ a: 123, id: "123" });
stringify4({ anotherOne: "bar" });
stringify4({ a: "bar" });
// Without inference - string type
const stringify5 = build({
type: "string",
});
stringify5("foo");
expectError(stringify5({ id: "123" }));
// Without inference - null type
const stringify6 = build({
type: "null",
});
stringify6(null);
expectError(stringify6("a string"));
// Without inference - boolean type
const stringify7 = build({
type: "boolean",
});
stringify7(true);
expectError(stringify7("a string"));
// largeArrayMechanism
build({}, { largeArrayMechanism: 'json-stringify'} )
build({}, { largeArrayMechanism: 'default'} )
expectError(build({} as Schema, { largeArrayMechanism: 'invalid'} ))
build({}, { largeArraySize: 2000 } )
build({}, { largeArraySize: '2e4' } )
build({}, { largeArraySize: 2n } )
expectError(build({} as Schema, { largeArraySize: ['asdf']} ))