manyfest 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.config/configstore/update-notifier-npm.json +1 -1
- package/README.md +35 -11
- package/package.json +3 -2
- package/source/Manyfest-HashTranslation.js +118 -0
- package/source/Manyfest-LogToConsole.js +1 -1
- package/source/Manyfest-ObjectAddressResolver.js +585 -0
- package/source/Manyfest-SchemaManipulation.js +112 -0
- package/source/Manyfest.js +195 -258
- package/test/Manyfest_Object_CheckExistence_tests.js +70 -0
- package/test/Manyfest_Object_HashTranslation_tests.js +166 -0
- package/test/Manyfest_Object_Populate_tests.js +140 -0
- package/test/{Manyfest_ObjectAccess_tests.js → Manyfest_Object_Read_tests.js} +74 -52
- package/test/Manyfest_Object_SchemaManipulation_tests.js +93 -0
- package/test/Manyfest_Object_Validate_tests.js +82 -0
- package/test/Manyfest_Object_Write_tests.js +161 -0
- package/test/Manyfest_tests.js +50 -1
- package/test/Manyfest_AdvancedObjectAccess_tests.js +0 -80
package/source/Manyfest.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* @author <steven@velozo.com>
|
|
4
4
|
*/
|
|
5
5
|
let libSimpleLog = require('./Manyfest-LogToConsole.js');
|
|
6
|
+
let libObjectAddressResolver = require('./Manyfest-ObjectAddressResolver.js');
|
|
7
|
+
let libHashTranslation = require('./Manyfest-HashTranslation.js');
|
|
8
|
+
let libSchemaManipulation = require('./Manyfest-SchemaManipulation.js');
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Manyfest object address-based descriptions and manipulations.
|
|
@@ -11,15 +14,31 @@ let libSimpleLog = require('./Manyfest-LogToConsole.js');
|
|
|
11
14
|
*/
|
|
12
15
|
class Manyfest
|
|
13
16
|
{
|
|
14
|
-
constructor(pManifest, pInfoLog, pErrorLog)
|
|
17
|
+
constructor(pManifest, pInfoLog, pErrorLog, pOptions)
|
|
15
18
|
{
|
|
16
19
|
// Wire in logging
|
|
17
20
|
this.logInfo = (typeof(pInfoLog) === 'function') ? pInfoLog : libSimpleLog;
|
|
18
21
|
this.logError = (typeof(pErrorLog) === 'function') ? pErrorLog : libSimpleLog;
|
|
19
22
|
|
|
23
|
+
// Create an object address resolver and map in the functions
|
|
24
|
+
this.objectAddressResolver = new libObjectAddressResolver(this.logInfo, this.logError);
|
|
25
|
+
|
|
20
26
|
this.options = (
|
|
21
27
|
{
|
|
22
|
-
strict: false
|
|
28
|
+
strict: false,
|
|
29
|
+
defaultValues:
|
|
30
|
+
{
|
|
31
|
+
"String": "",
|
|
32
|
+
"Number": 0,
|
|
33
|
+
"Float": 0.0,
|
|
34
|
+
"Integer": 0,
|
|
35
|
+
"Boolean": false,
|
|
36
|
+
"Binary": 0,
|
|
37
|
+
"DateTime": 0,
|
|
38
|
+
"Array": [],
|
|
39
|
+
"Object": {},
|
|
40
|
+
"Null": null
|
|
41
|
+
}
|
|
23
42
|
});
|
|
24
43
|
|
|
25
44
|
this.scope = undefined;
|
|
@@ -33,6 +52,10 @@ class Manyfest
|
|
|
33
52
|
{
|
|
34
53
|
this.loadManifest(pManifest);
|
|
35
54
|
}
|
|
55
|
+
|
|
56
|
+
this.schemaManipulations = new libSchemaManipulation(this.logInfo, this.logError);
|
|
57
|
+
|
|
58
|
+
this.hashTranslations = new libHashTranslation(this.logInfo, this.logError);
|
|
36
59
|
}
|
|
37
60
|
|
|
38
61
|
/*************************************************************************
|
|
@@ -72,12 +95,12 @@ class Manyfest
|
|
|
72
95
|
}
|
|
73
96
|
else
|
|
74
97
|
{
|
|
75
|
-
this.logError(`(${this.scope}) Error loading scope from manifest; expecting a string but property was type ${typeof(pManifest.Scope)}
|
|
98
|
+
this.logError(`(${this.scope}) Error loading scope from manifest; expecting a string but property was type ${typeof(pManifest.Scope)}.`, pManifest);
|
|
76
99
|
}
|
|
77
100
|
}
|
|
78
101
|
else
|
|
79
102
|
{
|
|
80
|
-
this.logError(`(${this.scope}) Error loading scope from manifest object. Property "Scope" does not exist in the root of the object
|
|
103
|
+
this.logError(`(${this.scope}) Error loading scope from manifest object. Property "Scope" does not exist in the root of the object.`, pManifest);
|
|
81
104
|
}
|
|
82
105
|
|
|
83
106
|
if (pManifest.hasOwnProperty('Descriptors'))
|
|
@@ -92,12 +115,12 @@ class Manyfest
|
|
|
92
115
|
}
|
|
93
116
|
else
|
|
94
117
|
{
|
|
95
|
-
this.logError(`(${this.scope}) Error loading description object from manifest object. Expecting an object in 'Manifest.Descriptors' but the property was type ${typeof(pManifest.
|
|
118
|
+
this.logError(`(${this.scope}) Error loading description object from manifest object. Expecting an object in 'Manifest.Descriptors' but the property was type ${typeof(pManifest.Descriptors)}.`, pManifest);
|
|
96
119
|
}
|
|
97
120
|
}
|
|
98
121
|
else
|
|
99
122
|
{
|
|
100
|
-
this.logError(`(${this.scope}) Error loading object description from manifest object. Property "Descriptors" does not exist in the root of the object
|
|
123
|
+
this.logError(`(${this.scope}) Error loading object description from manifest object. Property "Descriptors" does not exist in the root of the Manifest object.`, pManifest);
|
|
101
124
|
}
|
|
102
125
|
}
|
|
103
126
|
|
|
@@ -136,14 +159,18 @@ class Manyfest
|
|
|
136
159
|
this.elementDescriptors[pAddress] = pDescriptor;
|
|
137
160
|
|
|
138
161
|
// Always add the address as a hash
|
|
139
|
-
// TODO: Check if this is a good idea or not.
|
|
140
|
-
// Collisions are bound to happen with both representations of the address/hash in here.
|
|
141
162
|
this.elementHashes[pAddress] = pAddress;
|
|
142
163
|
|
|
143
164
|
if (pDescriptor.hasOwnProperty('Hash'))
|
|
144
165
|
{
|
|
166
|
+
// TODO: Check if this is a good idea or not..
|
|
167
|
+
// Collisions are bound to happen with both representations of the address/hash in here and developers being able to create their own hashes.
|
|
145
168
|
this.elementHashes[pDescriptor.Hash] = pAddress;
|
|
146
169
|
}
|
|
170
|
+
else
|
|
171
|
+
{
|
|
172
|
+
pDescriptor.Hash = pAddress;
|
|
173
|
+
}
|
|
147
174
|
|
|
148
175
|
return true;
|
|
149
176
|
}
|
|
@@ -156,9 +183,9 @@ class Manyfest
|
|
|
156
183
|
|
|
157
184
|
getDescriptorByHash(pHash)
|
|
158
185
|
{
|
|
159
|
-
if (this.elementHashes.hasOwnProperty(pHash))
|
|
186
|
+
if (this.elementHashes.hasOwnProperty(pHash) || this.hashTranslations.translationTable.hasOwnProperty(pHash))
|
|
160
187
|
{
|
|
161
|
-
return this.getDescriptor(this.elementHashes[pHash]);
|
|
188
|
+
return this.getDescriptor(this.elementHashes[this.hashTranslations.translate(pHash)]);
|
|
162
189
|
}
|
|
163
190
|
else
|
|
164
191
|
{
|
|
@@ -175,209 +202,53 @@ class Manyfest
|
|
|
175
202
|
/*************************************************************************
|
|
176
203
|
* Beginning of Object Manipulation (read & write) Functions
|
|
177
204
|
*/
|
|
178
|
-
//
|
|
179
|
-
|
|
205
|
+
// Check if an element exists by its hash
|
|
206
|
+
checkAddressExistsByHash (pObject, pHash)
|
|
180
207
|
{
|
|
181
|
-
if (this.elementHashes.hasOwnProperty(pHash))
|
|
208
|
+
if (this.elementHashes.hasOwnProperty(pHash) || this.hashTranslations.translationTable.hasOwnProperty(pHash))
|
|
182
209
|
{
|
|
183
|
-
return this.
|
|
210
|
+
return this.checkAddressExists(pObject, this.elementHashes[this.hashTranslations.translate(pHash)]);
|
|
184
211
|
}
|
|
185
212
|
else
|
|
186
213
|
{
|
|
187
|
-
this.logError(`(${this.scope}) Error in
|
|
214
|
+
this.logError(`(${this.scope}) Error in checkAddressExistsByHash; the Hash ${pHash} doesn't exist in the schema.`);
|
|
188
215
|
return undefined;
|
|
189
216
|
}
|
|
190
217
|
}
|
|
191
218
|
|
|
219
|
+
// Check if an element exists at an address
|
|
220
|
+
checkAddressExists (pObject, pAddress)
|
|
221
|
+
{
|
|
222
|
+
return this.objectAddressResolver.checkAddressExists(pObject, pAddress);
|
|
223
|
+
}
|
|
224
|
+
|
|
192
225
|
|
|
193
|
-
|
|
226
|
+
// Get the value of an element by its hash
|
|
227
|
+
getValueByHash (pObject, pHash)
|
|
194
228
|
{
|
|
195
|
-
if (
|
|
229
|
+
if (this.elementHashes.hasOwnProperty(pHash) || this.hashTranslations.translationTable.hasOwnProperty(pHash))
|
|
196
230
|
{
|
|
197
|
-
return
|
|
231
|
+
return this.getValueAtAddress(pObject, this.elementHashes[this.hashTranslations.translate(pHash)]);
|
|
198
232
|
}
|
|
199
233
|
else
|
|
200
234
|
{
|
|
201
|
-
|
|
235
|
+
this.logError(`(${this.scope}) Error in getValueByHash; the Hash ${pHash} doesn't exist in the schema.`);
|
|
236
|
+
return undefined;
|
|
202
237
|
}
|
|
203
238
|
}
|
|
204
239
|
|
|
205
240
|
// Get the value of an element at an address
|
|
206
241
|
getValueAtAddress (pObject, pAddress)
|
|
207
242
|
{
|
|
208
|
-
|
|
209
|
-
if (!typeof(pObject) === 'object') return undefined;
|
|
210
|
-
// Make sure pAddress is a string
|
|
211
|
-
if (!typeof(pAddress) === 'string') return undefined;
|
|
212
|
-
|
|
213
|
-
// TODO: Make this work for things like SomeRootObject.Metadata["Some.People.Use.Bad.Object.Property.Names"]
|
|
214
|
-
let tmpSeparatorIndex = pAddress.indexOf('.');
|
|
215
|
-
|
|
216
|
-
// This is the terminal address string (no more dots so the RECUSION ENDS IN HERE somehow)
|
|
217
|
-
if (tmpSeparatorIndex === -1)
|
|
218
|
-
{
|
|
219
|
-
// Check if it's a boxed property
|
|
220
|
-
let tmpBracketStartIndex = pAddress.indexOf('[');
|
|
221
|
-
let tmpBracketStopIndex = pAddress.indexOf(']');
|
|
222
|
-
// Boxed elements look like this:
|
|
223
|
-
// MyValues[10]
|
|
224
|
-
// MyValues['Name']
|
|
225
|
-
// MyValues["Age"]
|
|
226
|
-
// MyValues[`Cost`]
|
|
227
|
-
//
|
|
228
|
-
// When we are passed SomeObject["Name"] this code below recurses as if it were SomeObject.Name
|
|
229
|
-
// The requirements to detect a boxed element are:
|
|
230
|
-
// 1) The start bracket is after character 0
|
|
231
|
-
if ((tmpBracketStartIndex > 0)
|
|
232
|
-
// 2) The end bracket has something between them
|
|
233
|
-
&& (tmpBracketStopIndex > tmpBracketStartIndex)
|
|
234
|
-
// 3) There is data
|
|
235
|
-
&& (tmpBracketStopIndex - tmpBracketStartIndex > 0))
|
|
236
|
-
{
|
|
237
|
-
// The "Name" of the Object contained too the left of the bracket
|
|
238
|
-
let tmpBoxedPropertyName = pAddress.substring(0, tmpBracketStartIndex).trim();
|
|
239
|
-
|
|
240
|
-
// If the subproperty doesn't test as a proper Object, none of the rest of this is possible.
|
|
241
|
-
// This is a rare case where Arrays testing as Objects is useful
|
|
242
|
-
if (typeof(pObject[tmpBoxedPropertyName]) !== 'object')
|
|
243
|
-
{
|
|
244
|
-
return undefined;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// The "Reference" to the property within it, either an array element or object property
|
|
248
|
-
let tmpBoxedPropertyReference = pAddress.substring(tmpBracketStartIndex+1, tmpBracketStopIndex).trim();
|
|
249
|
-
// Attempt to parse the reference as a number, which will be used as an array element
|
|
250
|
-
let tmpBoxedPropertyNumber = parseInt(tmpBoxedPropertyReference, 10);
|
|
251
|
-
|
|
252
|
-
// Guard: If the referrant is a number and the boxed property is not an array, or vice versa, return undefined.
|
|
253
|
-
// This seems confusing to me at first read, so explaination:
|
|
254
|
-
// Is the Boxed Object an Array? TRUE
|
|
255
|
-
// And is the Reference inside the boxed Object not a number? TRUE
|
|
256
|
-
// --> So when these are in agreement, it's an impossible access state
|
|
257
|
-
if (Array.isArray(pObject[tmpBoxedPropertyName]) == isNaN(tmpBoxedPropertyNumber))
|
|
258
|
-
{
|
|
259
|
-
return undefined;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// 4) If the middle part is *only* a number (no single, double or backtick quotes) it is an array element,
|
|
263
|
-
// otherwise we will try to treat it as a dynamic object property.
|
|
264
|
-
if (isNaN(tmpBoxedPropertyNumber))
|
|
265
|
-
{
|
|
266
|
-
// This isn't a number ... let's treat it as a dynamic object property.
|
|
267
|
-
// We would expect the property to be wrapped in some kind of quotes so strip them
|
|
268
|
-
tmpBoxedPropertyReference = this.cleanWrapCharacters('"', tmpBoxedPropertyReference);
|
|
269
|
-
tmpBoxedPropertyReference = this.cleanWrapCharacters('`', tmpBoxedPropertyReference);
|
|
270
|
-
tmpBoxedPropertyReference = this.cleanWrapCharacters("'", tmpBoxedPropertyReference);
|
|
271
|
-
|
|
272
|
-
// Return the value in the property
|
|
273
|
-
return pObject[tmpBoxedPropertyName][tmpBoxedPropertyReference];
|
|
274
|
-
}
|
|
275
|
-
else
|
|
276
|
-
{
|
|
277
|
-
return pObject[tmpBoxedPropertyName][tmpBoxedPropertyNumber];
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
else
|
|
281
|
-
{
|
|
282
|
-
// Now is the point in recursion to return the value in the address
|
|
283
|
-
return pObject[pAddress];
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
else
|
|
287
|
-
{
|
|
288
|
-
let tmpSubObjectName = pAddress.substring(0, tmpSeparatorIndex);
|
|
289
|
-
let tmpNewAddress = pAddress.substring(tmpSeparatorIndex+1);
|
|
290
|
-
|
|
291
|
-
// Test if the tmpNewAddress is an array or object
|
|
292
|
-
// Check if it's a boxed property
|
|
293
|
-
let tmpBracketStartIndex = tmpSubObjectName.indexOf('[');
|
|
294
|
-
let tmpBracketStopIndex = tmpSubObjectName.indexOf(']');
|
|
295
|
-
// Boxed elements look like this:
|
|
296
|
-
// MyValues[42]
|
|
297
|
-
// MyValues['Color']
|
|
298
|
-
// MyValues["Weight"]
|
|
299
|
-
// MyValues[`Diameter`]
|
|
300
|
-
//
|
|
301
|
-
// When we are passed SomeObject["Name"] this code below recurses as if it were SomeObject.Name
|
|
302
|
-
// The requirements to detect a boxed element are:
|
|
303
|
-
// 1) The start bracket is after character 0
|
|
304
|
-
if ((tmpBracketStartIndex > 0)
|
|
305
|
-
// 2) The end bracket has something between them
|
|
306
|
-
&& (tmpBracketStopIndex > tmpBracketStartIndex)
|
|
307
|
-
// 3) There is data
|
|
308
|
-
&& (tmpBracketStopIndex - tmpBracketStartIndex > 0))
|
|
309
|
-
{
|
|
310
|
-
let tmpBoxedPropertyName = tmpSubObjectName.substring(0, tmpBracketStartIndex).trim();
|
|
311
|
-
|
|
312
|
-
let tmpBoxedPropertyReference = tmpSubObjectName.substring(tmpBracketStartIndex+1, tmpBracketStopIndex).trim();
|
|
313
|
-
|
|
314
|
-
let tmpBoxedPropertyNumber = parseInt(tmpBoxedPropertyReference, 10);
|
|
315
|
-
|
|
316
|
-
// Guard: If the referrant is a number and the boxed property is not an array, or vice versa, return undefined.
|
|
317
|
-
// This seems confusing to me at first read, so explaination:
|
|
318
|
-
// Is the Boxed Object an Array? TRUE
|
|
319
|
-
// And is the Reference inside the boxed Object not a number? TRUE
|
|
320
|
-
// --> So when these are in agreement, it's an impossible access state
|
|
321
|
-
// This could be a failure in the recursion chain because they passed something like this in:
|
|
322
|
-
// StudentData.Sections.Algebra.Students[1].Tardy
|
|
323
|
-
// BUT
|
|
324
|
-
// StudentData.Sections.Algebra.Students[1] is an object, so the .Tardy is not possible to access
|
|
325
|
-
// This could be a failure in the recursion chain because they passed something like this in:
|
|
326
|
-
// StudentData.Sections.Algebra.Students["JaneDoe"].Grade
|
|
327
|
-
// BUT
|
|
328
|
-
// StudentData.Sections.Algebra.Students["JaneDoe"] is an array, so the .Grade is not possible to access
|
|
329
|
-
// TODO: Should this be an error or something? Should we keep a log of failures like this?
|
|
330
|
-
if (Array.isArray(pObject[tmpBoxedPropertyName]) == isNaN(tmpBoxedPropertyNumber))
|
|
331
|
-
{
|
|
332
|
-
return undefined;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
//This is a bracketed value
|
|
336
|
-
// 4) If the middle part is *only* a number (no single, double or backtick quotes) it is an array element,
|
|
337
|
-
// otherwise we will try to reat it as a dynamic object property.
|
|
338
|
-
if (isNaN(tmpBoxedPropertyNumber))
|
|
339
|
-
{
|
|
340
|
-
// This isn't a number ... let's treat it as a dynanmic object property.
|
|
341
|
-
tmpBoxedPropertyReference = this.cleanWrapCharacters('"', tmpBoxedPropertyReference);
|
|
342
|
-
tmpBoxedPropertyReference = this.cleanWrapCharacters('`', tmpBoxedPropertyReference);
|
|
343
|
-
tmpBoxedPropertyReference = this.cleanWrapCharacters("'", tmpBoxedPropertyReference);
|
|
344
|
-
|
|
345
|
-
// Recurse directly into the subobject
|
|
346
|
-
return this.getValueAtAddress(pObject[tmpBoxedPropertyName][tmpBoxedPropertyReference], tmpNewAddress);
|
|
347
|
-
}
|
|
348
|
-
else
|
|
349
|
-
{
|
|
350
|
-
// We parsed a valid number out of the boxed property name, so recurse into the array
|
|
351
|
-
return this.getValueAtAddress(pObject[tmpBoxedPropertyName][tmpBoxedPropertyNumber], tmpNewAddress);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// If there is an object property already named for the sub object, but it isn't an object
|
|
356
|
-
// then the system can't set the value in there. Error and abort!
|
|
357
|
-
if (pObject.hasOwnProperty(tmpSubObjectName) && typeof(pObject[tmpSubObjectName]) !== 'object')
|
|
358
|
-
{
|
|
359
|
-
return undefined;
|
|
360
|
-
}
|
|
361
|
-
else if (pObject.hasOwnProperty(tmpSubObjectName))
|
|
362
|
-
{
|
|
363
|
-
// If there is already a subobject pass that to the recursive thingy
|
|
364
|
-
return this.getValueAtAddress(pObject[tmpSubObjectName], tmpNewAddress);
|
|
365
|
-
}
|
|
366
|
-
else
|
|
367
|
-
{
|
|
368
|
-
// Create a subobject and then pass that
|
|
369
|
-
pObject[tmpSubObjectName] = {};
|
|
370
|
-
return this.getValueAtAddress(pObject[tmpSubObjectName], tmpNewAddress);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
243
|
+
return this.objectAddressResolver.getValueAtAddress(pObject, pAddress);
|
|
373
244
|
}
|
|
374
245
|
|
|
375
246
|
// Set the value of an element by its hash
|
|
376
247
|
setValueByHash(pObject, pHash, pValue)
|
|
377
248
|
{
|
|
378
|
-
if (this.elementHashes.hasOwnProperty(pHash))
|
|
249
|
+
if (this.elementHashes.hasOwnProperty(pHash) || this.hashTranslations.translationTable.hasOwnProperty(pHash))
|
|
379
250
|
{
|
|
380
|
-
return this.setValueAtAddress(pObject, this.elementHashes[pHash], pValue);
|
|
251
|
+
return this.setValueAtAddress(pObject, this.elementHashes[this.hashTranslations.translate(pHash)], pValue);
|
|
381
252
|
}
|
|
382
253
|
else
|
|
383
254
|
{
|
|
@@ -386,79 +257,13 @@ class Manyfest
|
|
|
386
257
|
}
|
|
387
258
|
}
|
|
388
259
|
|
|
260
|
+
|
|
389
261
|
// Set the value of an element at an address
|
|
390
262
|
setValueAtAddress (pObject, pAddress, pValue)
|
|
391
263
|
{
|
|
392
|
-
|
|
393
|
-
if (!typeof(pObject) === 'object') return false;
|
|
394
|
-
// Make sure pAddress is a string
|
|
395
|
-
if (!typeof(pAddress) === 'string') return false;
|
|
396
|
-
|
|
397
|
-
let tmpSeparatorIndex = pAddress.indexOf('.');
|
|
398
|
-
|
|
399
|
-
if (tmpSeparatorIndex === -1)
|
|
400
|
-
{
|
|
401
|
-
// Now is the time to set the value in the object
|
|
402
|
-
pObject[pAddress] = pValue;
|
|
403
|
-
return true;
|
|
404
|
-
}
|
|
405
|
-
else
|
|
406
|
-
{
|
|
407
|
-
let tmpSubObjectName = pAddress.substring(0, tmpSeparatorIndex);
|
|
408
|
-
let tmpNewAddress = pAddress.substring(tmpSeparatorIndex+1);
|
|
409
|
-
|
|
410
|
-
// If there is an object property already named for the sub object, but it isn't an object
|
|
411
|
-
// then the system can't set the value in there. Error and abort!
|
|
412
|
-
if (pObject.hasOwnProperty(tmpSubObjectName) && typeof(pObject[tmpSubObjectName]) !== 'object')
|
|
413
|
-
{
|
|
414
|
-
if (!pObject.hasOwnProperty('__ERROR'))
|
|
415
|
-
pObject['__ERROR'] = {};
|
|
416
|
-
// Put it in an error object so data isn't lost
|
|
417
|
-
pObject['__ERROR'][pAddress] = pValue;
|
|
418
|
-
return false;
|
|
419
|
-
}
|
|
420
|
-
else if (pObject.hasOwnProperty(tmpSubObjectName))
|
|
421
|
-
{
|
|
422
|
-
// If there is already a subobject pass that to the recursive thingy
|
|
423
|
-
return this.setValueAtAddress(pObject[tmpSubObjectName], tmpNewAddress, pValue);
|
|
424
|
-
}
|
|
425
|
-
else
|
|
426
|
-
{
|
|
427
|
-
// Create a subobject and then pass that
|
|
428
|
-
pObject[tmpSubObjectName] = {};
|
|
429
|
-
return this.setValueAtAddress(pObject[tmpSubObjectName], tmpNewAddress, pValue);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
264
|
+
return this.objectAddressResolver.setValueAtAddress(pObject, pAddress, pValue);
|
|
432
265
|
}
|
|
433
266
|
|
|
434
|
-
setValueAtAddressInContainer(pRecordObject, pFormContainerAddress, pFormContainerIndex, pFormValueAddress, pFormValue)
|
|
435
|
-
{
|
|
436
|
-
// First see if there *is* a container object
|
|
437
|
-
let tmpContainerObject = this.getValueAtAddress(pRecordObject, pFormContainerAddress);
|
|
438
|
-
|
|
439
|
-
if (typeof(pFormContainerAddress) !== 'string') return false;
|
|
440
|
-
|
|
441
|
-
let tmpFormContainerIndex = parseInt(pFormContainerIndex, 10);
|
|
442
|
-
if (isNaN(tmpFormContainerIndex)) return false;
|
|
443
|
-
|
|
444
|
-
if ((typeof(tmpContainerObject) !== 'object') || (!Array.isArray(tmpContainerObject)))
|
|
445
|
-
{
|
|
446
|
-
// Check if there is a value here and we want to store it in the "__OverwrittenData" thing
|
|
447
|
-
tmpContainerObject = [];
|
|
448
|
-
this.setValueAtAddress(pRecordObject, pFormContainerAddress, tmpContainerObject);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
for (let i = 0; (tmpContainerObject.length + i) <= (tmpFormContainerIndex+1); i++)
|
|
452
|
-
{
|
|
453
|
-
// Add objects to this container until it has enough
|
|
454
|
-
tmpContainerObject.push({});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Now set the value *in* the container object
|
|
458
|
-
return this.setValueAtAddress(tmpContainerObject[tmpFormContainerIndex], pFormValueAddress, pFormValue);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
|
|
462
267
|
// Validate the consistency of an object against the schema
|
|
463
268
|
validate(pObject)
|
|
464
269
|
{
|
|
@@ -475,7 +280,13 @@ class Manyfest
|
|
|
475
280
|
tmpValidationData.Errors.push(`Expected passed in object to be type object but was passed in ${typeof(pObject)}`);
|
|
476
281
|
}
|
|
477
282
|
|
|
478
|
-
|
|
283
|
+
let addValidationError = (pAddress, pErrorMessage) =>
|
|
284
|
+
{
|
|
285
|
+
tmpValidationData.Error = true;
|
|
286
|
+
tmpValidationData.Errors.push(`Element at address "${pAddress}" ${pErrorMessage}.`);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Now enumerate through the values and check for anomalies based on the schema
|
|
479
290
|
for (let i = 0; i < this.elementAddresses.length; i++)
|
|
480
291
|
{
|
|
481
292
|
let tmpDescriptor = this.getDescriptor(this.elementAddresses[i]);
|
|
@@ -483,18 +294,144 @@ class Manyfest
|
|
|
483
294
|
|
|
484
295
|
if (typeof(tmpValue) == 'undefined')
|
|
485
296
|
{
|
|
297
|
+
// This will technically mean that `Object.Some.Value = undefined` will end up showing as "missing"
|
|
298
|
+
// TODO: Do we want to do a different message based on if the property exists but is undefined?
|
|
486
299
|
tmpValidationData.MissingElements.push(tmpDescriptor.Address);
|
|
487
|
-
|
|
488
300
|
if (tmpDescriptor.Required || this.options.strict)
|
|
489
301
|
{
|
|
490
|
-
|
|
491
|
-
|
|
302
|
+
addValidationError(tmpDescriptor.Address, 'is flagged REQUIRED but is not set in the object');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Now see if there is a data type specified for this element
|
|
307
|
+
if (tmpDescriptor.DataType)
|
|
308
|
+
{
|
|
309
|
+
let tmpElementType = typeof(tmpValue);
|
|
310
|
+
switch(tmpDescriptor.DataType.toString().trim().toLowerCase())
|
|
311
|
+
{
|
|
312
|
+
case 'string':
|
|
313
|
+
if (tmpElementType != 'string')
|
|
314
|
+
{
|
|
315
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} but is of the type ${tmpElementType}`);
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
|
|
319
|
+
case 'number':
|
|
320
|
+
if (tmpElementType != 'number')
|
|
321
|
+
{
|
|
322
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} but is of the type ${tmpElementType}`);
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
|
|
326
|
+
case 'integer':
|
|
327
|
+
if (tmpElementType != 'number')
|
|
328
|
+
{
|
|
329
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} but is of the type ${tmpElementType}`);
|
|
330
|
+
}
|
|
331
|
+
else
|
|
332
|
+
{
|
|
333
|
+
let tmpValueString = tmpValue.toString();
|
|
334
|
+
if (tmpValueString.indexOf('.') > -1)
|
|
335
|
+
{
|
|
336
|
+
// TODO: Is this an error?
|
|
337
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} but has a decimal point in the number.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
341
|
+
|
|
342
|
+
case 'float':
|
|
343
|
+
if (tmpElementType != 'number')
|
|
344
|
+
{
|
|
345
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} but is of the type ${tmpElementType}`);
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
|
|
349
|
+
case 'DateTime':
|
|
350
|
+
let tmpValueDate = new Date(tmpValue);
|
|
351
|
+
if (tmpValueDate.toString() == 'Invalid Date')
|
|
352
|
+
{
|
|
353
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} but is not parsable as a Date by Javascript`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
default:
|
|
357
|
+
// Check if this is a string, in the default case
|
|
358
|
+
// Note this is only when a DataType is specified and it is an unrecognized data type.
|
|
359
|
+
if (tmpElementType != 'string')
|
|
360
|
+
{
|
|
361
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} (which auto-converted to String because it was unrecognized) but is of the type ${tmpElementType}`);
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
492
364
|
}
|
|
493
365
|
}
|
|
494
366
|
}
|
|
495
367
|
|
|
496
368
|
return tmpValidationData;
|
|
497
369
|
}
|
|
370
|
+
|
|
371
|
+
// Returns a default value, or, the default value for the data type (which is overridable with configuration)
|
|
372
|
+
getDefaultValue(pDescriptor)
|
|
373
|
+
{
|
|
374
|
+
if (pDescriptor.hasOwnProperty('Default'))
|
|
375
|
+
{
|
|
376
|
+
return pDescriptor.Default;
|
|
377
|
+
}
|
|
378
|
+
else
|
|
379
|
+
{
|
|
380
|
+
// Default to a null if it doesn't have a type specified.
|
|
381
|
+
// This will ensure a placeholder is created but isn't misinterpreted.
|
|
382
|
+
let tmpDataType = (pDescriptor.hasOwnProperty('DataType')) ? pDescriptor.DataType : 'String';
|
|
383
|
+
if (this.options.defaultValues.hasOwnProperty(tmpDataType))
|
|
384
|
+
{
|
|
385
|
+
return this.options.defaultValues[tmpDataType];
|
|
386
|
+
}
|
|
387
|
+
else
|
|
388
|
+
{
|
|
389
|
+
// give up and return null
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Enumerate through the schema and populate default values if they don't exist.
|
|
396
|
+
populateDefaults(pObject, pOverwriteProperties)
|
|
397
|
+
{
|
|
398
|
+
return this.populateObject(pObject, pOverwriteProperties,
|
|
399
|
+
// This just sets up a simple filter to see if there is a default set.
|
|
400
|
+
(pDescriptor) =>
|
|
401
|
+
{
|
|
402
|
+
return pDescriptor.hasOwnProperty('Default');
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Forcefully populate all values even if they don't have defaults.
|
|
407
|
+
// Based on type, this can do unexpected things.
|
|
408
|
+
populateObject(pObject, pOverwriteProperties, fFilter)
|
|
409
|
+
{
|
|
410
|
+
// Automatically create an object if one isn't passed in.
|
|
411
|
+
let tmpObject = (typeof(pObject) === 'object') ? pObject : {};
|
|
412
|
+
// Default to *NOT OVERWRITING* properties
|
|
413
|
+
let tmpOverwriteProperties = (typeof(pOverwriteProperties) == 'undefined') ? false : pOverwriteProperties;
|
|
414
|
+
// This is a filter function, which is passed the schema and allows complex filtering of population
|
|
415
|
+
// The default filter function just returns true, populating everything.
|
|
416
|
+
let tmpFilterFunction = (typeof(fFilter) == 'function') ? fFilter : (pDescriptor) => { return true; };
|
|
417
|
+
|
|
418
|
+
this.elementAddresses.forEach(
|
|
419
|
+
(pAddress) =>
|
|
420
|
+
{
|
|
421
|
+
let tmpDescriptor = this.getDescriptor(pAddress);
|
|
422
|
+
// Check the filter function to see if this is an address we want to set the value for.
|
|
423
|
+
if (tmpFilterFunction(tmpDescriptor))
|
|
424
|
+
{
|
|
425
|
+
// If we are overwriting properties OR the property does not exist
|
|
426
|
+
if (tmpOverwriteProperties || !this.checkAddressExists(tmpObject, pAddress))
|
|
427
|
+
{
|
|
428
|
+
this.setValueAtAddress(tmpObject, pAddress, this.getDefaultValue(tmpDescriptor));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return tmpObject;
|
|
434
|
+
}
|
|
498
435
|
};
|
|
499
436
|
|
|
500
437
|
module.exports = Manyfest;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Manyfest
|
|
3
|
+
*
|
|
4
|
+
* @license MIT
|
|
5
|
+
*
|
|
6
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
var Chai = require("chai");
|
|
10
|
+
var Expect = Chai.expect;
|
|
11
|
+
|
|
12
|
+
let libManyfest = require('../source/Manyfest.js');
|
|
13
|
+
|
|
14
|
+
let _SampleDataArchiveOrgFrankenberry = require('./Data-Archive-org-Frankenberry.json');
|
|
15
|
+
|
|
16
|
+
suite
|
|
17
|
+
(
|
|
18
|
+
'Manyfest Object Check Element Existence',
|
|
19
|
+
function()
|
|
20
|
+
{
|
|
21
|
+
setup (()=> {} );
|
|
22
|
+
|
|
23
|
+
suite
|
|
24
|
+
(
|
|
25
|
+
'Basic Check Existence',
|
|
26
|
+
()=>
|
|
27
|
+
{
|
|
28
|
+
test
|
|
29
|
+
(
|
|
30
|
+
'It should be easy to check if an element exists by address.',
|
|
31
|
+
(fTestComplete)=>
|
|
32
|
+
{
|
|
33
|
+
let _Manyfest = new libManyfest({ Scope:'Archive.org', Descriptors: {'metadata.creator': {Name:'Creator', Hash:'Creator'}}});
|
|
34
|
+
// Property not in schema:
|
|
35
|
+
let tmpTitleExists = _Manyfest.checkAddressExists(_SampleDataArchiveOrgFrankenberry, 'metadata.title');
|
|
36
|
+
Expect(tmpTitleExists)
|
|
37
|
+
.to.equal(true);
|
|
38
|
+
fTestComplete();
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
test
|
|
42
|
+
(
|
|
43
|
+
'If an element does not exist, checkAddressExists should return false.',
|
|
44
|
+
(fTestComplete)=>
|
|
45
|
+
{
|
|
46
|
+
let _Manyfest = new libManyfest({ Scope:'Archive.org', Descriptors: {'metadata.creator': {Name:'Creator', Hash:'Creator'}}});
|
|
47
|
+
// Property not in schema:
|
|
48
|
+
let tmpTitleExists = _Manyfest.checkAddressExists(_SampleDataArchiveOrgFrankenberry, 'metadata.someStrangeProperty');
|
|
49
|
+
Expect(tmpTitleExists)
|
|
50
|
+
.to.equal(false);
|
|
51
|
+
fTestComplete();
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
test
|
|
55
|
+
(
|
|
56
|
+
'It should be trivial to access subproperties with a schema by hash.',
|
|
57
|
+
(fTestComplete)=>
|
|
58
|
+
{
|
|
59
|
+
let _Manyfest = new libManyfest({ Scope:'Archive.org', Descriptors: {'metadata.creator': {Name:'Creator', Hash:'Creator'}}});
|
|
60
|
+
// Property not schema, accessed by hash:
|
|
61
|
+
let tmpCreator = _Manyfest.checkAddressExistsByHash(_SampleDataArchiveOrgFrankenberry, 'Creator');
|
|
62
|
+
Expect(tmpCreator)
|
|
63
|
+
.to.equal(true);
|
|
64
|
+
fTestComplete();
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
);
|