UNPKG

5.02 kBJavaScriptView Raw
1
2import cloneDeep from 'lodash/cloneDeep'
3import typeOf from 'component-type'
4
5import DEFAULT_TYPES from './types'
6
7import {
8 ElementInvalidError,
9 PropertyInvalidError,
10 PropertyRequiredError,
11 PropertyUnknownError,
12 ValueInvalidError,
13 ValueRequiredError,
14} from './errors'
15
16/**
17 * Create a struct factory from a set of `options`.
18 *
19 * @param {Object} options
20 * @return {Function}
21 */
22
23function superstruct(options = {}) {
24 const TYPES = {
25 ...DEFAULT_TYPES,
26 ...(options.types || {}),
27 }
28
29 /**
30 * Define a scalar struct with a `schema` type string.
31 *
32 * @param {String} schema
33 * @param {Any} defaults
34 * @return {Function}
35 */
36
37 function scalarStruct(schema, defaults) {
38 const isOptional = schema.endsWith('?')
39 const type = isOptional ? schema.slice(0, -1) : schema
40 const types = type.split(/\s*\|\s*/g)
41
42 const fns = types.map((t) => {
43 const fn = TYPES[t]
44
45 if (typeof fn !== 'function') {
46 throw new Error(`No struct validator function found for type "${t}".`)
47 }
48
49 return fn
50 })
51
52 return (value) => {
53 if (!isOptional && value === undefined) {
54 throw new ValueRequiredError({ type })
55 }
56
57 if (value !== undefined && !fns.some(fn => fn(value))) {
58 throw new ValueInvalidError({ type, value })
59 }
60
61 return value
62 }
63 }
64
65 /**
66 * Define a list struct with a `schema` array.
67 *
68 * @param {Array} schema
69 * @param {Any} defaults
70 * @return {Function}
71 */
72
73 function listStruct(schema, defaults) {
74 if (schema.length !== 1) {
75 throw new Error(`List structs must be defined as an array with a single element, but you passed ${schema.length} elements.`)
76 }
77
78 schema = schema[0]
79 const fn = struct(schema)
80 const type = 'array'
81
82 return (value) => {
83 if (value === undefined) {
84 throw new ValueRequiredError({ type })
85 } else if (typeOf(value) !== 'array') {
86 throw new ValueInvalidError({ type, value })
87 }
88
89 const ret = value.map((v, index) => {
90 try {
91 return fn(v)
92 } catch (e) {
93 const path = [index].concat(e.path)
94
95 switch (e.code) {
96 case 'value_invalid':
97 throw new ElementInvalidError({ ...e, index, path })
98 default:
99 if ('path' in e) e.path = path
100 throw e
101 }
102 }
103 })
104
105 return ret
106 }
107 }
108
109 /**
110 * Define an object struct with a `schema` dictionary.
111 *
112 * @param {Object} schema
113 * @param {Any} defaults
114 * @return {Function}
115 */
116
117 function objectStruct(schema, defaults) {
118 const structs = {}
119 const type = 'object'
120
121 for (const key in schema) {
122 const fn = struct(schema[key])
123 structs[key] = fn
124 }
125
126 return (value) => {
127 let isUndefined = false
128
129 if (value === undefined) {
130 isUndefined = true
131 value = {}
132 } else if (typeOf(value) !== 'object') {
133 throw new ValueInvalidError({ type, value })
134 }
135
136 const ret = {}
137
138 for (const key in structs) {
139 const s = structs[key]
140 const v = value[key]
141 let r
142
143 try {
144 r = s(v)
145 } catch (e) {
146 const path = [key].concat(e.path)
147
148 switch (e.code) {
149 case 'value_invalid':
150 throw new PropertyInvalidError({ ...e, key, path })
151 case 'value_required':
152 throw isUndefined
153 ? new ValueRequiredError({ type })
154 : new PropertyRequiredError({ ...e, key, path })
155 default:
156 if ('path' in e) e.path = path
157 throw e
158 }
159 }
160
161 if (key in value) {
162 ret[key] = r
163 }
164 }
165
166 for (const key in value) {
167 if (!(key in structs)) {
168 throw new PropertyUnknownError({ key, path: [key] })
169 }
170 }
171
172 return isUndefined ? undefined : ret
173 }
174 }
175
176 /**
177 * Define a struct with `schema`.
178 *
179 * @param {Function|String|Array|Object} schema
180 * @param {Any} defaults
181 * @return {Function}
182 */
183
184 function struct(schema, defaults) {
185 let s
186
187 if (typeOf(schema) === 'function') {
188 s = schema
189 } else if (typeOf(schema) === 'string') {
190 s = scalarStruct(schema, defaults)
191 } else if (typeOf(schema) === 'array') {
192 s = listStruct(schema, defaults)
193 } else if (typeOf(schema) === 'object') {
194 s = objectStruct(schema, defaults)
195 } else {
196 throw new Error(`A struct schema definition must be a string, array or object, but you passed: ${schema}`)
197 }
198
199 return (value) => {
200 if (value === undefined) {
201 value = typeof defaults === 'function'
202 ? defaults()
203 : cloneDeep(defaults)
204 }
205
206 return s(value)
207 }
208 }
209
210 /**
211 * Return the struct factory.
212 */
213
214 return struct
215}
216
217/**
218 * Export the factory and the factory creator.
219 *
220 * @type {Function}
221 */
222
223const struct = superstruct()
224
225export default struct
226export { struct, superstruct }