Skip to content
Merged
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
114 changes: 86 additions & 28 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function build (schema, options) {

var dependencies = []
var dependenciesName = []
if (hasAnyOf(schema)) {
if (hasAnyOf(schema) || hasArrayOfTypes(schema)) {
dependencies.push(new Ajv())
dependenciesName.push('ajv')
}
Expand All @@ -113,6 +113,40 @@ function hasAnyOf (schema) {
return false
}

function hasArrayOfTypes (schema) {
if (Array.isArray(schema.type)) { return true }
var i

if (schema.type === 'object') {
if (schema.properties) {
var propertyKeys = Object.keys(schema.properties)
for (i = 0; i < propertyKeys.length; i++) {
if (hasArrayOfTypes(schema.properties[propertyKeys[i]])) {
return true
}
}
}
} else if (schema.type === 'array') {
if (Array.isArray(schema.items)) {
for (i = 0; i < schema.items.length; i++) {
if (hasArrayOfTypes(schema.items[i])) {
return true
}
}
} else if (schema.items) {
return hasArrayOfTypes(schema.items)
}
} else if (Array.isArray(schema.anyOf)) {
for (i = 0; i < schema.anyOf.length; i++) {
if (hasArrayOfTypes(schema.anyOf[i])) {
return true
}
}
}

return false
}

function $asNull () {
return 'null'
}
Expand Down Expand Up @@ -493,32 +527,7 @@ function buildArray (schema, code, name, externalSchema, fullSchema) {
result = schema.items.reduce((res, item, i) => {
var accessor = '[i]'
const tmpRes = nested(laterCode, name, accessor, item, externalSchema, fullSchema, i)
var condition = `i === ${i} && `
switch (item.type) {
case 'null':
condition += `obj${accessor} === null`
break
case 'string':
condition += `typeof obj${accessor} === 'string'`
break
case 'integer':
condition += `Number.isInteger(obj${accessor})`
break
case 'number':
condition += `Number.isFinite(obj${accessor})`
break
case 'boolean':
condition += `typeof obj${accessor} === 'boolean'`
break
case 'object':
condition += `obj${accessor} && typeof obj${accessor} === 'object' && obj${accessor}.constructor === Object`
break
case 'array':
condition += `Array.isArray(obj${accessor})`
break
default:
throw new Error(`${item.type} unsupported`)
}
var condition = `i === ${i} && ${buildArrayTypeCondition(item.type, accessor)}`
return {
code: `${res.code}
${i > 0 ? 'else' : ''} if (${condition}) {
Expand Down Expand Up @@ -561,6 +570,43 @@ function buildArray (schema, code, name, externalSchema, fullSchema) {
return code
}

function buildArrayTypeCondition (type, accessor) {
var condition
switch (type) {
case 'null':
condition = `obj${accessor} === null`
break
case 'string':
condition = `typeof obj${accessor} === 'string'`
break
case 'integer':
condition = `Number.isInteger(obj${accessor})`
break
case 'number':
condition = `Number.isFinite(obj${accessor})`
break
case 'boolean':
condition = `typeof obj${accessor} === 'boolean'`
break
case 'object':
condition = `obj${accessor} && typeof obj${accessor} === 'object' && obj${accessor}.constructor === Object`
break
case 'array':
condition = `Array.isArray(obj${accessor})`
break
default:
if (Array.isArray(type)) {
var conditions = type.map((subType) => {
return buildArrayTypeCondition(subType, accessor)
})
condition = `(${conditions.join(' || ')})`
Copy link
Contributor Author

@florianreinhart florianreinhart Feb 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is potential for an optimization here. In the current implementation the generated code checks if the item matches any of the types. The nested() function basically performs the same check again using ajv. We could split this condition here into separate ones and use the knowledge of the item's type for the nested() function. However, this requires some more refactoring.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to experiment! We do like optimizations! :D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm landing this now, feel free to send a followup PR with the optimizations!

} else {
throw new Error(`${type} unsupported`)
}
}
return condition
}

function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKey) {
var code = ''
var funcName
Expand Down Expand Up @@ -622,7 +668,19 @@ function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKe
} else throw new Error(`${schema} unsupported`)
break
default:
throw new Error(`${type} unsupported`)
if (Array.isArray(type)) {
type.forEach((type, index) => {
var tempSchema = {type: type}
var nestedResult = nested(laterCode, name, key, tempSchema, externalSchema, fullSchema, subKey)
code += `
${index === 0 ? 'if' : 'else if'}(ajv.validate(${require('util').inspect(tempSchema, {depth: null})}, obj${accessor}))
${nestedResult.code}
`
laterCode = nestedResult.laterCode
})
} else {
throw new Error(`${type} unsupported`)
}
}

return {
Expand Down
184 changes: 184 additions & 0 deletions test/typesArray.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
'use strict'

const test = require('tap').test
const build = require('..')

test('simple object with multi-type property', (t) => {
t.plan(2)

const schema = {
title: 'simple object with multi-type property',
type: 'object',
properties: {
stringOrNumber: {
type: ['string', 'number']
}
}
}
const stringify = build(schema)

try {
const value = stringify({
stringOrNumber: 'string'
})
t.is(value, '{"stringOrNumber":"string"}')
} catch (e) {
t.fail()
}

try {
const value = stringify({
stringOrNumber: 42
})
t.is(value, '{"stringOrNumber":42}')
} catch (e) {
t.fail()
}
})

test('object with array of multiple types', (t) => {
t.plan(3)

const schema = {
title: 'object with array of multiple types',
type: 'object',
properties: {
arrayOfStringsAndNumbers: {
type: 'array',
items: {
type: ['string', 'number']
}
}
}
}
const stringify = build(schema)

try {
const value = stringify({
arrayOfStringsAndNumbers: ['string1', 'string2']
})
t.is(value, '{"arrayOfStringsAndNumbers":["string1","string2"]}')
} catch (e) {
console.log(e)
t.fail()
}

try {
const value = stringify({
arrayOfStringsAndNumbers: [42, 7]
})
t.is(value, '{"arrayOfStringsAndNumbers":[42,7]}')
} catch (e) {
t.fail()
}

try {
const value = stringify({
arrayOfStringsAndNumbers: ['string1', 42, 7, 'string2']
})
t.is(value, '{"arrayOfStringsAndNumbers":["string1",42,7,"string2"]}')
} catch (e) {
t.fail()
}
})

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.is(value, '{"fixedTupleOfStringsAndNumbers":["string1",42,7]}')
} catch (e) {
console.log(e)
t.fail()
}

try {
const value = stringify({
fixedTupleOfStringsAndNumbers: ['string1', 42, 'string2']
})
t.is(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)

try {
const value = stringify({
objectOrBoolean: { stringOrNumber: 'string' }
})
t.is(value, '{"objectOrBoolean":{"stringOrNumber":"string"}}')
} catch (e) {
console.log(e)
t.fail()
}

try {
const value = stringify({
objectOrBoolean: { stringOrNumber: 42 }
})
t.is(value, '{"objectOrBoolean":{"stringOrNumber":42}}')
} catch (e) {
t.fail()
}

try {
const value = stringify({
objectOrBoolean: true
})
t.is(value, '{"objectOrBoolean":true}')
} catch (e) {
t.fail()
}
})