manyfest 1.0.0

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.
@@ -0,0 +1,357 @@
1
+ /**
2
+ * @license MIT
3
+ * @author <steven@velozo.com>
4
+ */
5
+ let libSimpleLog = require('./Manyfest-LogToConsole.js');
6
+
7
+ /**
8
+ * Manyfest object address-based descriptions and manipulations.
9
+ *
10
+ * @class Manyfest
11
+ */
12
+ class Manyfest
13
+ {
14
+ constructor(pManifest, pInfoLog, pErrorLog)
15
+ {
16
+ // Wire in logging
17
+ this.logInfo = (typeof(pInfoLog) === 'function') ? pInfoLog : libSimpleLog;
18
+ this.logError = (typeof(pErrorLog) === 'function') ? pErrorLog : libSimpleLog;
19
+
20
+ this.options = (
21
+ {
22
+ strict: false
23
+ });
24
+
25
+ this.scope = undefined;
26
+ this.elementAddresses = undefined;
27
+ this.elementHashes = undefined;
28
+ this.elementDescriptors = undefined;
29
+
30
+ this.reset();
31
+
32
+ if (typeof(pManifest) === 'object')
33
+ {
34
+ this.loadManifest(pManifest);
35
+ }
36
+ }
37
+
38
+ /*************************************************************************
39
+ * Schema Manifest Loading, Reading, Manipulation and Serialization Functions
40
+ */
41
+
42
+ // Reset critical manifest properties
43
+ reset()
44
+ {
45
+ this.scope = 'DEFAULT';
46
+ this.elementAddresses = [];
47
+ this.elementHashes = {};
48
+ this.elementDescriptors = {};
49
+ }
50
+
51
+ // Deserialize a Manifest from a string
52
+ deserialize(pManifestString)
53
+ {
54
+ // TODO: Add guards for bad manifest string
55
+ return this.loadManifest(JSON.parse(pManifestString));
56
+ }
57
+
58
+ // Load a manifest from an object
59
+ loadManifest(pManifest)
60
+ {
61
+ if (typeof(pManifest) !== 'object')
62
+ {
63
+ this.logError(`(${this.scope}) Error loading manifest; expecting an object but parameter was type ${typeof(pManifest)}.`);
64
+ return false;
65
+ }
66
+
67
+ if (pManifest.hasOwnProperty('Scope'))
68
+ {
69
+ if (typeof(pManifest.Scope) === 'string')
70
+ {
71
+ this.scope = pManifest.Scope;
72
+ }
73
+ else
74
+ {
75
+ this.logError(`(${this.scope}) Error loading scope from manifest; expecting a string but property was type ${typeof(pManifest.Scope)}.`);
76
+ }
77
+ }
78
+ else
79
+ {
80
+ this.logError(`(${this.scope}) Error loading scope from manifest object. Property "Scope" does not exist in the root of the object.`);
81
+ }
82
+
83
+ if (pManifest.hasOwnProperty('Descriptors'))
84
+ {
85
+ if (typeof(pManifest.Descriptors) === 'object')
86
+ {
87
+ let tmpDescriptionAddresses = Object.keys(pManifest.Descriptors);
88
+ for (let i = 0; i < tmpDescriptionAddresses.length; i++)
89
+ {
90
+ this.addDescriptor(tmpDescriptionAddresses[i], pManifest.Descriptors[tmpDescriptionAddresses[i]]);
91
+ }
92
+ }
93
+ else
94
+ {
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.Description)}.`);
96
+ }
97
+ }
98
+ else
99
+ {
100
+ this.logError(`(${this.scope}) Error loading object description from manifest object. Property "Descriptors" does not exist in the root of the object.`);
101
+ }
102
+ }
103
+
104
+ // Serialize the Manifest to a string
105
+ serialize()
106
+ {
107
+ return JSON.stringify(this.getManifest());
108
+ }
109
+
110
+ getManifest()
111
+ {
112
+ let tmpManifest = (
113
+ {
114
+ Scope: this.scope,
115
+ Descriptors: JSON.parse(JSON.stringify(this.elementDescriptors))
116
+ });
117
+ }
118
+
119
+ // Add a descriptor to the manifest
120
+ addDescriptor(pAddress, pDescriptor)
121
+ {
122
+ if (typeof(pDescriptor) === 'object')
123
+ {
124
+ // Add the Address into the Descriptor if it doesn't exist:
125
+ if (!pDescriptor.hasOwnProperty('Address'))
126
+ {
127
+ pDescriptor.Address = pAddress;
128
+ }
129
+
130
+ if (!this.elementDescriptors.hasOwnProperty(pAddress))
131
+ {
132
+ this.elementAddresses.push(pAddress);
133
+ }
134
+
135
+ // Add the element descriptor to the schema
136
+ this.elementDescriptors[pAddress] = pDescriptor;
137
+
138
+ // 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
+ this.elementHashes[pAddress] = pAddress;
142
+
143
+ if (pDescriptor.hasOwnProperty('Hash'))
144
+ {
145
+ this.elementHashes[pDescriptor.Hash] = pAddress;
146
+ }
147
+
148
+ return true;
149
+ }
150
+ else
151
+ {
152
+ this.logError(`(${this.scope}) Error loading object descriptor for address '${pAddress}' from manifest object. Expecting an object but property was type ${typeof(pDescriptor)}.`);
153
+ return false;
154
+ }
155
+ }
156
+
157
+ getDescriptorByHash(pHash)
158
+ {
159
+ if (this.elementHashes.hasOwnProperty(pHash))
160
+ {
161
+ return this.getDescriptor(this.elementHashes[pHash]);
162
+ }
163
+ else
164
+ {
165
+ this.logError(`(${this.scope}) Error in getDescriptorByHash; the Hash ${pHash} doesn't exist in the schema.`);
166
+ return undefined;
167
+ }
168
+ }
169
+
170
+ getDescriptor(pAddress)
171
+ {
172
+ return this.elementDescriptors[pAddress];
173
+ }
174
+
175
+ /*************************************************************************
176
+ * Beginning of Object Manipulation (read & write) Functions
177
+ */
178
+ // Get the value of an element by its hash
179
+ getValueByHash (pObject, pHash)
180
+ {
181
+ if (this.elementHashes.hasOwnProperty(pHash))
182
+ {
183
+ return this.getValueAtAddress(pObject, this.elementHashes[pHash]);
184
+ }
185
+ else
186
+ {
187
+ this.logError(`(${this.scope}) Error in getValueByHash; the Hash ${pHash} doesn't exist in the schema.`);
188
+ return undefined;
189
+ }
190
+ }
191
+
192
+ // Get the value of an element at an address
193
+ getValueAtAddress (pObject, pAddress)
194
+ {
195
+ // Make sure pObject is an object
196
+ if (!typeof(pObject) === 'object') return undefined;
197
+ // Make sure pAddress is a string
198
+ if (!typeof(pAddress) === 'string') return undefined;
199
+
200
+ let tmpSeparatorIndex = pAddress.indexOf('.');
201
+
202
+ if (tmpSeparatorIndex === -1)
203
+ {
204
+ // Now is the point in recursion to return the value in the address
205
+ return pObject[pAddress];
206
+ }
207
+ else
208
+ {
209
+ let tmpSubObjectName = pAddress.substring(0, tmpSeparatorIndex);
210
+ let tmpNewAddress = pAddress.substring(tmpSeparatorIndex+1);
211
+
212
+ // If there is an object property already named for the sub object, but it isn't an object
213
+ // then the system can't set the value in there. Error and abort!
214
+ if (pObject.hasOwnProperty(tmpSubObjectName) && typeof(pObject[tmpSubObjectName]) !== 'object')
215
+ {
216
+ return undefined;
217
+ }
218
+ else if (pObject.hasOwnProperty(tmpSubObjectName))
219
+ {
220
+ // If there is already a subobject pass that to the recursive thingy
221
+ return this.getValueAtAddress(pObject[tmpSubObjectName], tmpNewAddress);
222
+ }
223
+ else
224
+ {
225
+ // Create a subobject and then pass that
226
+ pObject[tmpSubObjectName] = {};
227
+ return this.getValueAtAddress(pObject[tmpSubObjectName], tmpNewAddress);
228
+ }
229
+ }
230
+ }
231
+
232
+ // Set the value of an element by its hash
233
+ setValueByHash(pObject, pHash, pValue)
234
+ {
235
+ if (this.elementHashes.hasOwnProperty(pHash))
236
+ {
237
+ return this.setValueAtAddress(pObject, this.elementHashes[pHash], pValue);
238
+ }
239
+ else
240
+ {
241
+ this.logError(`(${this.scope}) Error in setValueByHash; the Hash ${pHash} doesn't exist in the schema. Value ${pValue} will not be written!`);
242
+ return undefined;
243
+ }
244
+ }
245
+
246
+ // Set the value of an element at an address
247
+ setValueAtAddress (pObject, pAddress, pValue)
248
+ {
249
+ // Make sure pObject is an object
250
+ if (!typeof(pObject) === 'object') return false;
251
+ // Make sure pAddress is a string
252
+ if (!typeof(pAddress) === 'string') return false;
253
+
254
+ let tmpSeparatorIndex = pAddress.indexOf('.');
255
+
256
+ if (tmpSeparatorIndex === -1)
257
+ {
258
+ // Now is the time to set the value in the object
259
+ pObject[pAddress] = pValue;
260
+ return true;
261
+ }
262
+ else
263
+ {
264
+ let tmpSubObjectName = pAddress.substring(0, tmpSeparatorIndex);
265
+ let tmpNewAddress = pAddress.substring(tmpSeparatorIndex+1);
266
+
267
+ // If there is an object property already named for the sub object, but it isn't an object
268
+ // then the system can't set the value in there. Error and abort!
269
+ if (pObject.hasOwnProperty(tmpSubObjectName) && typeof(pObject[tmpSubObjectName]) !== 'object')
270
+ {
271
+ if (!pObject.hasOwnProperty('__ERROR'))
272
+ pObject['__ERROR'] = {};
273
+ // Put it in an error object so data isn't lost
274
+ pObject['__ERROR'][pAddress] = pValue;
275
+ return false;
276
+ }
277
+ else if (pObject.hasOwnProperty(tmpSubObjectName))
278
+ {
279
+ // If there is already a subobject pass that to the recursive thingy
280
+ return this.setValueAtAddress(pObject[tmpSubObjectName], tmpNewAddress, pValue);
281
+ }
282
+ else
283
+ {
284
+ // Create a subobject and then pass that
285
+ pObject[tmpSubObjectName] = {};
286
+ return this.setValueAtAddress(pObject[tmpSubObjectName], tmpNewAddress, pValue);
287
+ }
288
+ }
289
+ }
290
+
291
+ setValueAtAddressInContainer(pRecordObject, pFormContainerAddress, pFormContainerIndex, pFormValueAddress, pFormValue)
292
+ {
293
+ // First see if there *is* a container object
294
+ let tmpContainerObject = this.getValueAtAddress(pRecordObject, pFormContainerAddress);
295
+
296
+ if (typeof(pFormContainerAddress) !== 'string') return false;
297
+
298
+ let tmpFormContainerIndex = parseInt(pFormContainerIndex, 10);
299
+ if (isNaN(tmpFormContainerIndex)) return false;
300
+
301
+ if ((typeof(tmpContainerObject) !== 'object') || (!Array.isArray(tmpContainerObject)))
302
+ {
303
+ // Check if there is a value here and we want to store it in the "__OverwrittenData" thing
304
+ tmpContainerObject = [];
305
+ this.setValueAtAddress(pRecordObject, pFormContainerAddress, tmpContainerObject);
306
+ }
307
+
308
+ for (let i = 0; (tmpContainerObject.length + i) <= (tmpFormContainerIndex+1); i++)
309
+ {
310
+ // Add objects to this container until it has enough
311
+ tmpContainerObject.push({});
312
+ }
313
+
314
+ // Now set the value *in* the container object
315
+ return this.setValueAtAddress(tmpContainerObject[tmpFormContainerIndex], pFormValueAddress, pFormValue);
316
+ }
317
+
318
+
319
+ // Validate the consistency of an object against the schema
320
+ validate(pObject)
321
+ {
322
+ let tmpValidationData =
323
+ {
324
+ Error: null,
325
+ Errors: [],
326
+ MissingElements:[]
327
+ };
328
+
329
+ if (typeof(pObject) !== 'object')
330
+ {
331
+ tmpValidationData.Error = true;
332
+ tmpValidationData.Errors.push(`Expected passed in object to be type object but was passed in ${typeof(pObject)}`);
333
+ }
334
+
335
+ // Now enumerate through the values and check for anomalies
336
+ for (let i = 0; i < this.elementAddresses.length; i++)
337
+ {
338
+ let tmpDescriptor = this.getDescriptor(this.elementAddresses[i]);
339
+ let tmpValue = this.getValueAtAddress(pObject, tmpDescriptor.Address);
340
+
341
+ if (typeof(tmpValue) == 'undefined')
342
+ {
343
+ tmpValidationData.MissingElements.push(tmpDescriptor.Address);
344
+
345
+ if (tmpDescriptor.Required || this.options.strict)
346
+ {
347
+ tmpValidationData.Error = true;
348
+ tmpValidationData.Errors.push(`Element at address '${tmpDescriptor.Address}' is flagged Required but is not present.`);
349
+ }
350
+ }
351
+ }
352
+
353
+ return tmpValidationData;
354
+ }
355
+ };
356
+
357
+ module.exports = Manyfest;
@@ -0,0 +1,167 @@
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
+ suite
15
+ (
16
+ 'Manyfest Basic',
17
+ function()
18
+ {
19
+ setup
20
+ (
21
+ ()=>
22
+ {
23
+ // No custom per-test spool-up required
24
+ }
25
+ );
26
+
27
+ suite
28
+ (
29
+ 'Object Sanity',
30
+ ()=>
31
+ {
32
+ test
33
+ (
34
+ 'The class should initialize itself into a happy little object.',
35
+ (fTestComplete)=>
36
+ {
37
+ let _Manyfest = new libManyfest({});
38
+ Expect(_Manyfest)
39
+ .to.be.an('object', 'Manyfest should initialize as an object with no parameters.');
40
+ fTestComplete();
41
+ }
42
+ );
43
+ test
44
+ (
45
+ 'Default properties should be automatically set.',
46
+ (fTestComplete)=>
47
+ {
48
+ let _Manyfest = new libManyfest({});
49
+ Expect(_Manyfest.scope)
50
+ .to.be.a('string', 'Manyfest should have a scope.');
51
+ Expect(_Manyfest.scope)
52
+ .to.equal('DEFAULT', 'Manyfest should default to the Scope DEFAULT.');
53
+ fTestComplete();
54
+ }
55
+ );
56
+ }
57
+ );
58
+
59
+ suite
60
+ (
61
+ 'Object Access',
62
+ ()=>
63
+ {
64
+ test
65
+ (
66
+ 'Properties should be gettable and settable without a schema.',
67
+ (fTestComplete)=>
68
+ {
69
+ let _Manyfest = new libManyfest({});
70
+ let _SimpleObject = {Name:'Bob',Age:31,Pets:{Fido:'Dog',Spot:'Cat'}};
71
+
72
+ Expect(_Manyfest.getValueAtAddress(_SimpleObject,'Name'))
73
+ .to.equal('Bob');
74
+
75
+ _Manyfest.setValueAtAddress(_SimpleObject,'Name','Jim');
76
+
77
+ Expect(_Manyfest.getValueAtAddress(_SimpleObject,'Name'))
78
+ .to.equal('Jim');
79
+
80
+ fTestComplete();
81
+ }
82
+ );
83
+ test
84
+ (
85
+ 'Properties should be accessible via Hash.',
86
+ (fTestComplete)=>
87
+ {
88
+ let animalManyfest = new libManyfest(
89
+ {
90
+ "Scope": "Animal",
91
+ "Descriptors":
92
+ {
93
+ "IDAnimal": { "Name":"Database ID", "Description":"The unique integer-based database identifier for an Animal record.", "DataType":"Integer" },
94
+ "Name": { "Description":"The animal's colloquial species name (e.g. Rabbit, Dog, Bear, Mongoose)." },
95
+ "Type": { "Description":"Whether or not the animal is wild, domesticated, agricultural, in a research lab or a part of a zoo.." },
96
+ "MedicalStats":
97
+ {
98
+ "Name":"Medical Statistics", "Description":"Basic medical statistics for this animal"
99
+ },
100
+ "MedicalStats.Temps.MinET": { "Name":"Minimum Environmental Temperature", "NameShort":"MinET", "Description":"Safest minimum temperature for this animal to survive in."},
101
+ "MedicalStats.Temps.MaxET": { "Name":"Maximum Environmental Temperature", "NameShort":"MaxET", "Description":"Safest maximum temperature for this animal to survive in."},
102
+ "MedicalStats.Temps.CET":
103
+ {
104
+ "Name":"Comfortable Environmental Temperature",
105
+ "NameShort":"Comf Env Temp",
106
+ "Hash":"ComfET",
107
+ "Description":"The most comfortable temperature for this animal to survive in."
108
+ }
109
+ }
110
+ });
111
+
112
+ Expect(animalManyfest.getValueByHash({MedicalStats: { Temps: { CET:200 }},Name:'Froggy'}, 'ComfET'))
113
+ .to.equal(200);
114
+
115
+ fTestComplete();
116
+ }
117
+ );
118
+ test
119
+ (
120
+ 'Validate should check that required elements exist',
121
+ (fTestComplete)=>
122
+ {
123
+ let animalManyfest = new libManyfest(
124
+ {
125
+ "Scope": "Animal",
126
+ "Descriptors":
127
+ {
128
+ "IDAnimal": { "Name":"Database ID", "Description":"The unique integer-based database identifier for an Animal record.", "DataType":"Integer" },
129
+ "Name": { "Required":true, "Description":"The animal's colloquial species name (e.g. Rabbit, Dog, Bear, Mongoose)." }
130
+ }
131
+ });
132
+
133
+ let tmpValidationResults = animalManyfest.validate({MedicalStats: { Temps: { CET:200 }},Name:'Froggy'});
134
+
135
+ Expect(tmpValidationResults.Error)
136
+ .to.equal(null);
137
+
138
+ fTestComplete();
139
+ }
140
+ );
141
+ test
142
+ (
143
+ 'Validate should error when required elements do not exist',
144
+ (fTestComplete)=>
145
+ {
146
+ let animalManyfest = new libManyfest(
147
+ {
148
+ "Scope": "Animal",
149
+ "Descriptors":
150
+ {
151
+ "IDAnimal": { "Name":"Database ID", "Description":"The unique integer-based database identifier for an Animal record.", "DataType":"Integer" },
152
+ "Name": { "Required":true, "Description":"The animal's colloquial species name (e.g. Rabbit, Dog, Bear, Mongoose)." }
153
+ }
154
+ });
155
+
156
+ let tmpValidationResults = animalManyfest.validate({MedicalStats: { Temps: { CET:200 }}});
157
+
158
+ Expect(tmpValidationResults.Error)
159
+ .to.equal(true);
160
+
161
+ fTestComplete();
162
+ }
163
+ );
164
+ }
165
+ );
166
+ }
167
+ );