manyfest 1.0.2 → 1.0.3
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 +29 -11
- package/package.json +3 -2
- package/source/Manyfest-LogToConsole.js +1 -1
- package/source/Manyfest-ObjectAddressResolver.js +585 -0
- package/source/Manyfest.js +178 -251
- package/test/Manyfest_Object_CheckExistence_tests.js +70 -0
- package/test/Manyfest_Object_Populate_tests.js +140 -0
- package/test/{Manyfest_ObjectAccess_tests.js → Manyfest_Object_Read_tests.js} +73 -51
- package/test/Manyfest_Object_Validate_tests.js +82 -0
- package/test/Manyfest_Object_Write_tests.js +161 -0
- package/test/Manyfest_tests.js +49 -0
- package/test/Manyfest_AdvancedObjectAccess_tests.js +0 -80
package/source/Manyfest.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @author <steven@velozo.com>
|
|
4
4
|
*/
|
|
5
5
|
let libSimpleLog = require('./Manyfest-LogToConsole.js');
|
|
6
|
+
let libObjectAddressResolver = require('./Manyfest-ObjectAddressResolver.js');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Manyfest object address-based descriptions and manipulations.
|
|
@@ -11,15 +12,31 @@ let libSimpleLog = require('./Manyfest-LogToConsole.js');
|
|
|
11
12
|
*/
|
|
12
13
|
class Manyfest
|
|
13
14
|
{
|
|
14
|
-
constructor(pManifest, pInfoLog, pErrorLog)
|
|
15
|
+
constructor(pManifest, pInfoLog, pErrorLog, pOptions)
|
|
15
16
|
{
|
|
16
17
|
// Wire in logging
|
|
17
18
|
this.logInfo = (typeof(pInfoLog) === 'function') ? pInfoLog : libSimpleLog;
|
|
18
19
|
this.logError = (typeof(pErrorLog) === 'function') ? pErrorLog : libSimpleLog;
|
|
19
20
|
|
|
21
|
+
// Create an object address resolver and map in the functions
|
|
22
|
+
this.objectAddressResolver = new libObjectAddressResolver(this.logInfo, this.logError);
|
|
23
|
+
|
|
20
24
|
this.options = (
|
|
21
25
|
{
|
|
22
|
-
strict: false
|
|
26
|
+
strict: false,
|
|
27
|
+
defaultValues:
|
|
28
|
+
{
|
|
29
|
+
"String": "",
|
|
30
|
+
"Number": 0,
|
|
31
|
+
"Float": 0.0,
|
|
32
|
+
"Integer": 0,
|
|
33
|
+
"Boolean": false,
|
|
34
|
+
"Binary": 0,
|
|
35
|
+
"DateTime": 0,
|
|
36
|
+
"Array": [],
|
|
37
|
+
"Object": {},
|
|
38
|
+
"Null": null
|
|
39
|
+
}
|
|
23
40
|
});
|
|
24
41
|
|
|
25
42
|
this.scope = undefined;
|
|
@@ -72,12 +89,12 @@ class Manyfest
|
|
|
72
89
|
}
|
|
73
90
|
else
|
|
74
91
|
{
|
|
75
|
-
this.logError(`(${this.scope}) Error loading scope from manifest; expecting a string but property was type ${typeof(pManifest.Scope)}
|
|
92
|
+
this.logError(`(${this.scope}) Error loading scope from manifest; expecting a string but property was type ${typeof(pManifest.Scope)}.`, pManifest);
|
|
76
93
|
}
|
|
77
94
|
}
|
|
78
95
|
else
|
|
79
96
|
{
|
|
80
|
-
this.logError(`(${this.scope}) Error loading scope from manifest object. Property "Scope" does not exist in the root of the object
|
|
97
|
+
this.logError(`(${this.scope}) Error loading scope from manifest object. Property "Scope" does not exist in the root of the object.`, pManifest);
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
if (pManifest.hasOwnProperty('Descriptors'))
|
|
@@ -92,12 +109,12 @@ class Manyfest
|
|
|
92
109
|
}
|
|
93
110
|
else
|
|
94
111
|
{
|
|
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.
|
|
112
|
+
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
113
|
}
|
|
97
114
|
}
|
|
98
115
|
else
|
|
99
116
|
{
|
|
100
|
-
this.logError(`(${this.scope}) Error loading object description from manifest object. Property "Descriptors" does not exist in the root of the object
|
|
117
|
+
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
118
|
}
|
|
102
119
|
}
|
|
103
120
|
|
|
@@ -175,201 +192,45 @@ class Manyfest
|
|
|
175
192
|
/*************************************************************************
|
|
176
193
|
* Beginning of Object Manipulation (read & write) Functions
|
|
177
194
|
*/
|
|
178
|
-
//
|
|
179
|
-
|
|
195
|
+
// Check if an element exists by its hash
|
|
196
|
+
checkAddressExistsByHash (pObject, pHash)
|
|
180
197
|
{
|
|
181
198
|
if (this.elementHashes.hasOwnProperty(pHash))
|
|
182
199
|
{
|
|
183
|
-
return this.
|
|
200
|
+
return this.checkAddressExists(pObject, this.elementHashes[pHash]);
|
|
184
201
|
}
|
|
185
202
|
else
|
|
186
203
|
{
|
|
187
|
-
this.logError(`(${this.scope}) Error in
|
|
204
|
+
this.logError(`(${this.scope}) Error in checkAddressExistsByHash; the Hash ${pHash} doesn't exist in the schema.`);
|
|
188
205
|
return undefined;
|
|
189
206
|
}
|
|
190
207
|
}
|
|
191
208
|
|
|
209
|
+
// Check if an element exists at an address
|
|
210
|
+
checkAddressExists (pObject, pAddress)
|
|
211
|
+
{
|
|
212
|
+
return this.objectAddressResolver.checkAddressExists(pObject, pAddress);
|
|
213
|
+
}
|
|
214
|
+
|
|
192
215
|
|
|
193
|
-
|
|
216
|
+
// Get the value of an element by its hash
|
|
217
|
+
getValueByHash (pObject, pHash)
|
|
194
218
|
{
|
|
195
|
-
if (
|
|
219
|
+
if (this.elementHashes.hasOwnProperty(pHash))
|
|
196
220
|
{
|
|
197
|
-
return
|
|
221
|
+
return this.getValueAtAddress(pObject, this.elementHashes[pHash]);
|
|
198
222
|
}
|
|
199
223
|
else
|
|
200
224
|
{
|
|
201
|
-
|
|
225
|
+
this.logError(`(${this.scope}) Error in getValueByHash; the Hash ${pHash} doesn't exist in the schema.`);
|
|
226
|
+
return undefined;
|
|
202
227
|
}
|
|
203
228
|
}
|
|
204
229
|
|
|
205
230
|
// Get the value of an element at an address
|
|
206
231
|
getValueAtAddress (pObject, pAddress)
|
|
207
232
|
{
|
|
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
|
-
}
|
|
233
|
+
return this.objectAddressResolver.getValueAtAddress(pObject, pAddress);
|
|
373
234
|
}
|
|
374
235
|
|
|
375
236
|
// Set the value of an element by its hash
|
|
@@ -386,79 +247,13 @@ class Manyfest
|
|
|
386
247
|
}
|
|
387
248
|
}
|
|
388
249
|
|
|
250
|
+
|
|
389
251
|
// Set the value of an element at an address
|
|
390
252
|
setValueAtAddress (pObject, pAddress, pValue)
|
|
391
253
|
{
|
|
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
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
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);
|
|
254
|
+
return this.objectAddressResolver.setValueAtAddress(pObject, pAddress, pValue);
|
|
459
255
|
}
|
|
460
256
|
|
|
461
|
-
|
|
462
257
|
// Validate the consistency of an object against the schema
|
|
463
258
|
validate(pObject)
|
|
464
259
|
{
|
|
@@ -475,7 +270,13 @@ class Manyfest
|
|
|
475
270
|
tmpValidationData.Errors.push(`Expected passed in object to be type object but was passed in ${typeof(pObject)}`);
|
|
476
271
|
}
|
|
477
272
|
|
|
478
|
-
|
|
273
|
+
let addValidationError = (pAddress, pErrorMessage) =>
|
|
274
|
+
{
|
|
275
|
+
tmpValidationData.Error = true;
|
|
276
|
+
tmpValidationData.Errors.push(`Element at address "${pAddress}" ${pErrorMessage}.`);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Now enumerate through the values and check for anomalies based on the schema
|
|
479
280
|
for (let i = 0; i < this.elementAddresses.length; i++)
|
|
480
281
|
{
|
|
481
282
|
let tmpDescriptor = this.getDescriptor(this.elementAddresses[i]);
|
|
@@ -483,18 +284,144 @@ class Manyfest
|
|
|
483
284
|
|
|
484
285
|
if (typeof(tmpValue) == 'undefined')
|
|
485
286
|
{
|
|
287
|
+
// This will technically mean that `Object.Some.Value = undefined` will end up showing as "missing"
|
|
288
|
+
// TODO: Do we want to do a different message based on if the property exists but is undefined?
|
|
486
289
|
tmpValidationData.MissingElements.push(tmpDescriptor.Address);
|
|
487
|
-
|
|
488
290
|
if (tmpDescriptor.Required || this.options.strict)
|
|
489
291
|
{
|
|
490
|
-
|
|
491
|
-
|
|
292
|
+
addValidationError(tmpDescriptor.Address, 'is flagged REQUIRED but is not set in the object');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Now see if there is a data type specified for this element
|
|
297
|
+
if (tmpDescriptor.DataType)
|
|
298
|
+
{
|
|
299
|
+
let tmpElementType = typeof(tmpValue);
|
|
300
|
+
switch(tmpDescriptor.DataType.toString().trim().toLowerCase())
|
|
301
|
+
{
|
|
302
|
+
case 'string':
|
|
303
|
+
if (tmpElementType != 'string')
|
|
304
|
+
{
|
|
305
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} but is of the type ${tmpElementType}`);
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
|
|
309
|
+
case 'number':
|
|
310
|
+
if (tmpElementType != 'number')
|
|
311
|
+
{
|
|
312
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} but is of the type ${tmpElementType}`);
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
|
|
316
|
+
case 'integer':
|
|
317
|
+
if (tmpElementType != 'number')
|
|
318
|
+
{
|
|
319
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} but is of the type ${tmpElementType}`);
|
|
320
|
+
}
|
|
321
|
+
else
|
|
322
|
+
{
|
|
323
|
+
let tmpValueString = tmpValue.toString();
|
|
324
|
+
if (tmpValueString.indexOf('.') > -1)
|
|
325
|
+
{
|
|
326
|
+
// TODO: Is this an error?
|
|
327
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} but has a decimal point in the number.`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
|
|
332
|
+
case 'float':
|
|
333
|
+
if (tmpElementType != 'number')
|
|
334
|
+
{
|
|
335
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} but is of the type ${tmpElementType}`);
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
|
|
339
|
+
case 'DateTime':
|
|
340
|
+
let tmpValueDate = new Date(tmpValue);
|
|
341
|
+
if (tmpValueDate.toString() == 'Invalid Date')
|
|
342
|
+
{
|
|
343
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} but is not parsable as a Date by Javascript`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
default:
|
|
347
|
+
// Check if this is a string, in the default case
|
|
348
|
+
// Note this is only when a DataType is specified and it is an unrecognized data type.
|
|
349
|
+
if (tmpElementType != 'string')
|
|
350
|
+
{
|
|
351
|
+
addValidationError(tmpDescriptor.Address, `has a DataType ${tmpDescriptor.DataType} (which auto-converted to String because it was unrecognized) but is of the type ${tmpElementType}`);
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
492
354
|
}
|
|
493
355
|
}
|
|
494
356
|
}
|
|
495
357
|
|
|
496
358
|
return tmpValidationData;
|
|
497
359
|
}
|
|
360
|
+
|
|
361
|
+
// Returns a default value, or, the default value for the data type (which is overridable with configuration)
|
|
362
|
+
getDefaultValue(pDescriptor)
|
|
363
|
+
{
|
|
364
|
+
if (pDescriptor.hasOwnProperty('Default'))
|
|
365
|
+
{
|
|
366
|
+
return pDescriptor.Default;
|
|
367
|
+
}
|
|
368
|
+
else
|
|
369
|
+
{
|
|
370
|
+
// Default to a null if it doesn't have a type specified.
|
|
371
|
+
// This will ensure a placeholder is created but isn't misinterpreted.
|
|
372
|
+
let tmpDataType = (pDescriptor.hasOwnProperty('DataType')) ? pDescriptor.DataType : 'String';
|
|
373
|
+
if (this.options.defaultValues.hasOwnProperty(tmpDataType))
|
|
374
|
+
{
|
|
375
|
+
return this.options.defaultValues[tmpDataType];
|
|
376
|
+
}
|
|
377
|
+
else
|
|
378
|
+
{
|
|
379
|
+
// give up and return null
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Enumerate through the schema and populate default values if they don't exist.
|
|
386
|
+
populateDefaults(pObject, pOverwriteProperties)
|
|
387
|
+
{
|
|
388
|
+
return this.populateObject(pObject, pOverwriteProperties,
|
|
389
|
+
// This just sets up a simple filter to see if there is a default set.
|
|
390
|
+
(pDescriptor) =>
|
|
391
|
+
{
|
|
392
|
+
return pDescriptor.hasOwnProperty('Default');
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Forcefully populate all values even if they don't have defaults.
|
|
397
|
+
// Based on type, this can do unexpected things.
|
|
398
|
+
populateObject(pObject, pOverwriteProperties, fFilter)
|
|
399
|
+
{
|
|
400
|
+
// Automatically create an object if one isn't passed in.
|
|
401
|
+
let tmpObject = (typeof(pObject) === 'object') ? pObject : {};
|
|
402
|
+
// Default to *NOT OVERWRITING* properties
|
|
403
|
+
let tmpOverwriteProperties = (typeof(pOverwriteProperties) == 'undefined') ? false : pOverwriteProperties;
|
|
404
|
+
// This is a filter function, which is passed the schema and allows complex filtering of population
|
|
405
|
+
// The default filter function just returns true, populating everything.
|
|
406
|
+
let tmpFilterFunction = (typeof(fFilter) == 'function') ? fFilter : (pDescriptor) => { return true; };
|
|
407
|
+
|
|
408
|
+
this.elementAddresses.forEach(
|
|
409
|
+
(pAddress) =>
|
|
410
|
+
{
|
|
411
|
+
let tmpDescriptor = this.getDescriptor(pAddress);
|
|
412
|
+
// Check the filter function to see if this is an address we want to set the value for.
|
|
413
|
+
if (tmpFilterFunction(tmpDescriptor))
|
|
414
|
+
{
|
|
415
|
+
// If we are overwriting properties OR the property does not exist
|
|
416
|
+
if (tmpOverwriteProperties || !this.checkAddressExists(tmpObject, pAddress))
|
|
417
|
+
{
|
|
418
|
+
this.setValueAtAddress(tmpObject, pAddress, this.getDefaultValue(tmpDescriptor));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return tmpObject;
|
|
424
|
+
}
|
|
498
425
|
};
|
|
499
426
|
|
|
500
427
|
module.exports = Manyfest;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Meadow
|
|
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
|
+
);
|