Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions benchmark/bench-thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ const bench = new Bench({
})

const FJS = require('..')
const stringify = FJS(benchmark.schema)
const stringify = benchmark.compile ? null : FJS(benchmark.schema, benchmark.options)

bench.add(benchmark.name, () => {
stringify(benchmark.input)
if (benchmark.compile) {
FJS(benchmark.schema, benchmark.options)(benchmark.input)
} else {
stringify(benchmark.input)
}
}).run().then(() => {
const task = bench.tasks[0]
const hz = task.result.throughput.mean // ops/sec
Expand Down
86 changes: 86 additions & 0 deletions benchmark/bench.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,92 @@ const benchmarks = [
},
input: { firstName: 'Max', lastName: 'Power', age: 22 }
},
{
name: 'oneOf external refs compile and first stringify',
compile: true,
schema: {
title: 'External refs oneOf schema',
type: 'object',
properties: {
content: {
oneOf: [
{ $ref: 'BenchmarkFoo#' },
{ $ref: 'BenchmarkBar#' },
{ $ref: 'BenchmarkBaz#' }
]
}
}
},
options: {
schema: {
BenchmarkFoo: {
type: 'object',
properties: {
kind: { const: 'foo' },
title: { type: 'string' },
count: { type: 'integer' },
flags: {
type: 'array',
items: { type: 'string' }
}
},
required: ['kind', 'title', 'count', 'flags'],
additionalProperties: false
},
BenchmarkBar: {
type: 'object',
properties: {
kind: { const: 'bar' },
name: { type: 'string' },
value: { type: 'number' },
meta: {
type: 'object',
properties: {
active: { type: 'boolean' },
tags: {
type: 'array',
items: { type: 'string' }
}
},
required: ['active', 'tags'],
additionalProperties: false
}
},
required: ['kind', 'name', 'value', 'meta'],
additionalProperties: false
},
BenchmarkBaz: {
type: 'object',
properties: {
kind: { const: 'baz' },
id: { type: 'string' },
nested: {
type: 'object',
properties: {
score: { type: 'number' },
note: { type: 'string' }
},
required: ['score', 'note'],
additionalProperties: false
}
},
required: ['kind', 'id', 'nested'],
additionalProperties: false
}
}
},
input: {
content: {
kind: 'bar',
name: 'benchmark',
value: 42.5,
meta: {
active: true,
tags: ['one', 'two', 'three']
}
}
}
},
{
name: 'object with const string property',
schema: {
Expand Down
49 changes: 47 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,51 @@ function getSafeSchemaRef (context, location) {
return schemaRef
}

function getValidatorSchemaRef (context, location) {
let schemaRef = location.getSchemaRef()
const schema = location.schema

// A schema that is only a registered $ref can validate against that target
// instead of a JSON-pointer path through the root schema.
if (
typeof schema.$ref !== 'string' ||
Object.keys(schema).length !== 1
) {
return schemaRef
}

const ref = schema.$ref
const hashIndex = ref.indexOf('#')
const refSchemaId = hashIndex === -1 ? ref : ref.slice(0, hashIndex)
const refJsonPointer = hashIndex === -1 ? '' : ref.slice(hashIndex)
const isBareRef = refJsonPointer === '' || refJsonPointer === '#'
const isNamedFragmentRef = /^#[A-Za-z_][-A-Za-z0-9._]*$/.test(refJsonPointer)
const isJsonPointerRef = refJsonPointer.startsWith('#/')

if (
(isBareRef || isNamedFragmentRef || isJsonPointerRef) &&
refSchemaId &&
context.refResolver.hasSchema(refSchemaId)
) {
const refSchema = context.refResolver.getSchema(refSchemaId)
const refSubSchema = isBareRef
? refSchema
: context.refResolver.getSchema(refSchemaId, refJsonPointer)
const hasRelativeSchemaId = (
typeof refSchema.$id === 'string' &&
refSchema.$id.charAt(0) === '#'
)
if (
refSubSchema !== null &&
(!hasRelativeSchemaId || refSchema.$id === refJsonPointer)
) {
schemaRef = refSchemaId + (isBareRef ? '#' : refJsonPointer)
}
}

return schemaRef
}

function build (schema, options) {
isValidSchema(schema)

Expand Down Expand Up @@ -1148,7 +1193,7 @@ function buildOneOf (context, location, input) {
}

const nestedResult = buildValue(context, mergedLocation, input)
const schemaRef = optionLocation.getSchemaRef()
const schemaRef = getValidatorSchemaRef(context, optionLocation)

code += `
${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input})) {
Expand Down Expand Up @@ -1181,7 +1226,7 @@ function buildIfThenElse (context, location, input) {
)

const ifLocation = location.getPropertyLocation('if')
const ifSchemaRef = ifLocation.getSchemaRef()
const ifSchemaRef = getValidatorSchemaRef(context, ifLocation)

const thenLocation = location.getPropertyLocation('then')
let thenMergedSchemaId = context.mergedSchemasIds.get(thenSchema)
Expand Down
42 changes: 42 additions & 0 deletions test/anyof.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,48 @@ test('anyOf and $ref - multiple external $ref', (t) => {
t.assert.equal(output, '{"obj":{"prop":{"prop2":"test"}}}')
})

test('anyOf with registered $ref branches validates directly against the ref target', (t) => {
t.plan(3)

const schema = {
title: 'anyOf with registered $ref branches',
type: 'object',
properties: {
content: {
anyOf: [
{ $ref: 'Foo#' },
{ $ref: 'Bar#/definitions/Baz' }
]
}
}
}
const externalSchema = {
Foo: {
type: 'object',
properties: { a: { type: 'string' } },
required: ['a'],
additionalProperties: false
},
Bar: {
definitions: {
Baz: {
type: 'object',
properties: { b: { type: 'string' } },
required: ['b'],
additionalProperties: false
}
}
}
}

const code = build(schema, { mode: 'standalone', schema: externalSchema })
t.assert.ok(code.includes('validator.validate("Foo#"'), 'validates directly against the bare registered id')
t.assert.ok(code.includes('validator.validate("Bar#/definitions/Baz"'), 'validates directly against the registered sub-path ref')

const stringify = build(schema, { schema: externalSchema })
t.assert.equal(stringify({ content: { b: 'yo' } }), '{"content":{"b":"yo"}}')
})

test('anyOf looks for all of the array items', (t) => {
t.plan(1)

Expand Down
112 changes: 112 additions & 0 deletions test/if-then-else.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -538,3 +538,115 @@ test('if-then-else with allOf', (t) => {
t.assert.doesNotThrow(() => JSON.parse(output))
t.assert.equal(output, '{"base":"test","foo":"value"}')
})

test('if with a bare top-level $ref validates directly against the registered schema', (t) => {
t.plan(2)

const schema = {
type: 'object',
if: { $ref: 'IsFoo#' },
then: {
type: 'object',
properties: {
foo: { type: 'string' }
}
},
else: {
type: 'object',
properties: {
bar: { type: 'string' }
}
}
}
const externalSchema = {
IsFoo: {
type: 'object',
properties: {
kind: { type: 'string', const: 'foo' }
},
required: ['kind']
}
}

const code = build(schema, { mode: 'standalone', schema: externalSchema })
t.assert.ok(code.includes('validator.validate("IsFoo#"'), 'validates directly against the bare registered id')

const stringify = build(schema, { schema: externalSchema })
t.assert.equal(stringify({ kind: 'foo', foo: 'value', bar: 'ignored' }), '{"foo":"value"}')
})

test('if with a $ref into a JSON-pointer sub-path validates directly against that sub-path', (t) => {
t.plan(2)

const schema = {
type: 'object',
if: { $ref: 'Rules#/definitions/IsFoo' },
then: {
type: 'object',
properties: {
foo: { type: 'string' }
}
},
else: {
type: 'object',
properties: {
bar: { type: 'string' }
}
}
}
const externalSchema = {
Rules: {
definitions: {
IsFoo: {
type: 'object',
properties: {
kind: { type: 'string', const: 'foo' }
},
required: ['kind']
}
}
}
}

const code = build(schema, { mode: 'standalone', schema: externalSchema })
t.assert.ok(code.includes('validator.validate("Rules#/definitions/IsFoo"'), 'validates directly against the registered sub-path ref')

const stringify = build(schema, { schema: externalSchema })
t.assert.equal(stringify({ kind: 'foo', foo: 'value', bar: 'ignored' }), '{"foo":"value"}')
})

test('if with a $ref that has sibling keywords is left unchanged', (t) => {
t.plan(2)

const schema = {
type: 'object',
if: { $ref: 'IsFoo#', required: ['extra'] },
then: {
type: 'object',
properties: {
foo: { type: 'string' }
}
},
else: {
type: 'object',
properties: {
bar: { type: 'string' }
}
}
}
const externalSchema = {
IsFoo: {
type: 'object',
properties: {
kind: { type: 'string', const: 'foo' }
},
required: ['kind']
}
}

const code = build(schema, { mode: 'standalone', schema: externalSchema })
t.assert.ok(!code.includes('validator.validate("IsFoo#"'), 'ref with sibling keywords is not redirected')

const stringify = build(schema, { schema: externalSchema })
t.assert.equal(stringify({ kind: 'foo', foo: 'ignored', bar: 'fallback' }), '{"bar":"fallback"}')
})
Loading
Loading