forked from serby/schemata
-
Notifications
You must be signed in to change notification settings - Fork 0
/
schemata.js
258 lines (226 loc) · 8.25 KB
/
schemata.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
const isPrimitive = require('is-primitive')
const clone = require('lodash.clonedeep')
const SchemataArray = require('./lib/array')
const castProperty = require('./lib/property-caster')
const hasTag = require('./lib/has-tag')
const isSchemata = require('./lib/is-schemata')
const isSchemataArray = require('./lib/is-array')
const getType = require('./lib/type-getter')
const { validate, validateRecursive } = require('./lib/validate')
const convertCamelcaseToHuman = require('./lib/camelcase-to-human-converter')
const createSchemata = ({ name, description, properties } = {}) => {
if (name === undefined) throw new Error('name is required')
const internalSchema = clone(properties || {})
Object.keys(internalSchema).forEach((k) => {
if (!properties[k].defaultValue) return
if (typeof properties[k].defaultValue === 'function') return
if (isPrimitive(properties[k].defaultValue)) return
throw new Error(
`The defaultValue for the schema property "${k}" must be either a primitive value or a function`
)
})
return {
getName() {
return name
},
getDescription() {
return description
},
getProperties() {
return clone(internalSchema)
},
/*
* Returns an object with properties defined in schema but with empty values.
*
* The empty value will depend on the type:
* - Array = []
* - Object = {}
* - String = null
* - Boolean = null
* - Number = null
*/
makeBlank() {
const newEntity = {}
Object.keys(internalSchema).forEach((key) => {
const type = getType(internalSchema[key].type)
if (typeof type === 'object') {
// If the type is a schemata instance use its makeBlank() function
if (isSchemata(type)) {
newEntity[key] = type.makeBlank()
return null
}
// If the type is a schemata array, create an empty array
if (isSchemataArray(type)) {
newEntity[key] = []
return null
}
throw new Error(`Invalid property type on '${key}'`)
}
switch (type) {
case Object:
newEntity[key] = {}
return null
case Array:
newEntity[key] = []
return null
default:
newEntity[key] = null
}
})
return newEntity
},
/*
* Returns a new object with properties and default values from the schema definition.
* If existingEntity is passed then extends it with the default properties.
*/
makeDefault(existingEntity) {
const newEntity = this.makeBlank()
if (!existingEntity) existingEntity = {}
Object.keys(internalSchema).forEach((key) => {
const property = internalSchema[key]
const existingValue = existingEntity[key]
const type = getType(property.type, existingEntity)
// If an existingEntity is passed then use its value
// If it doesn't have that property then the default will be used.
// If an existingEntity is a schemata instance it's own makeDefault() will
// also be called so that partial sub-objects can be used.
if (existingEntity !== undefined && existingEntity[key] !== undefined) {
newEntity[key] = isSchemata(type)
? type.makeDefault(existingValue)
: existingEntity[key]
return
}
switch (typeof property.defaultValue) {
case 'undefined':
// If the property is a schemata instance use its makeDefault() function
if (isSchemata(type)) {
newEntity[key] = type.makeDefault(existingValue)
return
}
// In the absence of a defaultValue property the makeBlank() value is used
return
case 'function':
// In the case of a defaultValue() function, run it to create the default
// value. This is important when using mutable values like Object and Array
// which would be a reference to the schema's property if it were set as
// property.defaultValue = Object|Array|Date
newEntity[key] = property.defaultValue()
return
default:
// If defaultValue is a primitive value use it as-is
newEntity[key] = property.defaultValue
}
})
return newEntity
},
/*
* Takes an object and strips out properties not in the schema. If a tag is given
* then only properties with that tag will remain.
*/
stripUnknownProperties(entityObject, tag, ignoreTagForSubSchemas) {
const newEntity = {}
Object.keys(entityObject).forEach((key) => {
const property = internalSchema[key]
let subSchemaTag
// If the schema doesn't have this property, or if the property is in
// the schema but doesn't have the given tag, don't keep it
if (
typeof property === 'undefined' ||
!hasTag(internalSchema, key, tag)
)
return
const type = getType(property.type, entityObject)
// If the property is null, leave it alone
if (entityObject[key] === null) {
newEntity[key] = null
return
}
// If the type is a schemata instance use its stripUnknownProperties() function
if (isSchemata(type)) {
subSchemaTag = ignoreTagForSubSchemas ? undefined : tag
newEntity[key] = type.stripUnknownProperties(
entityObject[key],
subSchemaTag,
ignoreTagForSubSchemas
)
return
}
// If this property is of a primitive type, keep it as is
if (typeof property.type !== 'object') {
newEntity[key] = entityObject[key]
return
}
// If the type is a schemata array, call stripUnknownProperties() on each item in the array
if (isSchemataArray(property.type)) {
// The array can't be processed if it's not an array
if (!Array.isArray(entityObject[key])) return
// Create a new array to copy items over to
newEntity[key] = []
subSchemaTag = ignoreTagForSubSchemas ? undefined : tag
entityObject[key].forEach((item, index) => {
newEntity[key][index] =
property.type.arraySchema.stripUnknownProperties(
item,
subSchemaTag,
ignoreTagForSubSchemas
)
})
}
})
return newEntity
},
/*
* Casts all the properties in the given entityObject that are defined in the schema.
* If tag is provided then only properties that are in the schema and have the given tag will be cast.
*/
cast(entityObject, tag) {
const newEntity = {}
Object.keys(entityObject).forEach((key) => {
// Copy all properties
newEntity[key] = entityObject[key]
// Only cast properties in the schema and tagged, if tag is provided
if (
internalSchema[key] !== undefined &&
internalSchema[key].type &&
hasTag(internalSchema, key, tag)
) {
newEntity[key] = castProperty(
internalSchema[key].type,
entityObject[key],
key,
entityObject
)
}
})
return newEntity
},
validate: validate(internalSchema),
validateRecursive: validateRecursive(internalSchema),
/*
* Returns the human readable name for a particular property.
*/
propertyName(property) {
if (internalSchema[property] === undefined)
throw new RangeError(`No property '${property}' in schema`)
return internalSchema[property].name === undefined
? convertCamelcaseToHuman(property)
: internalSchema[property].name
},
/*
* Extend a schema with another one. Returns a new schemata instance with combined properties.
*/
extend(schema) {
return createSchemata({
name: schema.getName() || this.getName(),
description: schema.getDescription() || this.getDescription(),
properties: {
...this.getProperties(),
...schema.getProperties()
}
})
}
}
}
createSchemata.Array = SchemataArray
createSchemata.castProperty = castProperty
module.exports = createSchemata