Skip to content

Commit e4d25ea

Browse files
authored
fix: avoid side effect per oneOf and anyOf (#291)
* fix: avoid side effect * remove inspect
1 parent 8b3d849 commit e4d25ea

File tree

3 files changed

+135
-2
lines changed

3 files changed

+135
-2
lines changed

index.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
const Ajv = require('ajv')
66
const merge = require('deepmerge')
7+
const clone = require('rfdc')({ proto: true })
8+
const fjsCloned = Symbol('fast-json-stringify.cloned')
79

810
const validate = require('./schema-validator')
911
let stringSimilarity = null
@@ -1069,6 +1071,12 @@ function buildArrayTypeCondition (type, accessor) {
10691071
}
10701072

10711073
function dereferenceOfRefs (location, type) {
1074+
if (!location.schema[fjsCloned]) {
1075+
const schemaClone = clone(location.schema)
1076+
schemaClone[fjsCloned] = true
1077+
location.schema = schemaClone
1078+
}
1079+
10721080
const schema = location.schema
10731081
const locations = []
10741082

@@ -1193,7 +1201,7 @@ function nested (laterCode, name, key, location, subKey, isArray) {
11931201

11941202
// see comment on anyOf about derefencing the schema before calling ajv.validate
11951203
code += `
1196-
${index === 0 ? 'if' : 'else if'}(ajv.validate(${require('util').inspect(location.schema, { depth: null, maxArrayLength: null })}, obj${accessor}))
1204+
${index === 0 ? 'if' : 'else if'}(ajv.validate(${JSON.stringify(location.schema)}, obj${accessor}))
11971205
${nestedResult.code}
11981206
`
11991207
laterCode = nestedResult.laterCode
@@ -1207,7 +1215,7 @@ function nested (laterCode, name, key, location, subKey, isArray) {
12071215
`
12081216
} else if ('const' in schema) {
12091217
code += `
1210-
if(ajv.validate(${require('util').inspect(schema, { depth: null })}, obj${accessor}))
1218+
if(ajv.validate(${require('util').inspect(schema, { depth: null, showHidden: false })}, obj${accessor}))
12111219
json += '${JSON.stringify(schema.const)}'
12121220
else
12131221
throw new Error(\`Item $\{JSON.stringify(obj${accessor})} does not match schema definition.\`)

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"types": "index.d.ts",
77
"scripts": {
88
"benchmark": "node bench.js",
9+
"lint:fix": "standard --fix",
910
"test:lint": "standard",
1011
"test:typescript": "tsc --project ./test/types/tsconfig.json",
1112
"test:unit": "tap -J test/*.test.js test/**/*.test.js",
@@ -45,6 +46,7 @@
4546
"dependencies": {
4647
"ajv": "^6.11.0",
4748
"deepmerge": "^4.2.2",
49+
"rfdc": "^1.2.0",
4850
"string-similarity": "^4.0.1"
4951
},
5052
"engines": {

test/side-effect.test.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
'use strict'
2+
3+
const { test } = require('tap')
4+
const clone = require('rfdc/default')
5+
const build = require('..')
6+
7+
test('oneOf with $ref should not change the input schema', t => {
8+
t.plan(2)
9+
10+
const referenceSchema = {
11+
$id: 'externalId',
12+
type: 'object',
13+
properties: {
14+
name: { type: 'string' }
15+
}
16+
}
17+
18+
const schema = {
19+
$id: 'mainSchema',
20+
type: 'object',
21+
properties: {
22+
people: {
23+
oneOf: [{ $ref: 'externalId' }]
24+
}
25+
}
26+
}
27+
const clonedSchema = clone(schema)
28+
const stringify = build(schema, {
29+
schema: {
30+
[referenceSchema.$id]: referenceSchema
31+
}
32+
})
33+
34+
const value = stringify({ people: { name: 'hello', foo: 'bar' } })
35+
t.is(value, '{"people":{"name":"hello"}}')
36+
t.deepEqual(schema, clonedSchema)
37+
})
38+
39+
test('oneOf and anyOf with $ref should not change the input schema', t => {
40+
t.plan(3)
41+
42+
const referenceSchema = {
43+
$id: 'externalSchema',
44+
type: 'object',
45+
properties: {
46+
name: { type: 'string' }
47+
}
48+
}
49+
50+
const schema = {
51+
$id: 'rootSchema',
52+
type: 'object',
53+
properties: {
54+
people: {
55+
oneOf: [{ $ref: 'externalSchema' }]
56+
},
57+
love: {
58+
anyOf: [
59+
{ $ref: '#/definitions/foo' },
60+
{ type: 'boolean' }
61+
]
62+
}
63+
},
64+
definitions: {
65+
foo: { type: 'string' }
66+
}
67+
}
68+
const clonedSchema = clone(schema)
69+
const stringify = build(schema, {
70+
schema: {
71+
[referenceSchema.$id]: referenceSchema
72+
}
73+
})
74+
75+
const valueAny1 = stringify({ people: { name: 'hello', foo: 'bar' }, love: 'music' })
76+
const valueAny2 = stringify({ people: { name: 'hello', foo: 'bar' }, love: true })
77+
78+
t.equal(valueAny1, '{"people":{"name":"hello"},"love":"music"}')
79+
t.equal(valueAny2, '{"people":{"name":"hello"},"love":true}')
80+
t.deepEqual(schema, clonedSchema)
81+
})
82+
83+
test('multiple $ref tree', t => {
84+
t.plan(2)
85+
86+
const referenceDeepSchema = {
87+
$id: 'deepId',
88+
type: 'number'
89+
}
90+
91+
const referenceSchema = {
92+
$id: 'externalId',
93+
type: 'object',
94+
properties: {
95+
name: { $ref: '#/definitions/foo' },
96+
age: { $ref: 'deepId' }
97+
},
98+
definitions: {
99+
foo: { type: 'string' }
100+
}
101+
}
102+
103+
const schema = {
104+
$id: 'mainSchema',
105+
type: 'object',
106+
properties: {
107+
people: {
108+
oneOf: [{ $ref: 'externalId' }]
109+
}
110+
}
111+
}
112+
const clonedSchema = clone(schema)
113+
const stringify = build(schema, {
114+
schema: {
115+
[referenceDeepSchema.$id]: referenceDeepSchema,
116+
[referenceSchema.$id]: referenceSchema
117+
}
118+
})
119+
120+
const value = stringify({ people: { name: 'hello', foo: 'bar', age: 42 } })
121+
t.is(value, '{"people":{"name":"hello","age":42}}')
122+
t.deepEqual(schema, clonedSchema)
123+
})

0 commit comments

Comments
 (0)