From 56f269080ac9c22aa3e15090a4d5548351428926 Mon Sep 17 00:00:00 2001 From: Florian Reinhart Date: Thu, 22 Feb 2018 14:25:18 +0100 Subject: [PATCH] Add support for array of types in schema definition --- index.js | 114 +++++++++++++++++++------ test/typesArray.test.js | 184 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 270 insertions(+), 28 deletions(-) create mode 100644 test/typesArray.test.js diff --git a/index.js b/index.js index cf3dc7f2..a61c2b26 100644 --- a/index.js +++ b/index.js @@ -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') } @@ -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' } @@ -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}) { @@ -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(' || ')})` + } else { + throw new Error(`${type} unsupported`) + } + } + return condition +} + function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKey) { var code = '' var funcName @@ -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 { diff --git a/test/typesArray.test.js b/test/typesArray.test.js new file mode 100644 index 00000000..d1baa272 --- /dev/null +++ b/test/typesArray.test.js @@ -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() + } +})