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
26 changes: 17 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ function getSchemaId (schema, rootSchemaId) {
return rootSchemaId
}

// a schema ref on a comment line of the generated code: a line terminator in a property name
// would end the comment and turn the rest of the name into code. JSON.stringify covers \n and
// \r, the two separators JS also treats as line terminators are escaped by hand
const LINE_SEPARATORS = /[\u2028\u2029]/g
function asComment (schemaRef) {
return JSON.stringify(schemaRef).replace(LINE_SEPARATORS, (c) => '\\u' + c.charCodeAt(0).toString(16))
}

function getSafeSchemaRef (context, location) {
let schemaRef = location.getSchemaRef() || ''
if (schemaRef.startsWith(context.rootSchemaId)) {
Expand Down Expand Up @@ -633,7 +641,7 @@ function buildObject (context, location, input) {
const schemaRef = getSafeSchemaRef(context, location)

const functionCode = `
// ${schemaRef}
// ${asComment(schemaRef)}
function ${functionName} (input) {
const obj = ${toJSON('input')}
if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_OBJECT'}
Expand Down Expand Up @@ -693,14 +701,14 @@ function buildArray (context, location, input) {

let functionCode = `
function ${functionName} (obj) {
// ${schemaRef}
// ${asComment(schemaRef)}
let json = ''
`

functionCode += `
if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
if (!Array.isArray(obj)) {
throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
throw new TypeError(${JSON.stringify(`The value of '${schemaRef}' does not match schema definition.`)})
}
const arrayLength = obj.length
`
Expand Down Expand Up @@ -785,7 +793,7 @@ function buildArray (context, location, input) {
if (${objVar} === null) {
json += ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
} else if (!Array.isArray(${objVar})) {
throw new TypeError(\`The value of '${safeSchemaRef}' does not match schema definition.\`)
throw new TypeError(${JSON.stringify(`The value of '${safeSchemaRef}' does not match schema definition.`)})
} else {
const arrayLength_${objVar} = ${objVar}.length
`
Expand Down Expand Up @@ -987,7 +995,7 @@ function buildMultiTypeSerializer (context, location, input) {
}
})
code += `
else throw new TypeError(\`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.\`)
else throw new TypeError(${JSON.stringify(`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.`)})
`

return code
Expand Down Expand Up @@ -1228,14 +1236,14 @@ function buildOneOf (context, location, input) {
context.validatorSchemaRefs.add(schemaRef)

code += `
${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input})) {
${index === 0 ? 'if' : 'else if'}(validator.validate(${JSON.stringify(schemaRef)}, ${input})) {
${nestedResult}
}
`
}

code += `
else throw new TypeError(\`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.\`)
else throw new TypeError(${JSON.stringify(`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.`)})
`

return code
Expand Down Expand Up @@ -1278,7 +1286,7 @@ function buildIfThenElse (context, location, input) {

if (!elseSchema) {
return `
if (validator.validate("${ifSchemaRef}", ${input})) {
if (validator.validate(${JSON.stringify(ifSchemaRef)}, ${input})) {
${buildValue(context, thenMergedLocation, input)}
} else {
${buildValue(context, rootLocation, input)}
Expand All @@ -1302,7 +1310,7 @@ function buildIfThenElse (context, location, input) {
}

return `
if (validator.validate("${ifSchemaRef}", ${input})) {
if (validator.validate(${JSON.stringify(ifSchemaRef)}, ${input})) {
${buildValue(context, thenMergedLocation, input)}
} else {
${buildValue(context, elseMergedLocation, input)}
Expand Down
4 changes: 3 additions & 1 deletion lib/location.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ class Location {
}

getPropertyLocation (propertyName) {
// a JSON pointer segment, so a property name with a / or a ~ still points at the property
const segment = String(propertyName).replace(/~/g, '~0').replace(/\//g, '~1')
const propertyLocation = new Location(
this.schema[propertyName],
this.schemaId,
this.jsonPointer + '/' + propertyName
this.jsonPointer + '/' + segment
)
return propertyLocation
}
Expand Down
41 changes: 41 additions & 0 deletions test/property-names.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict'

const { test } = require('node:test')
const build = require('..')

// the schema ref of a property with anyOf, oneOf or if goes into the generated code as a
// string and into a JSON pointer, so a name that needs escaping in either broke the build
const names = ['new\nline', 'q"uote', 'back\\slash', 'a/b', 'a~b', 'a%b', 'a#b', 'a b', 'tick`', 'dollar$' + '{x}', '\\u', "single'quote"]
const branches = {
oneOf: { oneOf: [{ type: 'string' }, { type: 'object', properties: { k: { type: 'integer' } }, required: ['k'] }] },
anyOf: { anyOf: [{ type: 'string' }, { type: 'object', properties: { k: { type: 'integer' } }, required: ['k'] }] },
if: { if: { type: 'string' }, then: { type: 'string' }, else: { type: 'integer' } }
}

for (const name of names) {
for (const [keyword, subschema] of Object.entries(branches)) {
test(`a property named ${JSON.stringify(name)} with ${keyword}`, (t) => {
t.plan(2)

const stringify = build({ type: 'object', properties: { [name]: subschema } })
t.assert.equal(stringify({ [name]: 'v' }), JSON.stringify({ [name]: 'v' }))
t.assert.throws(() => stringify({ [name]: {} }), keyword === 'if' ? Error : new TypeError(`The value of '#/properties/${name.replace(/~/g, '~0').replace(/\//g, '~1')}' does not match schema definition.`))
})
}
}

// the ref also goes on a comment line of the generated code, above the function of a nested
// object or array, where a line terminator in the name ends the comment early
for (const name of ['new' + String.fromCharCode(10) + 'line', 'line' + String.fromCharCode(0x2028) + 'sep', 'para' + String.fromCharCode(0x2029) + 'graph']) {
test(`a property named ${JSON.stringify(name)} with a nested object and array`, (t) => {
t.plan(1)

const stringify = build({
type: 'object',
properties: {
[name]: { type: 'object', properties: { list: { type: 'array', items: { type: 'integer' } } }, nullable: true }
}
})
t.assert.equal(stringify({ [name]: { list: [1] } }), JSON.stringify({ [name]: { list: [1] } }))
})
}