fast-json-stringify 7.1.2 → 7.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/index.js +59 -4
- package/lib/serializer.js +4 -1
- package/lib/validator.js +2 -2
- package/package.json +1 -1
- package/test/primitive-array-prototype.test.js +51 -0
- package/test/ref.test.js +227 -0
- package/test/standalone-mode.test.js +32 -0
- package/test/strict-schema.test.js +55 -0
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ const stringify = fastJson(mySchema, {
|
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
- `schema`: external schemas references by $ref property. [More details](#ref)
|
|
130
|
-
- `ajv`: [ajv v8 instance's settings](https://ajv.js.org/options.html) for those properties that require `ajv`. [More details](#anyof)
|
|
130
|
+
- `ajv`: [ajv v8 instance's settings](https://ajv.js.org/options.html) for those properties that require `ajv`. [More details](#anyof) Schema strictness defaults to `false`; pass `ajv: { strictSchema: true }` to reject unknown schema keywords or `ajv: { strictSchema: 'log' }` to log warnings. This applies only to schema paths compiled by Ajv, not to every schema processed by fast-json-stringify.
|
|
131
131
|
- `rounding`: setup how the `integer` types will be rounded when not integers. [More details](#integer)
|
|
132
132
|
- `maxDepth`: maximum number of nested schema levels allowed during compilation.
|
|
133
133
|
Defaults to `100` and may be set from `0` to `100`. Schemas that exceed this limit
|
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
/* eslint no-prototype-builtins: 0 */
|
|
4
4
|
|
|
5
5
|
const { RefResolver } = require('json-schema-ref-resolver')
|
|
6
|
+
const { MergeError } = require('@fastify/merge-json-schemas')
|
|
6
7
|
|
|
7
8
|
const Serializer = require('./lib/serializer')
|
|
8
9
|
const Validator = require('./lib/validator')
|
|
@@ -13,7 +14,7 @@ const mergeSchemas = require('./lib/merge-schemas')
|
|
|
13
14
|
const DEFAULT_MAX_DEPTH = 100
|
|
14
15
|
const NAMED_FRAGMENT_REF = /^#[a-z_][-\w._]*$/i
|
|
15
16
|
|
|
16
|
-
// Minimum array lengths at which copying into a
|
|
17
|
+
// Minimum array lengths at which copying into a plain array and
|
|
17
18
|
// calling native JSON.stringify beats the generated per-item loop. Measured
|
|
18
19
|
// against main with tinybench (alternating rounds, median ops/s, output
|
|
19
20
|
// consumed with Buffer.byteLength) on Node 20, 22, 24 and 26 x64.
|
|
@@ -87,6 +88,19 @@ const validLargeArrayMechanisms = new Set([
|
|
|
87
88
|
'json-stringify'
|
|
88
89
|
])
|
|
89
90
|
|
|
91
|
+
// Keywords that do not change the output, so a $ref carrying only these
|
|
92
|
+
// can still be dereferenced directly instead of merged with its target.
|
|
93
|
+
const IGNORED_REF_SIBLING_KEYWORDS = new Set([
|
|
94
|
+
'$ref',
|
|
95
|
+
'$comment',
|
|
96
|
+
'title',
|
|
97
|
+
'description',
|
|
98
|
+
'examples',
|
|
99
|
+
'deprecated',
|
|
100
|
+
'readOnly',
|
|
101
|
+
'writeOnly'
|
|
102
|
+
])
|
|
103
|
+
|
|
90
104
|
let schemaIdCounter = 0
|
|
91
105
|
|
|
92
106
|
function getMaxDepth (options) {
|
|
@@ -223,10 +237,12 @@ function isValidSchema (schema, name) {
|
|
|
223
237
|
|
|
224
238
|
function resolveRef (context, location) {
|
|
225
239
|
const seen = new Set()
|
|
240
|
+
const siblingLocations = []
|
|
226
241
|
let depth = 0
|
|
227
242
|
|
|
228
243
|
while (location.schema.$ref !== undefined) {
|
|
229
|
-
const
|
|
244
|
+
const refSchema = location.schema
|
|
245
|
+
const ref = refSchema.$ref
|
|
230
246
|
const locationRef = location.getSchemaRef()
|
|
231
247
|
|
|
232
248
|
if (seen.has(locationRef)) {
|
|
@@ -251,10 +267,45 @@ function resolveRef (context, location) {
|
|
|
251
267
|
throw new Error(`Cannot find reference "${ref}"`)
|
|
252
268
|
}
|
|
253
269
|
|
|
270
|
+
const siblingSchema = {}
|
|
271
|
+
for (const key in refSchema) {
|
|
272
|
+
if (!IGNORED_REF_SIBLING_KEYWORDS.has(key)) {
|
|
273
|
+
siblingSchema[key] = refSchema[key]
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (Object.keys(siblingSchema).length !== 0) {
|
|
278
|
+
siblingLocations.unshift(new Location(
|
|
279
|
+
cloneOriginSchema(context, siblingSchema, location.schemaId),
|
|
280
|
+
location.schemaId,
|
|
281
|
+
location.jsonPointer
|
|
282
|
+
))
|
|
283
|
+
}
|
|
284
|
+
|
|
254
285
|
location = new Location(schema, schemaId, jsonPointer)
|
|
255
286
|
}
|
|
256
287
|
|
|
257
|
-
|
|
288
|
+
if (siblingLocations.length === 0) {
|
|
289
|
+
return location
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Keyed by content, not by identity: a merged schema is cloned on every
|
|
293
|
+
// merge, so a recursive $ref would otherwise be merged again on each level.
|
|
294
|
+
const mergedSchemaKey = location.getSchemaRef() + JSON.stringify(siblingLocations.map((l) => l.schema))
|
|
295
|
+
|
|
296
|
+
let mergedSchemaId = context.mergedRefsIds.get(mergedSchemaKey)
|
|
297
|
+
if (mergedSchemaId === undefined) {
|
|
298
|
+
mergedSchemaId = `__fjs_merged_${schemaIdCounter++}`
|
|
299
|
+
try {
|
|
300
|
+
mergeLocations(context, mergedSchemaId, [location, ...siblingLocations])
|
|
301
|
+
} catch (err) {
|
|
302
|
+
if (!(err instanceof MergeError)) throw err
|
|
303
|
+
return location
|
|
304
|
+
}
|
|
305
|
+
context.mergedRefsIds.set(mergedSchemaKey, mergedSchemaId)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return getMergedLocation(context, mergedSchemaId)
|
|
258
309
|
}
|
|
259
310
|
|
|
260
311
|
function getSchemaDependencies (refResolver, schemaId) {
|
|
@@ -375,6 +426,7 @@ function build (schema, options) {
|
|
|
375
426
|
validatorSchemasIds: new Set(),
|
|
376
427
|
validatorSchemaRefs: new Set(),
|
|
377
428
|
mergedSchemasIds: new Map(),
|
|
429
|
+
mergedRefsIds: new Map(),
|
|
378
430
|
maxDepth,
|
|
379
431
|
recursiveSchemas: new Set(),
|
|
380
432
|
recursivePaths: new Set(),
|
|
@@ -1055,7 +1107,10 @@ function buildArray (context, location, input) {
|
|
|
1055
1107
|
// Copying small arrays costs more than the existing loop.
|
|
1056
1108
|
if (typeof arrayLength === 'number' && arrayLength >= ${fastPath.minLength} && Number.isInteger(arrayLength) && arrayLength <= 0xffffffff) {
|
|
1057
1109
|
const copy = new Array(arrayLength)
|
|
1058
|
-
|
|
1110
|
+
// V8's side-effect-free JSON.stringify fast path (V8 >= 13.8) requires
|
|
1111
|
+
// the standard Array.prototype; a null prototype halves its throughput.
|
|
1112
|
+
// Only detach the prototype when a toJSON hook would otherwise be called.
|
|
1113
|
+
if (Array.prototype.toJSON !== undefined) Object.setPrototypeOf(copy, null)
|
|
1059
1114
|
for (let i = 0; i < arrayLength; i++) {
|
|
1060
1115
|
const v = obj[i]
|
|
1061
1116
|
if (!(${fastPath.condition})) {
|
package/lib/serializer.js
CHANGED
|
@@ -140,7 +140,10 @@ module.exports = class Serializer {
|
|
|
140
140
|
// matching buildSingleTypeSerializer without rereading getters.
|
|
141
141
|
asPlainStringArray (arr, len = arr.length) {
|
|
142
142
|
const copy = new Array(len)
|
|
143
|
-
|
|
143
|
+
// V8's side-effect-free JSON.stringify fast path (V8 >= 13.8) requires the
|
|
144
|
+
// standard Array.prototype; a null prototype halves its throughput. Only
|
|
145
|
+
// detach the prototype when a toJSON hook would otherwise be called.
|
|
146
|
+
if (Array.prototype.toJSON !== undefined) Object.setPrototypeOf(copy, null)
|
|
144
147
|
for (let i = 0; i < len; i++) {
|
|
145
148
|
const value = arr[i]
|
|
146
149
|
if (typeof value !== 'string') {
|
package/lib/validator.js
CHANGED
|
@@ -15,7 +15,7 @@ class Validator {
|
|
|
15
15
|
this.ajv = new Ajv({
|
|
16
16
|
...ajvOptions,
|
|
17
17
|
...codeOptions,
|
|
18
|
-
strictSchema: false,
|
|
18
|
+
strictSchema: ajvOptions?.strictSchema ?? false,
|
|
19
19
|
validateSchema: false,
|
|
20
20
|
allowUnionTypes: true,
|
|
21
21
|
uriResolver: fastUri
|
|
@@ -50,7 +50,7 @@ class Validator {
|
|
|
50
50
|
const ajvSchema = clone(schema)
|
|
51
51
|
this.convertSchemaToAjvFormat(ajvSchema)
|
|
52
52
|
this.ajv.addSchema(ajvSchema, schemaKey)
|
|
53
|
-
this._ajvSchemas[schemaKey] =
|
|
53
|
+
this._ajvSchemas[schemaKey] = ajvSchema
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const build = require('..')
|
|
5
|
+
|
|
6
|
+
// The copy-then-JSON.stringify fast path for primitive arrays must produce the
|
|
7
|
+
// same output whether or not Array.prototype.toJSON has been patched, and must
|
|
8
|
+
// never invoke such a hook.
|
|
9
|
+
|
|
10
|
+
const numbers = Array.from({ length: 1000 }, (_, i) => i * 1.37)
|
|
11
|
+
const integers = Array.from({ length: 1000 }, (_, i) => i)
|
|
12
|
+
const strings = Array.from({ length: 1000 }, (_, i) => 'item-' + i)
|
|
13
|
+
|
|
14
|
+
const cases = [
|
|
15
|
+
['number', { type: 'array', items: { type: 'number' } }, numbers, {}],
|
|
16
|
+
['integer', { type: 'array', items: { type: 'integer' } }, integers, {}],
|
|
17
|
+
['string', { type: 'array', items: { type: 'string' } }, strings, { largeArraySize: 100 }]
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
for (const [name, schema, data, options] of cases) {
|
|
21
|
+
test(`${name} array fast path with pristine Array.prototype`, t => {
|
|
22
|
+
t.plan(2)
|
|
23
|
+
t.assert.equal(Array.prototype.toJSON, undefined)
|
|
24
|
+
const stringify = build(schema, options)
|
|
25
|
+
t.assert.equal(stringify(data), JSON.stringify(data))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test(`${name} array fast path ignores a patched Array.prototype.toJSON`, t => {
|
|
29
|
+
t.plan(2)
|
|
30
|
+
const expected = JSON.stringify(data)
|
|
31
|
+
let called = 0
|
|
32
|
+
// eslint-disable-next-line no-extend-native
|
|
33
|
+
Array.prototype.toJSON = function () { called++; return 'hooked' }
|
|
34
|
+
t.after(() => { delete Array.prototype.toJSON })
|
|
35
|
+
const stringify = build(schema, options)
|
|
36
|
+
t.assert.equal(stringify(data), expected)
|
|
37
|
+
t.assert.equal(called, 0)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test(`${name} array fast path resumes per-item coercion after a miss, with patched toJSON`, t => {
|
|
41
|
+
t.plan(1)
|
|
42
|
+
const mixed = data.slice()
|
|
43
|
+
mixed[500] = name === 'string' ? 42 : '7'
|
|
44
|
+
const stringify = build(schema, options)
|
|
45
|
+
const expected = stringify(mixed)
|
|
46
|
+
// eslint-disable-next-line no-extend-native
|
|
47
|
+
Array.prototype.toJSON = function () { return 'hooked' }
|
|
48
|
+
t.after(() => { delete Array.prototype.toJSON })
|
|
49
|
+
t.assert.equal(build(schema, options)(mixed), expected)
|
|
50
|
+
})
|
|
51
|
+
}
|
package/test/ref.test.js
CHANGED
|
@@ -2075,3 +2075,230 @@ test('ref nested', (t) => {
|
|
|
2075
2075
|
t.assert.doesNotThrow(() => JSON.parse(output))
|
|
2076
2076
|
t.assert.equal(output, '{"str":"test"}')
|
|
2077
2077
|
})
|
|
2078
|
+
|
|
2079
|
+
test('ref internal - sibling keywords', (t) => {
|
|
2080
|
+
t.plan(3)
|
|
2081
|
+
|
|
2082
|
+
const schema = {
|
|
2083
|
+
definitions: {
|
|
2084
|
+
def: {
|
|
2085
|
+
type: 'object',
|
|
2086
|
+
properties: {
|
|
2087
|
+
str: {
|
|
2088
|
+
type: 'string'
|
|
2089
|
+
},
|
|
2090
|
+
num: {
|
|
2091
|
+
type: 'integer'
|
|
2092
|
+
}
|
|
2093
|
+
},
|
|
2094
|
+
required: ['str']
|
|
2095
|
+
}
|
|
2096
|
+
},
|
|
2097
|
+
type: 'object',
|
|
2098
|
+
properties: {
|
|
2099
|
+
obj: {
|
|
2100
|
+
$ref: '#/definitions/def',
|
|
2101
|
+
required: ['num']
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
const object = {
|
|
2107
|
+
obj: {
|
|
2108
|
+
str: 'test',
|
|
2109
|
+
num: 42
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
const stringify = build(schema)
|
|
2114
|
+
const output = stringify(object)
|
|
2115
|
+
|
|
2116
|
+
t.assert.doesNotThrow(() => JSON.parse(output))
|
|
2117
|
+
t.assert.equal(output, '{"obj":{"str":"test","num":42}}')
|
|
2118
|
+
|
|
2119
|
+
t.assert.throws(() => {
|
|
2120
|
+
stringify({
|
|
2121
|
+
obj: {
|
|
2122
|
+
str: 'test'
|
|
2123
|
+
}
|
|
2124
|
+
})
|
|
2125
|
+
}, { message: '"num" is required!' })
|
|
2126
|
+
})
|
|
2127
|
+
|
|
2128
|
+
test('ref external - sibling keywords', (t) => {
|
|
2129
|
+
t.plan(3)
|
|
2130
|
+
|
|
2131
|
+
const externalSchema = {
|
|
2132
|
+
external: {
|
|
2133
|
+
definitions: {
|
|
2134
|
+
def: {
|
|
2135
|
+
type: 'object',
|
|
2136
|
+
properties: {
|
|
2137
|
+
str: {
|
|
2138
|
+
type: 'string'
|
|
2139
|
+
},
|
|
2140
|
+
num: {
|
|
2141
|
+
type: 'integer'
|
|
2142
|
+
}
|
|
2143
|
+
},
|
|
2144
|
+
required: ['str']
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
const schema = {
|
|
2151
|
+
type: 'object',
|
|
2152
|
+
properties: {
|
|
2153
|
+
obj: {
|
|
2154
|
+
$ref: 'external#/definitions/def',
|
|
2155
|
+
required: ['num']
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
const object = {
|
|
2161
|
+
obj: {
|
|
2162
|
+
str: 'test',
|
|
2163
|
+
num: 42
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
const stringify = build(schema, { schema: externalSchema })
|
|
2168
|
+
const output = stringify(object)
|
|
2169
|
+
|
|
2170
|
+
t.assert.doesNotThrow(() => JSON.parse(output))
|
|
2171
|
+
t.assert.equal(output, '{"obj":{"str":"test","num":42}}')
|
|
2172
|
+
|
|
2173
|
+
t.assert.throws(() => {
|
|
2174
|
+
stringify({
|
|
2175
|
+
obj: {
|
|
2176
|
+
str: 'test'
|
|
2177
|
+
}
|
|
2178
|
+
})
|
|
2179
|
+
}, { message: '"num" is required!' })
|
|
2180
|
+
})
|
|
2181
|
+
|
|
2182
|
+
test('ref external - recursive sibling keywords', (t) => {
|
|
2183
|
+
t.plan(3)
|
|
2184
|
+
|
|
2185
|
+
const externalSchema = {
|
|
2186
|
+
node: {
|
|
2187
|
+
$id: 'node',
|
|
2188
|
+
type: 'object',
|
|
2189
|
+
properties: {
|
|
2190
|
+
str: {
|
|
2191
|
+
type: 'string'
|
|
2192
|
+
},
|
|
2193
|
+
next: {
|
|
2194
|
+
$ref: 'node#',
|
|
2195
|
+
required: ['str']
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
const schema = {
|
|
2202
|
+
type: 'object',
|
|
2203
|
+
properties: {
|
|
2204
|
+
root: {
|
|
2205
|
+
$ref: 'node#',
|
|
2206
|
+
required: ['str']
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
const object = {
|
|
2212
|
+
root: {
|
|
2213
|
+
str: 'test',
|
|
2214
|
+
next: {
|
|
2215
|
+
str: 'nested'
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
const stringify = build(schema, { schema: externalSchema })
|
|
2221
|
+
const output = stringify(object)
|
|
2222
|
+
|
|
2223
|
+
t.assert.doesNotThrow(() => JSON.parse(output))
|
|
2224
|
+
t.assert.equal(output, '{"root":{"str":"test","next":{"str":"nested"}}}')
|
|
2225
|
+
|
|
2226
|
+
t.assert.throws(() => {
|
|
2227
|
+
stringify({
|
|
2228
|
+
root: {
|
|
2229
|
+
str: 'test',
|
|
2230
|
+
next: {}
|
|
2231
|
+
}
|
|
2232
|
+
})
|
|
2233
|
+
}, { message: '"str" is required!' })
|
|
2234
|
+
})
|
|
2235
|
+
|
|
2236
|
+
test('ref internal - conflicting sibling keywords', (t) => {
|
|
2237
|
+
t.plan(2)
|
|
2238
|
+
|
|
2239
|
+
const schema = {
|
|
2240
|
+
definitions: {
|
|
2241
|
+
def: {
|
|
2242
|
+
type: 'string'
|
|
2243
|
+
}
|
|
2244
|
+
},
|
|
2245
|
+
type: 'object',
|
|
2246
|
+
properties: {
|
|
2247
|
+
value: {
|
|
2248
|
+
$ref: '#/definitions/def',
|
|
2249
|
+
type: 'integer'
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
const object = {
|
|
2255
|
+
value: 42
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
const stringify = build(schema)
|
|
2259
|
+
const output = stringify(object)
|
|
2260
|
+
|
|
2261
|
+
t.assert.doesNotThrow(() => JSON.parse(output))
|
|
2262
|
+
t.assert.equal(output, '{"value":"42"}')
|
|
2263
|
+
})
|
|
2264
|
+
|
|
2265
|
+
test('ref external - sibling keywords with a duplicated anchor', (t) => {
|
|
2266
|
+
t.plan(1)
|
|
2267
|
+
|
|
2268
|
+
const externalSchema = {
|
|
2269
|
+
external: {
|
|
2270
|
+
$id: 'external',
|
|
2271
|
+
definitions: {
|
|
2272
|
+
def: {
|
|
2273
|
+
type: 'object',
|
|
2274
|
+
properties: {
|
|
2275
|
+
str: {
|
|
2276
|
+
$id: '#anchor',
|
|
2277
|
+
type: 'string'
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
const schema = {
|
|
2286
|
+
type: 'object',
|
|
2287
|
+
properties: {
|
|
2288
|
+
obj: {
|
|
2289
|
+
$ref: 'external#/definitions/def',
|
|
2290
|
+
properties: {
|
|
2291
|
+
num: {
|
|
2292
|
+
$id: '#anchor',
|
|
2293
|
+
type: 'integer'
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
t.assert.throws(
|
|
2301
|
+
() => build(schema, { schema: externalSchema }),
|
|
2302
|
+
{ message: /There is already another anchor "#anchor"/ }
|
|
2303
|
+
)
|
|
2304
|
+
})
|
|
@@ -318,6 +318,38 @@ test('inline validators with external oneOf refs and toJSON values', async (t) =
|
|
|
318
318
|
t.assert.throws(() => stringify({ value: true }), /does not match schema definition/)
|
|
319
319
|
})
|
|
320
320
|
|
|
321
|
+
test('restored validator handles toJSON values', async (t) => {
|
|
322
|
+
t.plan(3)
|
|
323
|
+
|
|
324
|
+
after(async () => {
|
|
325
|
+
await fs.promises.rm(destination, { force: true })
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
const code = fjs({
|
|
329
|
+
type: 'object',
|
|
330
|
+
properties: {
|
|
331
|
+
value: {
|
|
332
|
+
oneOf: [
|
|
333
|
+
{ type: 'string', format: 'date-time' },
|
|
334
|
+
{ type: 'integer' }
|
|
335
|
+
]
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}, { mode: 'standalone', inlineValidators: false })
|
|
339
|
+
|
|
340
|
+
const destination = path.resolve(tmpDir, 'standalone-restored-toJSON.js')
|
|
341
|
+
|
|
342
|
+
await fs.promises.writeFile(destination, code)
|
|
343
|
+
const stringify = require(destination)
|
|
344
|
+
|
|
345
|
+
t.assert.equal(
|
|
346
|
+
stringify({ value: new Date('2020-01-02T03:04:05.000Z') }),
|
|
347
|
+
'{"value":"2020-01-02T03:04:05.000Z"}'
|
|
348
|
+
)
|
|
349
|
+
t.assert.equal(stringify({ value: 42 }), '{"value":42}')
|
|
350
|
+
t.assert.throws(() => stringify({ value: true }), /does not match schema definition/)
|
|
351
|
+
})
|
|
352
|
+
|
|
321
353
|
test('does not emit unused inline validators', (t) => {
|
|
322
354
|
const code = build({ mode: 'standalone', inlineValidators: true })
|
|
323
355
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const build = require('..')
|
|
5
|
+
|
|
6
|
+
function schemaWithTypo () {
|
|
7
|
+
return { anyOf: [{ type: 'number', mininum: 10 }] }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
for (const compileValidators of [false, true]) {
|
|
11
|
+
const mode = compileValidators ? 'eager' : 'lazy'
|
|
12
|
+
|
|
13
|
+
for (const [name, ajv] of [
|
|
14
|
+
['omitted', undefined],
|
|
15
|
+
['empty', {}],
|
|
16
|
+
['undefined', { strictSchema: undefined }],
|
|
17
|
+
['null', { strictSchema: null }],
|
|
18
|
+
['false', { strictSchema: false }],
|
|
19
|
+
['strict true', { strict: true }],
|
|
20
|
+
['explicit false with strict true', { strict: true, strictSchema: false }]
|
|
21
|
+
]) {
|
|
22
|
+
test(`strictSchema keeps permissive behavior for ${name} (${mode})`, t => {
|
|
23
|
+
const stringify = build(schemaWithTypo(), { ajv, compileValidators })
|
|
24
|
+
t.assert.equal(stringify(1), '1')
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test(`strictSchema true rejects unknown keywords (${mode})`, t => {
|
|
29
|
+
const options = { ajv: { strictSchema: true }, compileValidators }
|
|
30
|
+
const unknownKeyword = /unknown keyword: "mininum"/
|
|
31
|
+
|
|
32
|
+
if (compileValidators) {
|
|
33
|
+
t.assert.throws(() => build(schemaWithTypo(), options), unknownKeyword)
|
|
34
|
+
} else {
|
|
35
|
+
const stringify = build(schemaWithTypo(), options)
|
|
36
|
+
t.assert.throws(() => stringify(1), unknownKeyword)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test(`strictSchema log warns about unknown keywords (${mode})`, t => {
|
|
41
|
+
const warnings = []
|
|
42
|
+
const logger = {
|
|
43
|
+
log () {},
|
|
44
|
+
error () {},
|
|
45
|
+
warn (message) { warnings.push(message) }
|
|
46
|
+
}
|
|
47
|
+
const stringify = build(schemaWithTypo(), {
|
|
48
|
+
ajv: { strictSchema: 'log', logger },
|
|
49
|
+
compileValidators
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
t.assert.equal(stringify(1), '1')
|
|
53
|
+
t.assert.ok(warnings.some(message => message.includes('unknown keyword: "mininum"')))
|
|
54
|
+
})
|
|
55
|
+
}
|