diff --git a/src/error.js b/src/error.js index c7cdc044..30b7147f 100644 --- a/src/error.js +++ b/src/error.js @@ -8,14 +8,15 @@ class StructError extends TypeError { constructor(attrs) { - const { data, value, type, path = [] } = attrs - const message = `Expected a value of type "${type}" ${path.length ? `for \`${path.join('.')}\`` : ''} but received \`${value}\`.` + const { data, value, type, path, errors = [] } = attrs + const message = `Expected a value of type \`${type}\`${path.length ? ` for \`${path.join('.')}\`` : ''} but received \`${value}\`.` super(message) this.data = data this.path = path this.value = value this.type = type - this.errors = [this] + this.errors = errors + if (!errors.length) errors.push(this) Error.captureStackTrace(this, this.constructor) } diff --git a/src/is-struct.js b/src/is-struct.js new file mode 100644 index 00000000..5b2cc751 --- /dev/null +++ b/src/is-struct.js @@ -0,0 +1,28 @@ + +/** + * A private string to identify structs by. + * + * @type {String} + */ + +const IS_STRUCT = '@@__STRUCT__@@' + +/** + * Check if a `value` is a struct. + * + * @param {Any} value + * @return {Boolean} + */ + +function isStruct(value) { + return !!(value && value[IS_STRUCT]) +} + +/** + * Export. + * + * @type {Function} + */ + +export default isStruct +export { IS_STRUCT } diff --git a/src/kinds.js b/src/kinds.js new file mode 100644 index 00000000..f3101e08 --- /dev/null +++ b/src/kinds.js @@ -0,0 +1,449 @@ + +import kindOf from 'kind-of' +import invariant from 'invariant' + +import isStruct from './is-struct' + +/** + * Kind. + */ + +class Kind { + + constructor(name, type, validate) { + this.name = name + this.type = type + this.validate = validate + } + +} + +/** + * Any. + */ + +function any(schema, defaults, options) { + if (isStruct(schema)) return schema.__kind + if (schema instanceof Kind) return schema + + switch (kindOf(schema)) { + case 'array': { + return schema.length > 1 + ? tuple(schema, defaults, options) + : list(schema, defaults, options) + } + + case 'function': { + return func(schema, defaults, options) + } + + case 'object': { + return object(schema, defaults, options) + } + + case 'string': { + let required = true + let type + + if (schema.endsWith('?')) { + required = false + schema = schema.slice(0, -1) + } + + if (schema.includes('|')) { + const scalars = schema.split(/\s*\|\s*/g) + type = union(scalars, defaults, options) + } else if (schema.includes('&')) { + const scalars = schema.split(/\s*&\s*/g) + type = intersection(scalars, defaults, options) + } else { + type = scalar(schema, defaults, options) + } + + if (!required) { + type = optional(type, undefined, options) + } + + return type + } + } + + invariant(false, `A schema definition must be an object, array, string or function, but you passed: ${schema}`) +} + +/** + * Dict. + */ + +function dict(schema, defaults, options) { + const obj = scalar('object', undefined, options) + const keys = any(schema[0], undefined, options) + const values = any(schema[1], undefined, options) + const name = 'dict' + const type = `dict<${keys.type},${values.type}>` + const validate = (value = defaults) => { + const [ error ] = obj.validate(value) + + if (error) { + error.type = type + return [error] + } + + const ret = {} + const errors = [] + + for (let k in value) { + const v = value[k] + const [ e, r ] = keys.validate(k) + + if (e) { + e.path = [k].concat(e.path) + e.data = value + errors.push(e) + continue + } + + k = r + const [ e2, r2 ] = values.validate(v) + + if (e2) { + e2.path = [k].concat(e2.path) + e2.data = value + errors.push(e2) + continue + } + + ret[k] = r2 + } + + if (errors.length) { + const first = errors[0] + first.errors = errors + return [first] + } + + return [undefined, ret] + } + + return new Kind(name, type, validate) +} + +/** + * Enums. + */ + +function enums(schema, defaults, options) { + const name = 'enum' + const type = schema.map((s) => { + try { + return JSON.stringify(s) + } catch (e) { + return String(s) + } + }).join(' | ') + + const validate = (value = defaults) => { + return schema.includes(value) + ? [undefined, value] + : [{ data: value, path: [], value, type }] + } + + return new Kind(name, type, validate) +} + +/** + * Function. + */ + +function func(schema, defaults, options) { + const name = 'function' + const type = '' + const validate = (value = defaults) => { + return schema(value) + ? [undefined, value] + : [{ type, value, data: value, path: [] }] + } + + return new Kind(name, type, validate) +} + +/** + * List. + */ + +function list(schema, defaults, options) { + invariant(schema.length === 1, `List structs must be defined as an array with a single element, but you passed ${schema.length} elements.`) + + const array = scalar('array', undefined, options) + const element = any(schema[0], undefined, options) + const name = 'list' + const type = `[${element.type}]` + const validate = (value = defaults) => { + const [ error, result ] = array.validate(value) + + if (error) { + error.type = type + return [error] + } + + value = result + const errors = [] + const ret = [] + + for (let i = 0; i < value.length; i++) { + const v = value[i] + const [ e, r ] = element.validate(v) + + if (e) { + e.path = [i].concat(e.path) + e.data = value + errors.push(e) + continue + } + + ret[i] = r + } + + if (errors.length) { + const first = errors[0] + first.errors = errors + return [first] + } + + return [undefined, ret] + } + + return new Kind(name, type, validate) +} + +/** + * Object. + */ + +function object(schema, defaults, options) { + invariant(kindOf(schema) === 'object', `Object structs must be defined as an object, but you passed: ${schema}`) + + const obj = scalar('object', undefined, options) + const ks = [] + const properties = {} + + for (const key in schema) { + ks.push(key) + const s = schema[key] + const d = defaults && defaults[key] + const kind = any(s, d, options) + properties[key] = kind + } + + const name = 'object' + const type = `{${ks.join()}}` + const validate = (value = defaults) => { + const [ error, result ] = obj.validate(value) + + if (error) { + error.type = type + return [error] + } + + value = result + const errors = [] + const ret = {} + const valueKeys = Object.keys(value) + const schemaKeys = Object.keys(properties) + const keys = new Set(valueKeys.concat(schemaKeys)) + + keys.forEach((key) => { + const v = value[key] + const kind = properties[key] + + if (!kind) { + const e = { data: value, path: [key], value: v } + errors.push(e) + return + } + + const [ e, r ] = kind.validate(v) + + if (e) { + e.path = [key].concat(e.path) + e.data = value + errors.push(e) + return + } + + if (key in value) { + ret[key] = r + } + }) + + if (errors.length) { + const first = errors[0] + first.errors = errors + return [first] + } + + return [undefined, ret] + } + + return new Kind(name, type, validate) +} + +/** + * Optional. + */ + +function optional(schema, defaults, options) { + return union([schema, 'undefined'], defaults, options) +} + +/** + * Scalar. + */ + +function scalar(schema, defaults, options) { + const { types } = options + const fn = types[schema] + invariant(typeof fn === 'function', `No struct validator function found for type "${schema}".`) + const kind = func(fn, defaults, options) + const name = 'scalar' + const type = schema + const validate = (value) => { + const [ error, result ] = kind.validate(value) + + if (error) { + error.type = type + return [error] + } + + return [undefined, result] + } + + return new Kind(name, type, validate) +} + +/** + * Tuple. + */ + +function tuple(schema, defaults, options) { + const kinds = schema.map(s => any(s, undefined, options)) + const array = scalar('array', undefined, options) + const name = 'tuple' + const type = `[${kinds.map(k => k.type).join()}]` + const validate = (value = defaults) => { + const [ error ] = array.validate(value) + + if (error) { + error.type = type + return [error] + } + + const ret = [] + const errors = [] + const length = Math.max(value.length, kinds.length) + + for (let i = 0; i < length; i++) { + const kind = kinds[i] + const v = value[i] + + if (!kind) { + const e = { data: value, path: [i], value: v } + errors.push(e) + continue + } + + const [ e, r ] = kind.validate(v) + + if (e) { + e.path = [i].concat(e.path) + e.data = value + errors.push(e) + continue + } + + ret[i] = r + } + + if (errors.length) { + const first = errors[0] + first.errors = errors + return [first] + } + + return [undefined, ret] + } + + return new Kind(name, type, validate) +} + +/** + * Union. + */ + +function union(schema, defaults, options) { + const kinds = schema.map(s => any(s, undefined, options)) + const name = 'union' + const type = kinds.map(k => k.type).join(' | ') + const validate = (value = defaults) => { + let error + + for (const k of kinds) { + const [ e, r ] = k.validate(value) + if (!e) return [undefined, r] + error = e + } + + error.type = type + return [error] + } + + return new Kind(name, type, validate) +} + +/** + * Intersection. + */ + +function intersection(schema, defaults, options) { + const types = schema.map(s => any(s, undefined, options)) + const name = 'intersection' + const type = types.map(t => t.type).join(' & ') + const validate = (value = defaults) => { + let v = value + + for (const t of types) { + const [ e, r ] = t.validate(v) + + if (e) { + e.type = type + return [e] + } + + v = r + } + + return [undefined, v] + } + + return new Kind(name, type, validate) +} + +/** + * Export. + * + * @type {Function} + */ + +export default { + any, + dict, + enum: enums, + function: func, + list, + object, + optional, + scalar, + tuple, + union, + intersection, +} diff --git a/src/schemas.js b/src/schemas.js deleted file mode 100644 index 1dd7d3f4..00000000 --- a/src/schemas.js +++ /dev/null @@ -1,322 +0,0 @@ - -import cloneDeep from 'clone-deep' -import kindOf from 'kind-of' -import invariant from 'invariant' - -import StructError from './error' - -/** - * Schema. - * - * @type {Schema} - */ - -class Schema { - - /** - * Create a schema with `schema`, `defaults` and `options`. - * - * @param {Any} schema - * @param {Any} defaults - * @param {Object} options - */ - - constructor(schema, defaults, options = {}) { - this.schema = schema - this.defaults = defaults - this.options = options - } - - /** - * Assert that a `value` is valid, throwing if not. - * - * @param {Any} value - * @return {Any|StructError} - */ - - assert(value) { - const [ error, result ] = this.validate(value) - if (error) throw error - return result - } - - /** - * Get the defaulted value, given an initial `value`. - * - * @param {Any} value - * @return {Any} - */ - - default(value) { - if (value !== undefined) return value - const { defaults } = this - return typeof defaults === 'function' ? defaults() : cloneDeep(defaults) - } - - /** - * Test that a `value` is valid. - * - * @param {Any} value - * @return {Boolean} - */ - - test(value) { - const [ error ] = this.validate(value) - return !error - } - - /** - * Validate a `value`, returning an error or the value with defaults. - * - * @param {Any} value - * @return {Array} - */ - - validate(value) { - value = this.default(value) - return this.check(value) - } - -} - -/** - * Function schema. - * - * @type {FunctionSchema} - */ - -class FunctionSchema extends Schema { - - constructor(schema, defaults, options = {}) { - super(schema, defaults, options) - - const type = '' - - this.kind = 'Function' - this.type = type - this.check = (value) => { - if ( - (options.required && value === undefined) || - (value !== undefined && !schema(value)) - ) { - const error = new StructError({ type, value, data: value }) - return [error] - } - - return [undefined, value] - } - } - -} - -/** - * Scalar schema. - * - * @type {ScalarSchema} - */ - -class ScalarSchema extends Schema { - - constructor(schema, defaults, options = {}) { - super(schema, defaults, options) - - const { types } = options - const required = !schema.endsWith('?') - const type = required ? schema : schema.slice(0, -1) - const ts = type.split(/\s*\|\s*/g) - const validators = ts.map((t) => { - const fn = types[t] - invariant(typeof fn === 'function', `No struct validator function found for type "${t}".`) - return fn - }) - - this.kind = 'Scalar' - this.type = type - this.options.required = required - this.check = (value) => { - if ( - (options.required && value === undefined) || - (value !== undefined && !validators.some(fn => fn(value))) - ) { - const error = new StructError({ type, value, data: value }) - return [error] - } - - return [undefined, value] - } - } - -} - -/** - * List schema. - * - * @type {ListSchema} - */ - -class ListSchema extends Schema { - - constructor(schema, defaults, options = {}) { - super(schema, defaults, options) - - invariant(schema.length === 1, `List structs must be defined as an array with a single element, but you passed ${schema.length} elements.`) - - const { struct } = options - const type = options.required ? 'array' : 'array?' - const valueStruct = struct(type) - const elementStruct = struct(schema[0]) - - this.kind = 'List' - this.type = type - this.check = (value) => { - const [ error ] = valueStruct.validate(value) - if (error) return [error] - - const errors = [] - const values = [] - const isUndefined = !options.required && value === undefined - value = isUndefined ? [] : value - - value.forEach((element, index) => { - const [ e, r ] = elementStruct.validate(element) - - if (e) { - e.path = [index].concat(e.path) - e.data = value - errors.push(e) - return - } - - values.push(r) - }) - - if (errors.length) { - const first = errors[0] - first.errors = errors - return [first] - } - - const ret = isUndefined && !values.length ? undefined : values - return [undefined, ret] - } - } - -} - -/** - * Object schema. - * - * @type {ObjectSchema} - */ - -class ObjectSchema extends Schema { - - /** - * Create an object schema with `schema`, `defaults` and `options`. - * - * @param {Any} schema - * @param {Any} defaults - * @param {Object} options - */ - - constructor(schema, defaults, options = {}) { - super(schema, defaults, options) - - invariant(kindOf(schema) === 'object', `Object structs must be defined as an object, but you passed: ${schema}`) - - const { struct } = options - const propertyStructs = {} - - for (const key in schema) { - const s = struct(schema[key]) - propertyStructs[key] = s - - if (s.options.required) { - this.options.required = true - } - } - - const type = this.options.required ? 'object' : 'object?' - const valueStruct = struct(type) - - this.kind = 'Object' - this.type = type - this.check = (value) => { - const [ error ] = valueStruct.validate(value) - if (error) return [error] - - const errors = [] - const values = {} - const isUndefined = !options.required && value === undefined - - value = isUndefined ? {} : value - - const valueKeys = Object.keys(value) - const schemaKeys = Object.keys(propertyStructs) - const keys = new Set(valueKeys.concat(schemaKeys)) - const hasKeys = !!valueKeys.length - - keys.forEach((k) => { - const v = value[k] - const s = propertyStructs[k] - - if (!s) { - const e = new StructError({ data: value, path: [k], value: v }) - errors.push(e) - return - } - - const [ e, r ] = s.validate(v) - - if (e) { - e.path = [k].concat(e.path) - e.data = value - errors.push(e) - return - } - - if (k in value) { - values[k] = r - } - }) - - if (errors.length) { - const first = errors[0] - first.errors = errors - return [first] - } - - const ret = isUndefined && !hasKeys ? undefined : values - return [undefined, ret] - } - } - - /** - * Get the defaulted value, given an initial `value`. - * - * @param {Object|Void} value - * @return {Object|Void} - */ - - default(value) { - const { defaults } = this - if (value !== undefined || defaults === undefined) return value - const defs = typeof defaults === 'function' ? defaults() : cloneDeep(defaults) - const ret = Object.assign({}, value, defs) - return ret - } - -} - -/** - * Export. - * - * @type {Function} - */ - -export default { - Function: FunctionSchema, - List: ListSchema, - Object: ObjectSchema, - Scalar: ScalarSchema, -} diff --git a/src/superstruct.js b/src/superstruct.js index 61b8d9dd..770b21de 100644 --- a/src/superstruct.js +++ b/src/superstruct.js @@ -1,44 +1,10 @@ -import kindOf from 'kind-of' import invariant from 'invariant' import TYPES from './types' -import Schemas from './schemas' - -/** - * A private string to identify structs by. - * - * @type {String} - */ - -const IS_STRUCT = '@@__STRUCT__@@' - -/** - * Public properties for struct functions. - * - * @type {Array} - */ - -const STRUCT_PROPERTIES = [ - 'kind', - 'type', - 'schema', - 'defaults', - 'options', -] - -/** - * Public methods for struct functions. - * - * @type {Array} - */ - -const STRUCT_METHODS = [ - 'assert', - 'default', - 'test', - 'validate', -] +import KINDS from './kinds' +import StructError from './error' +import isStruct, { IS_STRUCT } from './is-struct' /** * Create a struct factory with a `config`. @@ -54,68 +20,64 @@ function superstruct(config = {}) { } /** - * Create a `kind` struct with schema `definition`, `defaults` and `options`. + * Create a `kind` struct with `schema`, `defaults` and `options`. * - * @param {String} kind - * @param {Function|String|Array|Object} definition + * @param {Any} schema * @param {Any} defaults * @param {Object} options * @return {Function} */ - function createStruct(kind, definition, defaults, options = {}) { - if (isStruct(definition)) { - definition = definition.schema - } + function struct(schema, defaults, options = {}) { + if (isStruct(schema)) schema = schema.schema - const Schema = Schemas[kind] - const schema = new Schema(definition, defaults, { ...options, types, struct }) + const kind = KINDS.any(schema, defaults, { ...options, types }) - // Define the struct creator function. function Struct(data) { invariant(!(this instanceof Struct), 'The `Struct` creation function should not be used with the `new` keyword.') - return schema.assert(data) + return Struct.assert(data) } Struct[IS_STRUCT] = true - Struct.kind = kind + Struct.__kind = kind + Struct.kind = kind.name + Struct.type = kind.type + Struct.schema = schema + Struct.defaults = defaults + Struct.options = options + + Struct.assert = (value) => { + const [ error, result ] = kind.validate(value) + if (error) throw new StructError(error) + return result + } - STRUCT_PROPERTIES.forEach((prop) => { - Struct[prop] = schema[prop] - }) + Struct.test = (value) => { + const [ error ] = kind.validate(value) + return !error + } - STRUCT_METHODS.forEach((method) => { - Struct[method] = (...a) => schema[method](...a) - }) + Struct.validate = (value) => { + const [ error, result ] = kind.validate(value) + if (error) return [new StructError(error)] + return [undefined, result] + } return Struct } /** - * Define a struct with schema `definition`, `defaults` and `options`. - * - * @param {Function|String|Array|Object} definition - * @param {Any} defaults - * @param {Object} options - * @return {Function} + * Mix in a factory for each specific kind of struct. */ - function struct(definition, defaults, options) { - if (isStruct(definition)) return definition - const kind = getKind(definition) - const Struct = createStruct(kind, definition, defaults, options) - return Struct - } - - // Mix in a factory for each kind of struct. - Object.keys(Schemas).forEach((kind) => { - const lower = kind.toLowerCase() - struct[lower] = (...args) => createStruct(kind, ...args) - }) + Object.keys(KINDS).forEach((name) => { + const kind = KINDS[name] - // Mix in the `required` convenience flag. - Object.defineProperty(struct, 'required', { - get: () => (s, d, o = {}) => struct(s, d, { ...o, required: true }) + struct[name] = (schema, defaults, options) => { + const type = kind(schema, defaults, { ...options, types }) + const s = struct(type, defaults, options) + return s + } }) /** @@ -125,36 +87,6 @@ function superstruct(config = {}) { return struct } -/** - * Check if a `value` is a struct. - * - * @param {Any} value - * @return {Boolean} - */ - -function isStruct(value) { - return !!(value && value[IS_STRUCT]) -} - -/** - * Get the kind of a struct from its schema `definition`. - * - * @param {Any} definition - * @return {String} - */ - -function getKind(definition) { - switch (kindOf(definition)) { - case 'function': return 'Function' - case 'string': return 'Scalar' - case 'array': return 'List' - case 'object': return 'Object' - default: { - invariant(false, `A struct schema definition must be a string, array or object, but you passed: ${definition}`) - } - } -} - /** * Export. * diff --git a/test/fixtures/custom/value-invalid.js b/test/fixtures/custom/invalid.js similarity index 100% rename from test/fixtures/custom/value-invalid.js rename to test/fixtures/custom/invalid.js diff --git a/test/fixtures/custom/value-required.js b/test/fixtures/custom/required.js similarity index 100% rename from test/fixtures/custom/value-required.js rename to test/fixtures/custom/required.js diff --git a/test/fixtures/dict-scalars/defaults.js b/test/fixtures/dict-scalars/defaults.js new file mode 100644 index 00000000..9c46bd65 --- /dev/null +++ b/test/fixtures/dict-scalars/defaults.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct.dict(['string', 'number'], { a: 1, b: 2 }) + +export const data = undefined + +export const output = { a: 1, b: 2 } diff --git a/test/fixtures/dict-scalars/invalid.js b/test/fixtures/dict-scalars/invalid.js new file mode 100644 index 00000000..41d2a953 --- /dev/null +++ b/test/fixtures/dict-scalars/invalid.js @@ -0,0 +1,12 @@ + +import { struct } from '../../..' + +export const Struct = struct.dict(['string', 'number']) + +export const data = 'invalid' + +export const error = { + path: [], + value: 'invalid', + type: 'dict', +} diff --git a/test/fixtures/dict-scalars/optional.js b/test/fixtures/dict-scalars/optional.js new file mode 100644 index 00000000..d5614d37 --- /dev/null +++ b/test/fixtures/dict-scalars/optional.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct.optional(struct.dict(['string', 'number'])) + +export const data = undefined + +export const output = undefined diff --git a/test/fixtures/dict-scalars/property-invalid.js b/test/fixtures/dict-scalars/property-invalid.js new file mode 100644 index 00000000..1f69ed9d --- /dev/null +++ b/test/fixtures/dict-scalars/property-invalid.js @@ -0,0 +1,12 @@ + +import { struct } from '../../..' + +export const Struct = struct.dict(['string', 'number']) + +export const data = { a: 'invalid' } + +export const error = { + path: ['a'], + value: 'invalid', + type: 'number', +} diff --git a/test/fixtures/dict-scalars/property-optional.js b/test/fixtures/dict-scalars/property-optional.js new file mode 100644 index 00000000..75a05a9e --- /dev/null +++ b/test/fixtures/dict-scalars/property-optional.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct.dict(['string', 'number?']) + +export const data = { a: undefined } + +export const output = { a: undefined } diff --git a/test/fixtures/dict-scalars/valid.js b/test/fixtures/dict-scalars/valid.js new file mode 100644 index 00000000..116571e4 --- /dev/null +++ b/test/fixtures/dict-scalars/valid.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct.dict(['string', 'number']) + +export const data = { a: 1, b: 2 } + +export const output = { a: 1, b: 2 } diff --git a/test/fixtures/enum/defaults.js b/test/fixtures/enum/defaults.js new file mode 100644 index 00000000..6d323e55 --- /dev/null +++ b/test/fixtures/enum/defaults.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct.union(['string', 'number'], 42) + +export const data = undefined + +export const output = 42 diff --git a/test/fixtures/enum/invalid.js b/test/fixtures/enum/invalid.js new file mode 100644 index 00000000..52bd2851 --- /dev/null +++ b/test/fixtures/enum/invalid.js @@ -0,0 +1,12 @@ + +import { struct } from '../../..' + +export const Struct = struct.union(['string', 'number']) + +export const data = false + +export const error = { + path: [], + value: false, + type: 'string | number', +} diff --git a/test/fixtures/enum/optional.js b/test/fixtures/enum/optional.js new file mode 100644 index 00000000..bb9cf229 --- /dev/null +++ b/test/fixtures/enum/optional.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct.optional(struct.union(['string', 'number'])) + +export const data = undefined + +export const output = undefined diff --git a/test/fixtures/enum/valid.js b/test/fixtures/enum/valid.js new file mode 100644 index 00000000..59a0fc8a --- /dev/null +++ b/test/fixtures/enum/valid.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct.union(['string', 'number']) + +export const data = 42 + +export const output = 42 diff --git a/test/fixtures/list-objects/element-invalid.js b/test/fixtures/list-objects/element-invalid.js index 05263478..f85e85ba 100644 --- a/test/fixtures/list-objects/element-invalid.js +++ b/test/fixtures/list-objects/element-invalid.js @@ -12,5 +12,5 @@ export const data = [ export const error = { path: [1], value: 'invalid', - type: 'object', + type: '{id}', } diff --git a/test/fixtures/list-objects/value-invalid.js b/test/fixtures/list-objects/invalid.js similarity index 90% rename from test/fixtures/list-objects/value-invalid.js rename to test/fixtures/list-objects/invalid.js index 790e58c3..85b070c3 100644 --- a/test/fixtures/list-objects/value-invalid.js +++ b/test/fixtures/list-objects/invalid.js @@ -8,5 +8,5 @@ export const data = 'invalid' export const error = { path: [], value: 'invalid', - type: 'array', + type: '[{id}]', } diff --git a/test/fixtures/list-scalars/enum.js b/test/fixtures/list-scalars/enum.js deleted file mode 100644 index f171fa8e..00000000 --- a/test/fixtures/list-scalars/enum.js +++ /dev/null @@ -1,8 +0,0 @@ - -import { struct } from '../../..' - -export const Struct = struct(['number|string']) - -export const data = [1, '2', 3] - -export const output = [1, '2', 3] diff --git a/test/fixtures/list-scalars/value-invalid.js b/test/fixtures/list-scalars/invalid.js similarity index 89% rename from test/fixtures/list-scalars/value-invalid.js rename to test/fixtures/list-scalars/invalid.js index 3ba48768..820109fa 100644 --- a/test/fixtures/list-scalars/value-invalid.js +++ b/test/fixtures/list-scalars/invalid.js @@ -8,5 +8,5 @@ export const data = 'invalid' export const error = { path: [], value: 'invalid', - type: 'array', + type: '[number]', } diff --git a/test/fixtures/object-lists/property-invalid.js b/test/fixtures/object-lists/property-invalid.js index 8f8d8c97..8d7c7f02 100644 --- a/test/fixtures/object-lists/property-invalid.js +++ b/test/fixtures/object-lists/property-invalid.js @@ -14,5 +14,5 @@ export const data = { export const error = { path: ['tags'], value: 'invalid', - type: 'array', + type: '[string]', } diff --git a/test/fixtures/object-lists/property-optional.js b/test/fixtures/object-lists/property-optional.js new file mode 100644 index 00000000..f68be451 --- /dev/null +++ b/test/fixtures/object-lists/property-optional.js @@ -0,0 +1,15 @@ + +import { struct } from '../../..' + +export const Struct = struct({ + title: 'string', + tags: struct.optional(['string']), +}) + +export const data = { + title: 'hello world', +} + +export const output = { + title: 'hello world', +} diff --git a/test/fixtures/object-lists/property-required.js b/test/fixtures/object-lists/property-required.js index db71fe1f..2860239d 100644 --- a/test/fixtures/object-lists/property-required.js +++ b/test/fixtures/object-lists/property-required.js @@ -3,7 +3,7 @@ import { struct } from '../../..' export const Struct = struct({ title: 'string', - tags: struct.required(['string']), + tags: ['string'], }) export const data = { @@ -13,5 +13,5 @@ export const data = { export const error = { path: ['tags'], value: undefined, - type: 'array', + type: '[string]', } diff --git a/test/fixtures/object-objects/property-invalid.js b/test/fixtures/object-objects/property-invalid.js index a55fc960..ffe89b43 100644 --- a/test/fixtures/object-objects/property-invalid.js +++ b/test/fixtures/object-objects/property-invalid.js @@ -19,5 +19,5 @@ export const data = { export const error = { path: ['address'], value: 'invalid', - type: 'object', + type: '{street,city}', } diff --git a/test/fixtures/object-objects/property-optional.js b/test/fixtures/object-objects/property-optional.js index fa7b915c..2b367f16 100644 --- a/test/fixtures/object-objects/property-optional.js +++ b/test/fixtures/object-objects/property-optional.js @@ -4,10 +4,10 @@ import { struct } from '../../..' export const Struct = struct({ name: 'string', age: 'number', - address: { + address: struct.optional({ street: 'string?', city: 'string?', - } + }) }) export const data = { diff --git a/test/fixtures/object-objects/property-required.js b/test/fixtures/object-objects/property-required.js index b0d5876e..c2a63249 100644 --- a/test/fixtures/object-objects/property-required.js +++ b/test/fixtures/object-objects/property-required.js @@ -18,5 +18,5 @@ export const data = { export const error = { path: ['address'], value: undefined, - type: 'object', + type: '{street,city}', } diff --git a/test/fixtures/object-scalars/value-invalid.js b/test/fixtures/object-scalars/invalid.js similarity index 89% rename from test/fixtures/object-scalars/value-invalid.js rename to test/fixtures/object-scalars/invalid.js index 01c5a2f1..0e14d799 100644 --- a/test/fixtures/object-scalars/value-invalid.js +++ b/test/fixtures/object-scalars/invalid.js @@ -11,5 +11,5 @@ export const data = 'invalid' export const error = { path: [], value: 'invalid', - type: 'object', + type: '{name,age}', } diff --git a/test/fixtures/object-structs/property-invalid.js b/test/fixtures/object-structs/property-invalid.js index 11ad6341..5639be8f 100644 --- a/test/fixtures/object-structs/property-invalid.js +++ b/test/fixtures/object-structs/property-invalid.js @@ -2,7 +2,7 @@ import { struct } from '../../..' const address = struct({ - street: 'string?', + street: 'string', city: 'string', }) @@ -12,13 +12,13 @@ export const Struct = struct({ export const data = { address: { - street: false, - city: 'springfield', + street: '123 fake st', + city: false, } } export const error = { - path: ['address', 'street'], + path: ['address', 'city'], value: false, type: 'string', } diff --git a/test/fixtures/object-structs/property-optional-invalid.js b/test/fixtures/object-structs/property-optional-invalid.js new file mode 100644 index 00000000..2ac81d71 --- /dev/null +++ b/test/fixtures/object-structs/property-optional-invalid.js @@ -0,0 +1,24 @@ + +import { struct } from '../../..' + +const address = struct({ + street: 'string?', + city: 'string', +}) + +export const Struct = struct({ + address, +}) + +export const data = { + address: { + street: false, + city: 'springfield', + } +} + +export const error = { + path: ['address', 'street'], + value: false, + type: 'string | undefined', +} diff --git a/test/fixtures/scalar/intersection-invalid.js b/test/fixtures/scalar/intersection-invalid.js new file mode 100644 index 00000000..e6bf57ba --- /dev/null +++ b/test/fixtures/scalar/intersection-invalid.js @@ -0,0 +1,18 @@ + +import { superstruct } from '../../..' + +const struct = superstruct({ + types: { + empty: v => v.length === 0 + } +}) + +export const Struct = struct('string & empty') + +export const data = 'a' + +export const error = { + path: [], + value: 'a', + type: 'string & empty', +} diff --git a/test/fixtures/scalar/intersection-valid.js b/test/fixtures/scalar/intersection-valid.js new file mode 100644 index 00000000..c42b4bd5 --- /dev/null +++ b/test/fixtures/scalar/intersection-valid.js @@ -0,0 +1,14 @@ + +import { superstruct } from '../../..' + +const struct = superstruct({ + types: { + empty: v => v.length === 0 + } +}) + +export const Struct = struct('string & empty') + +export const data = '' + +export const output = '' diff --git a/test/fixtures/scalar/union-invalid.js b/test/fixtures/scalar/union-invalid.js new file mode 100644 index 00000000..28185fb2 --- /dev/null +++ b/test/fixtures/scalar/union-invalid.js @@ -0,0 +1,12 @@ + +import { struct } from '../../..' + +export const Struct = struct('string|number') + +export const data = false + +export const error = { + path: [], + value: false, + type: 'string | number', +} diff --git a/test/fixtures/scalar/union.js b/test/fixtures/scalar/union-valid.js similarity index 100% rename from test/fixtures/scalar/union.js rename to test/fixtures/scalar/union-valid.js diff --git a/test/fixtures/tuple-scalars/defaults.js b/test/fixtures/tuple-scalars/defaults.js new file mode 100644 index 00000000..6a33a823 --- /dev/null +++ b/test/fixtures/tuple-scalars/defaults.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct(['string', 'number'], ['A', 1]) + +export const data = undefined + +export const output = ['A', 1] diff --git a/test/fixtures/tuple-scalars/element-invalid.js b/test/fixtures/tuple-scalars/element-invalid.js new file mode 100644 index 00000000..b378d89a --- /dev/null +++ b/test/fixtures/tuple-scalars/element-invalid.js @@ -0,0 +1,12 @@ + +import { struct } from '../../..' + +export const Struct = struct(['string', 'number']) + +export const data = [false, 3] + +export const error = { + path: [0], + value: false, + type: 'string', +} diff --git a/test/fixtures/tuple-scalars/element-optional.js b/test/fixtures/tuple-scalars/element-optional.js new file mode 100644 index 00000000..c33189b4 --- /dev/null +++ b/test/fixtures/tuple-scalars/element-optional.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct(['string', 'number?']) + +export const data = ['A', undefined] + +export const output = ['A', undefined] diff --git a/test/fixtures/tuple-scalars/element-unknown.js b/test/fixtures/tuple-scalars/element-unknown.js new file mode 100644 index 00000000..5fdd7502 --- /dev/null +++ b/test/fixtures/tuple-scalars/element-unknown.js @@ -0,0 +1,12 @@ + +import { struct } from '../../..' + +export const Struct = struct(['string', 'number']) + +export const data = ['A', 3, 'unknown'] + +export const error = { + path: [2], + value: 'unknown', + type: undefined, +} diff --git a/test/fixtures/tuple-scalars/invalid.js b/test/fixtures/tuple-scalars/invalid.js new file mode 100644 index 00000000..617d2c38 --- /dev/null +++ b/test/fixtures/tuple-scalars/invalid.js @@ -0,0 +1,12 @@ + +import { struct } from '../../..' + +export const Struct = struct(['string', 'number']) + +export const data = 'invalid' + +export const error = { + path: [], + value: 'invalid', + type: '[string,number]', +} diff --git a/test/fixtures/tuple-scalars/optional.js b/test/fixtures/tuple-scalars/optional.js new file mode 100644 index 00000000..bd236ac8 --- /dev/null +++ b/test/fixtures/tuple-scalars/optional.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct.optional(['string', 'number']) + +export const data = undefined + +export const output = undefined diff --git a/test/fixtures/tuple-scalars/valid.js b/test/fixtures/tuple-scalars/valid.js new file mode 100644 index 00000000..7240c7c9 --- /dev/null +++ b/test/fixtures/tuple-scalars/valid.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct(['string', 'number']) + +export const data = ['A', 1] + +export const output = ['A', 1] diff --git a/test/fixtures/union-scalars/defaults.js b/test/fixtures/union-scalars/defaults.js new file mode 100644 index 00000000..485a7f04 --- /dev/null +++ b/test/fixtures/union-scalars/defaults.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct.enum(['one', 'two'], 'two') + +export const data = undefined + +export const output = 'two' diff --git a/test/fixtures/union-scalars/invalid.js b/test/fixtures/union-scalars/invalid.js new file mode 100644 index 00000000..2f31a64b --- /dev/null +++ b/test/fixtures/union-scalars/invalid.js @@ -0,0 +1,12 @@ + +import { struct } from '../../..' + +export const Struct = struct.enum(['one', 'two']) + +export const data = 'three' + +export const error = { + path: [], + value: 'three', + type: '"one" | "two"', +} diff --git a/test/fixtures/union-scalars/optional.js b/test/fixtures/union-scalars/optional.js new file mode 100644 index 00000000..1cc72a94 --- /dev/null +++ b/test/fixtures/union-scalars/optional.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct.optional(struct.enum(['one', 'two'])) + +export const data = undefined + +export const output = undefined diff --git a/test/fixtures/union-scalars/valid.js b/test/fixtures/union-scalars/valid.js new file mode 100644 index 00000000..fba54ad8 --- /dev/null +++ b/test/fixtures/union-scalars/valid.js @@ -0,0 +1,8 @@ + +import { struct } from '../../..' + +export const Struct = struct.enum(['one', 'two']) + +export const data = 'two' + +export const output = 'two'