1 |
|
2 | import cloneDeep from 'lodash/cloneDeep'
|
3 | import typeOf from 'component-type'
|
4 |
|
5 | import DEFAULT_TYPES from './types'
|
6 |
|
7 | import {
|
8 | ElementInvalidError,
|
9 | PropertyInvalidError,
|
10 | PropertyRequiredError,
|
11 | PropertyUnknownError,
|
12 | ValueInvalidError,
|
13 | ValueRequiredError,
|
14 | } from './errors'
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | function superstruct(options = {}) {
|
24 | const TYPES = {
|
25 | ...DEFAULT_TYPES,
|
26 | ...(options.types || {}),
|
27 | }
|
28 |
|
29 | |
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
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 |
|
67 |
|
68 |
|
69 |
|
70 |
|
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 |
|
111 |
|
112 |
|
113 |
|
114 |
|
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 |
|
178 |
|
179 |
|
180 |
|
181 |
|
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 |
|
212 |
|
213 |
|
214 | return struct
|
215 | }
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 | const struct = superstruct()
|
224 |
|
225 | export default struct
|
226 | export { struct, superstruct }
|