dal-engine-core-js-lib-dev 0.0.4 → 0.0.6

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/dist/index.cjs CHANGED
@@ -1,25 +1,51 @@
1
1
  'use strict';
2
2
 
3
- /**
4
- * Base class for all objects in design.
5
- */
3
+ class DALEngineError extends Error {
4
+ constructor (message) {
5
+ super(message);
6
+ }
7
+ }
8
+
9
+ class GraphWithNameExistsError extends DALEngineError {
10
+ constructor (name) {
11
+ let msg = `Graph with name "${name}" already exists.`;
12
+ super(msg);
13
+ }
14
+ }
15
+
16
+ class UnknownGraph extends DALEngineError {
17
+ constructor (name) {
18
+ let msg = `Graph with name "${name}" does not exist.`;
19
+ super(msg);
20
+ }
21
+ }
22
+
6
23
  class Base {
7
24
  /**
8
- * Initialize the base class.
25
+ * Initialize the base class of all members of the design.
26
+ * The dal_engine_uid is a unique identifier for each instance.
27
+ * When the design is serialized, the dal_engine_uid is used
28
+ * to identify when the design is being loaded from file.
9
29
  * @param {String} name
10
30
  */
11
31
  constructor () {
12
- this.uid = crypto.randomUUID();
32
+ this.dal_engine_uid = crypto.randomUUID();
13
33
  }
14
34
 
35
+ /**
36
+ * Returns the type of object as specifed in TYPES.js.
37
+ * (e.g. participant, behavior, invariant, graph node, etc.)
38
+ * @returns {Number} The type of node.
39
+ */
15
40
  getType () {
16
41
  return this.type;
17
42
  }
18
43
  }
19
44
 
20
- class DALEngineError extends Error {
21
- constructor (message) {
22
- super(message);
45
+ class BehaviorAlreadyExistsError extends DALEngineError {
46
+ constructor (name) {
47
+ let msg = `Node with Behavior name "${name}" already exists in the graph.`;
48
+ super(msg);
23
49
  }
24
50
  }
25
51
 
@@ -33,6 +59,18 @@ class InvalidTransitionError extends DALEngineError {
33
59
  }
34
60
  }
35
61
 
62
+ class MissingAttributes extends DALEngineError {
63
+ constructor (type, attribute) {
64
+ let msg;
65
+ if (Array.isArray(attribute) && attribute.length > 1) {
66
+ msg = `"${type}" must be initialized with the attributes "${attribute}".`;
67
+ } else {
68
+ msg = `"${type}" must be initialized with the attribute "${attribute}".`;
69
+ }
70
+ super(msg);
71
+ }
72
+ }
73
+
36
74
  class UnknownBehaviorError extends DALEngineError {
37
75
  constructor (behaviorName) {
38
76
  super(`The behavior named "${behaviorName}" was not found in graph.`);
@@ -42,38 +80,69 @@ class UnknownBehaviorError extends DALEngineError {
42
80
  }
43
81
  }
44
82
 
45
- class MissingAttributes extends DALEngineError {
46
- constructor (type, attribute) {
47
- let msg;
48
- if (Array.isArray(attribute) && attribute.length > 1) {
49
- msg = `"${type}" must be initialized with the attributes "${attribute}".`;
50
- } else {
51
- msg = `"${type}" must be initialized with the attribute "${attribute}".`;
52
- }
83
+ /**
84
+ * Checks if the provided object was loaded from a file. This is determind by
85
+ * checking if the argument is an object and has a "dal_engine_uid" attribute,
86
+ * which is added to all objects when they are created and is written to file
87
+ * when the object is serialized.
88
+ *
89
+ * @param {*} obj The object to check.
90
+ * @returns {Boolean} True if the object was loaded from file, false otherwise.
91
+ */
92
+ const isLoadedFromFile = (obj) => {
93
+ return typeof obj === "object" && obj !== null
94
+ && !Array.isArray(obj) && Object.hasOwn(obj, "dal_engine_uid");
95
+ };
96
+
97
+ class ParticipantAlreadyExistsError extends DALEngineError {
98
+ constructor (name) {
99
+ let msg = `Participant with name "${name}" already exists in the behavior.`;
53
100
  super(msg);
54
101
  }
55
102
  }
56
103
 
104
+ class UnknownParticipantError extends DALEngineError {
105
+ constructor (participantName) {
106
+ super(`The participant named "${participantName}" was not found in the behavior.`);
107
+ }
108
+ }
109
+
57
110
  let ENGINE_TYPES = {
58
111
  BEHAVIOR: 1,
59
112
  INVARIANT: 2,
60
113
  PARTICIPANT: 3,
61
- PRIMITIVE: 4,
62
- BEHAVIORAL_CONTROL_GRAPH: 5,
63
- GRAPH_NODE: 6,
114
+ BEHAVIORAL_CONTROL_GRAPH: 4,
115
+ GRAPH_NODE: 5,
64
116
  };
65
117
  ENGINE_TYPES = Object.freeze(ENGINE_TYPES);
66
118
 
67
119
  var ENGINE_TYPES$1 = ENGINE_TYPES;
68
120
 
69
- /**
70
- * Class representing a Invariant in the design.
71
- */
72
121
  class Invariant extends Base {
73
122
  /**
74
- * Initialize the Invariant.
75
- * @param {String} name
76
- * @param args
123
+ * Class representing a Invariant in the design. Invariants are rules that
124
+ * define a valid world state for participants in behaviors. The invariants
125
+ * are used to check if the design has entered a semantically invalid state
126
+ * during execution.
127
+ *
128
+ * The expected attributes in args are:
129
+ * - name: Name of the invariant.
130
+ * - rule: The rule that defines the how to enforce the invariant. This
131
+ * will be formally defined in a collection of invariant rules that can
132
+ * be chosen from when creating an invariant.
133
+ *
134
+ * Currently, the only supported invariant rule is the string min length
135
+ * rule, which can be specified as follows:
136
+ * {
137
+ * "name": "MinLengthConstraint",
138
+ * "rule": {
139
+ * "type": "minLength",
140
+ * "keys": ["value", "name"],
141
+ * "value": 1,
142
+ * },
143
+ * }
144
+ *
145
+ * @param {Object} args The arguments to initialize the invariant with.
77
146
  */
78
147
  constructor (args) {
79
148
  super();
@@ -81,17 +150,13 @@ class Invariant extends Base {
81
150
  this.invariantViolated = false;
82
151
  this.invariantType = null;
83
152
  this.traceId = null;
84
- if (typeof args === "object" && Object.hasOwn(args, "uid")) {
85
- this._loadInvariantFromJSON(args);
86
- } else {
87
- this._loadArgs(args);
88
- }
153
+ (isLoadedFromFile(args) ? this._loadFromFile(args) : this._loadArgs(args));
89
154
  }
90
155
 
91
156
  /**
92
- * Loads the provided arguments.
157
+ * Loads the invariant from the provided arguments.
93
158
  * @throws {MissingAttributes} Thrown when required attr is not present.
94
- * @param {Object} args
159
+ * @param {Object} args The arguments to initialize the invariant with.
95
160
  */
96
161
  _loadArgs (args) {
97
162
  const expectedAttributes = ["name", "rule"];
@@ -108,10 +173,10 @@ class Invariant extends Base {
108
173
  }
109
174
 
110
175
  /**
111
- * Loads the participant from a JSON object.
112
- * @param {Object} invariantJSON
176
+ * Loads the invariant from file.
177
+ * @param {Object} invariantJSON The JSON object to load the invariant from.
113
178
  */
114
- _loadInvariantFromJSON (invariantJSON) {
179
+ _loadFromFile (invariantJSON) {
115
180
  for (const [key, value] of Object.entries(invariantJSON)) {
116
181
  this[key] = value;
117
182
  } // Reset these because they are set by the execution
@@ -120,9 +185,10 @@ class Invariant extends Base {
120
185
  }
121
186
 
122
187
  /**
123
- * Evaluate the invariant.
124
- * @param {*} value
125
- * @returns {Boolean}
188
+ * Evaluate the invariant by applying the invariant rule to the provided
189
+ * value. Returns a flag indicating whether the invariant was violated.
190
+ * @param {*} value The value to evaluate the invariant on.
191
+ * @returns {Boolean} Returns flag indicating if invariant was violated.
126
192
  */
127
193
  evaluate (value) {
128
194
  this.invariantViolated = false;
@@ -133,8 +199,8 @@ class Invariant extends Base {
133
199
  }
134
200
 
135
201
  /**
136
- * Enforce the string min length invariant
137
- * @param value
202
+ * Enforce the string min length invariant.
203
+ * @param {*} value The value to enforce the invariant on.
138
204
  */
139
205
  enforceMinLength (value) {
140
206
  if ("keys" in this.rule) {
@@ -170,7 +236,7 @@ class Invariant extends Base {
170
236
  * automated testing.
171
237
  *
172
238
  * Substrate invariants correspond to a trace that represents
173
- * an environment that reveals a limitation of the substrate,
239
+ * an environment which reveals a limitation of the substrate,
174
240
  * thus motivating the invariant. It is also the environment
175
241
  * in which an implementation can prove that it respects this
176
242
  * invariant.
@@ -187,14 +253,17 @@ class Invariant extends Base {
187
253
  }
188
254
  }
189
255
 
190
- /**
191
- * Class representing a participant in the design.
192
- */
193
256
  class Participant extends Base {
194
257
  /**
195
- * Initialize the semantic participant.
196
- * @param {String} name
197
- * @param args
258
+ * Class representing a participant in the design. The participants are
259
+ * entites in deisgn that participate in the behavior. They are mapped onto
260
+ * the implementation and their value is loaded from the execution. The
261
+ * participants define a valid world state for the behavior to happen in
262
+ * through invariants. When the value of the participant violates an
263
+ * invariant, the design has entered a semantically invalid state and is
264
+ * the root cause of downstream failure(s).
265
+ *
266
+ * @param {Object} args The arguments to initialize the participant.
198
267
  */
199
268
  constructor (args) {
200
269
  super();
@@ -202,11 +271,7 @@ class Participant extends Base {
202
271
  this.invariants = [];
203
272
  this.abstractionId = null;
204
273
  this.invariantViolated = false;
205
- if (typeof args === "object" && Object.hasOwn(args, "uid")) {
206
- this._loadParticipantFromJSON(args);
207
- } else {
208
- this._loadArgs(args);
209
- }
274
+ (isLoadedFromFile(args) ? this._loadFromFile(args) : this._loadArgs(args));
210
275
  }
211
276
 
212
277
  /**
@@ -230,9 +295,9 @@ class Participant extends Base {
230
295
 
231
296
  /**
232
297
  * Loads the participant from a JSON object.
233
- * @param {Object} participantJSON
298
+ * @param {Object} participantJSON The JSON object read from file.
234
299
  */
235
- _loadParticipantFromJSON (participantJSON) {
300
+ _loadFromFile (participantJSON) {
236
301
  for (const [key, value] of Object.entries(participantJSON)) {
237
302
  if (key === "invariants") {
238
303
  value.forEach(node => this.invariants.push(new Invariant(node)));
@@ -243,8 +308,8 @@ class Participant extends Base {
243
308
 
244
309
  /**
245
310
  * Adds an invariant to the participant.
246
- * @param {Invariant} invariant
247
- * @returns
311
+ * @param {Invariant} invariant The invariant to add.
312
+ * @returns {Invariant} The invariant that was added.
248
313
  */
249
314
  addInvariant (invariant) {
250
315
  this.invariants.push(invariant);
@@ -252,16 +317,18 @@ class Participant extends Base {
252
317
  }
253
318
 
254
319
  /**
255
- * Sets the value of the participant.
256
- * @param {*} value
320
+ * Sets the value of this participant.
321
+ * @param {*} value The value to set for the participant.
257
322
  */
258
323
  setValue (value) {
259
324
  this.value = value;
260
325
  }
261
326
 
262
327
  /**
263
- * Enforces the particiants invariants.
264
- * @returns {Boolean}
328
+ * Enforces the participant's invariants. Raises a flag indicating if an
329
+ * invariant was violated and counts the number of invariant violations.
330
+ *
331
+ * @returns {Boolean} Returns flag indicating if any invariant was violated.
265
332
  */
266
333
  enforceInvariants () {
267
334
  this.invariantViolated = false;
@@ -281,19 +348,23 @@ class Participant extends Base {
281
348
  * This abstraction id will be used to assign a value to the
282
349
  * participant from the execution using the logged abstraction id.
283
350
  *
284
- * @param {String} abstractionId
351
+ * @param {String} abstractionId The abstraction ID to map to the
352
+ * participant.
285
353
  */
286
354
  mapAbstraction (abstractionId) {
355
+ /**
356
+ * TODO: Much like the behavior, I am settling on a clean way to map
357
+ * the participant onto the implementation without introducing new
358
+ * unncessary layers.
359
+ */
287
360
  this.abstractionId = abstractionId;
288
361
  }
289
362
  }
290
363
 
291
- /**
292
- * Class representing a Behavior in the design.
293
- */
294
364
  class Behavior extends Base {
295
365
  /**
296
366
  * Initialize the Behavior.
367
+ *
297
368
  * @param {String} name
298
369
  * @param args
299
370
  */
@@ -303,15 +374,12 @@ class Behavior extends Base {
303
374
  this.participants = [];
304
375
  this.abstractionIds = [];
305
376
  this.invalidWorldState = false;
306
- if (typeof args === "object" && Object.hasOwn(args, "uid")) {
307
- this._loadBehaviorFromJSON(args);
308
- } else {
309
- this._loadArgs(args);
310
- }
377
+ (isLoadedFromFile(args) ? this._loadFromFile(args) : this._loadArgs(args));
311
378
  }
312
379
 
313
380
  /**
314
381
  * Loads the provided arguments.
382
+ *
315
383
  * @throws {MissingAttributes} Thrown when required attr is not present.
316
384
  * @param {Object} args
317
385
  */
@@ -330,10 +398,12 @@ class Behavior extends Base {
330
398
  }
331
399
 
332
400
  /**
333
- * Loads the behavior from a JSON object.
334
- * @param {Object} behaviorJSON
401
+ * Loads the behavior from a JSON object that was read from file.
402
+ *
403
+ * @param {Object} behaviorJSON The JSON object representing the behavior
404
+ * read from file.
335
405
  */
336
- _loadBehaviorFromJSON (behaviorJSON) {
406
+ _loadFromFile (behaviorJSON) {
337
407
  for (const [key, value] of Object.entries(behaviorJSON)) {
338
408
  if (key === "participants") {
339
409
  value.forEach(node => this.participants.push(new Participant(node)));
@@ -344,45 +414,73 @@ class Behavior extends Base {
344
414
 
345
415
  /**
346
416
  * Adds a participant to the behavior.
347
- * @param {Participant} participant
348
- * @returns
417
+ *
418
+ * @param {Participant} participant The participant to add.
419
+ * @returns {Participant} The added participant.
420
+ * @throws {ParticipantAlreadyExistsError} Thrown when a participant with
421
+ * the same name already exists in the behavior.
349
422
  */
350
423
  addParticipant (participant) {
424
+ if (this.participants.some(p => p.name === participant.name)) {
425
+ throw new ParticipantAlreadyExistsError(participant.name);
426
+ }
351
427
  this.participants.push(participant);
352
428
  return participant;
353
429
  }
354
430
 
355
431
  /**
356
- * Set the participant value.
357
- * @param {String} participantName
358
- * @param {*} value
432
+ * Sets the value of a participant and checks for invariant violations.
433
+ * If any invariant is violated, the world state for this behavior is
434
+ * marked as invalid.
435
+ *
436
+ * @param {String} name Name of the participant whose value is being set.
437
+ * @param {*} value Value to set for the participant.
438
+ * @throws {UnknownParticipantError} Thrown when a participant with the
439
+ * provided name does not exist in the behavior.
359
440
  */
360
- setParticipantValue (participantName, value) {
361
- const participant = this.participants.find(obj => obj.name === participantName);
441
+ setParticipantValue (name, value) {
442
+ const participant = this.participants.find(obj => obj.name === name);
443
+ if (!participant) {
444
+ throw new UnknownParticipantError(name);
445
+ }
362
446
  participant.value = value;
363
- const violation = participant.enforceInvariants();
364
- if (violation) {
447
+ if (participant.enforceInvariants()) {
365
448
  this.invalidWorldState = true;
366
449
  }
367
450
  }
368
451
 
369
452
  /**
370
- * Maps the abstraction id from execution to the behavior.
371
- * @param {String} abstractionId
453
+ * Maps the abstraction id from implementation to the behavior.
454
+ *
455
+ * @param {String} abstractionId ID of mapped abstraction.
372
456
  */
373
457
  addMapping (abstractionId) {
458
+ /**
459
+ * TODO: After some more thinking, it seems to me that the
460
+ * uniqe identifier should be a file name and line number.
461
+ * It doesn't make sense to create a new abstraction id,
462
+ * however, I will resolve this soon and remove this TODO.
463
+ */
374
464
  this.abstractionIds.push(abstractionId);
375
465
  }
376
466
  }
377
467
 
378
- /**
379
- * Class representing a behavioral control graph node.
380
- */
468
+ class TransitionAlreadyExistsError extends DALEngineError {
469
+ constructor (behaviorName, transitionName) {
470
+ let msg = `Node with behavior named "${behaviorName}" already has a transition\
471
+ to behavior "${transitionName}".`;
472
+ super(msg);
473
+ }
474
+ }
475
+
381
476
  class GraphNode extends Base {
382
477
  /**
383
- * Initialize the node.
384
- * @param {String} name
385
- * @param args
478
+ * Class representing a behavioral control graph node. Each node has a
479
+ * behavior and a list of behaviors it can transition to. The node also
480
+ * has flags to indicate if the behavior is atomic or if the node is a
481
+ * design fork.
482
+ *
483
+ * @param {Object} args The args to initialize the graph node.
386
484
  */
387
485
  constructor (args) {
388
486
  super();
@@ -390,28 +488,41 @@ class GraphNode extends Base {
390
488
  this._behavior = null;
391
489
  this._goToBehaviorIds = [];
392
490
  this._isAtomic = false;
393
- if (typeof args === "object" && args !== null) {
394
- if (Object.hasOwn(args, "uid")) {
395
- this._loadNodeFromJSON(args);
396
- } else {
397
- this._behavior = args.behavior;
398
- this._goToBehaviorIds = args.goToBehaviorsIds;
399
- }
491
+ this._isDesignFork = false;
492
+ (isLoadedFromFile(args) ? this._loadFromFile(args) : this._loadArgs(args));
493
+ }
494
+
495
+ /**
496
+ * Loads the provided arguments.
497
+ * @param {Object} args Arguments to initialize the graph node with.
498
+ * @throws {MissingAttributes} Thrown when required attr is not present.
499
+ */
500
+ _loadArgs (args) {
501
+ const expectedAttributes = ["behavior", "goToBehaviorIds", "isAtomic", "isDesignFork"];
502
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
503
+ // Not an object, so all attributes are missing.
504
+ throw new MissingAttributes("GraphNode", expectedAttributes);
400
505
  }
506
+ expectedAttributes.forEach((attr) => {
507
+ if (!(attr in args)) {
508
+ throw new MissingAttributes("GraphNode", attr);
509
+ }
510
+ this["_" + attr] = args[attr];
511
+ });
401
512
  }
402
513
 
403
514
  /**
404
- * Loads the nodes from a JSON object.
405
- * @param {Object} nodesJSON
515
+ * Loads the node from file.
516
+ * @param {Object} nodeJSON The JSON object read from file.
406
517
  */
407
- _loadNodeFromJSON (nodesJSON) {
408
- for (const [key, value] of Object.entries(nodesJSON)) {
518
+ _loadFromFile (nodeJSON) {
519
+ for (const [key, value] of Object.entries(nodeJSON)) {
409
520
  if (key === "behavior") {
410
521
  this._behavior = new Behavior(value);
411
- } else if (key === "goToBehaviorsIds") {
522
+ } else if (key === "goToBehaviorIds") {
412
523
  value.forEach(behaviorId => this._goToBehaviorIds.push(behaviorId));
413
524
  } else {
414
- this[key] = nodesJSON[key];
525
+ this[key] = nodeJSON[key];
415
526
  }
416
527
  } }
417
528
 
@@ -425,24 +536,27 @@ class GraphNode extends Base {
425
536
 
426
537
  /**
427
538
  * Returns the list of behavior names that this node transitions to.
428
- * @returns {Array}
539
+ * @returns {Array} List of behavior names that this node transitions to.
429
540
  */
430
541
  getGoToBehaviors () {
431
542
  return this._goToBehaviorIds;
432
543
  }
433
544
 
434
545
  /**
435
- * Adds a behavior name to the list of behaviors that this
436
- * node transitions to.
546
+ * Adds a behavior to the transitions from this node.
437
547
  * @param {String} behaviorId ID of behavior.
548
+ * @throws {TransitionAlreadyExistsError} Raised when a transition to the
549
+ * provided behavior already exists.
438
550
  */
439
551
  addGoToBehavior (behaviorId) {
552
+ if (this._goToBehaviorIds.includes(behaviorId)) {
553
+ throw new TransitionAlreadyExistsError(this.behaviorName, behaviorId);
554
+ }
440
555
  this._goToBehaviorIds.push(behaviorId);
441
556
  }
442
557
 
443
558
  /**
444
- * Adds a behavior name to the list of behaviors that this
445
- * node transitions to.
559
+ * Adds list of behaviors to the transitions from this node.
446
560
  * @param {Array} behaviorIds IDs of behaviors.
447
561
  */
448
562
  addGoToBehaviors (behaviorIds) {
@@ -464,7 +578,7 @@ class GraphNode extends Base {
464
578
  * Raises a flag to indicate if the behavior is atomic or not.
465
579
  * @param {Boolean} isAtomic Flag indicates if the behavior is atomic.
466
580
  */
467
- setAtomic (isAtomic) {
581
+ setIsAtomic (isAtomic) {
468
582
  this._isAtomic = isAtomic;
469
583
  }
470
584
 
@@ -476,6 +590,22 @@ class GraphNode extends Base {
476
590
  return this._isAtomic;
477
591
  }
478
592
 
593
+ /**
594
+ * Raises a flag to indicate if this node is a fork in the design.
595
+ * @param {Boolean} forks Flag indicates if the node is a fork.
596
+ */
597
+ setIsDesignFork (forks) {
598
+ this._isDesignFork = forks;
599
+ }
600
+
601
+ /**
602
+ * Returns whether the node is a fork in the design or not.
603
+ * @returns {Boolean}
604
+ */
605
+ isDesignFork () {
606
+ return this._isDesignFork;
607
+ }
608
+
479
609
  /**
480
610
  * Checks if the provided behavior name is a valid
481
611
  * transition from this node.
@@ -493,33 +623,60 @@ class GraphNode extends Base {
493
623
  }
494
624
  }
495
625
 
496
- /**
497
- * Class representing the behavioral control graph.
498
- */
499
626
  class BehavioralControlGraph extends Base {
500
627
  /**
501
- * Initialize the behavioral control graph.
502
- * @param {String} name
503
- * @param args
628
+ * Class representing the behavioral control graph. The behavioral control
629
+ * graph is a directed graph where nodes represent behaviors and edges
630
+ * represent valid transitions between behaviors.
631
+ *
632
+ * The graph can be used to execute the design by starting at the atomic
633
+ * node and transitioning to observed behaviors. As the design is executed,
634
+ * the values of the participants are set from the observed values and the
635
+ * invariants are checked at each transition to recognize if the design has
636
+ * entered a semantically invalid state.
637
+ *
638
+ * Currently, it only has to be initialized with a name and the remaining
639
+ * attributes can be added using the provided methods. If the graph is being
640
+ * loaded from a file, then the presence of a UID in the args will select
641
+ * the relevant method to load the graph from a JSON object.
642
+ *
643
+ * @param {Object} args The args to initialize the behavioral control graph.
504
644
  */
505
645
  constructor (args) {
506
646
  super();
507
647
  this.type = ENGINE_TYPES$1.BEHAVIORAL_CONTROL_GRAPH;
508
648
  this.nodes = [];
509
- if (typeof args === "object" && args !== null) {
510
- if (Object.hasOwn(args, "uid")) {
511
- this._loadGraphFromJSON(args);
512
- } else {
513
- this.name = args.name;
514
- }
649
+ (isLoadedFromFile(args) ? this._loadFromFile(args) : this._loadArgs(args));
650
+ }
651
+
652
+ /**
653
+ * Loads the provided arguments.
654
+ * @throws {MissingAttributes} Thrown when required attr is not present.
655
+ * @param {Object} args
656
+ */
657
+ _loadArgs (args) {
658
+ /**
659
+ * TODO: Move the attributes to private and use getters and setters
660
+ * for them. Repeat for all the other classes.
661
+ */
662
+ const expectedAttributes = ["name"];
663
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
664
+ // Not an object, so all attributes are missing.
665
+ throw new MissingAttributes("BehavioralControlGraph", expectedAttributes);
515
666
  }
667
+ expectedAttributes.forEach((attr) => {
668
+ if (!(attr in args)) {
669
+ throw new MissingAttributes("BehavioralControlGraph", attr);
670
+ }
671
+ this[attr] = args[attr];
672
+ });
516
673
  }
517
674
 
518
675
  /**
519
676
  * Loads the graph from a JSON object..
520
677
  * @param {Object} graphJson
521
678
  */
522
- _loadGraphFromJSON (graphJson) {
679
+ _loadFromFile (graphJson) {
523
680
  for (const [key, value] of Object.entries(graphJson)) {
524
681
  if (key === "nodes") {
525
682
  value.forEach(node => this.nodes.push(new GraphNode(node)));
@@ -529,17 +686,27 @@ class BehavioralControlGraph extends Base {
529
686
  } }
530
687
 
531
688
  /**
532
- * Adds a node to the graph.
533
- * @param {Behavior} behaviorId
534
- * @param {Array} goToBehaviorsIds
535
- * @param {Boolean} isAtomic
536
- * @returns
689
+ * Adds a node to the graph with the provided arguments.
690
+ *
691
+ * @param {Behavior} behaviorId ID of the behavior represented by the node.
692
+ * @param {Array} goToBehaviorIds IDs of the behaviors that are valid
693
+ * transitions from this node.
694
+ * @param {Boolean} isAtomic Flag indicating if the behavior represented by
695
+ * the node is atomic.
696
+ * @param {Boolean} isDesignFork Flag indicating if the node is a
697
+ * design fork.
698
+ * @throws {BehaviorAlreadyExistsError} Raised when a node with the provided
699
+ * @returns {GraphNode} The created graph node.
537
700
  */
538
- _addNode (behaviorId, goToBehaviorsIds, isAtomic) {
701
+ addNode (behaviorId, goToBehaviorIds, isAtomic, isDesignFork) {
702
+ if (this.nodes.some((node) => node.getBehavior().name === behaviorId)) {
703
+ throw new BehaviorAlreadyExistsError(behaviorId);
704
+ }
539
705
  const node = new GraphNode({
540
706
  behavior: new Behavior({name: behaviorId}),
541
- goToBehaviorsIds: goToBehaviorsIds?goToBehaviorsIds:[],
707
+ goToBehaviorIds: goToBehaviorIds?goToBehaviorIds:[],
542
708
  isAtomic: isAtomic?isAtomic:false,
709
+ isDesignFork: isDesignFork?isDesignFork:false,
543
710
  });
544
711
  this.nodes.push(node);
545
712
  return node;
@@ -551,29 +718,25 @@ class BehavioralControlGraph extends Base {
551
718
  * @param {String} behaviorName
552
719
  * @throws {UnknownBehaviorError} Raised when the provided behavior
553
720
  * does not exist in the graph.
554
- * @returns
721
+ * @returns {GraphNode} The found graph node.
555
722
  */
556
- _findNode (behaviorName) {
557
- for (let i = 0; i < this.nodes.length; i++) {
558
- const behavior = this.nodes[i].getBehavior();
559
- if (behavior.name === behaviorName) {
560
- return this.nodes[i];
561
- }
723
+ findNode (behaviorName) {
724
+ const node = this.nodes.find(
725
+ (node) => node.getBehavior().name === behaviorName
726
+ );
727
+ if (!node) {
728
+ throw new UnknownBehaviorError(behaviorName);
562
729
  }
563
- throw new UnknownBehaviorError(behaviorName);
730
+ return node;
564
731
  }
565
732
 
566
733
  /**
567
734
  * Sets the active node given the behavior name.
568
- * The execution provides the next observed
569
- * behavior and the active node indicates if
570
- * if it is a valid transition.
571
- *
572
735
  *
573
736
  * @param {String} behaviorName
574
737
  */
575
- _setCurrentBehavior (behaviorName) {
576
- const node = this._findNode(behaviorName);
738
+ setCurrentBehavior (behaviorName) {
739
+ const node = this.findNode(behaviorName);
577
740
  /**
578
741
  * TODO: Ensure it is atomic because the execution
579
742
  * will only set a behavior when its the first one.
@@ -585,13 +748,15 @@ class BehavioralControlGraph extends Base {
585
748
  /**
586
749
  * Check if the observed behavior is a valid transition
587
750
  * given the current node.
588
- * @param {String} nextBehaviorName
751
+ *
752
+ * @param {String} nextBehaviorName Name of the next behavior to
753
+ * transition to.
589
754
  * @throws {InvalidTransitionError} Raised when the provided
590
755
  * behavior is not a valid transition.
591
756
  */
592
- _goToBehavior (nextBehaviorName) {
757
+ goToBehavior (nextBehaviorName) {
593
758
  if (this.currentNode.isValidTransition(nextBehaviorName)) {
594
- this.currentNode = this._findNode(nextBehaviorName);
759
+ this.currentNode = this.findNode(nextBehaviorName);
595
760
  } else {
596
761
  throw new InvalidTransitionError(this.currentNode.getBehavior().name, nextBehaviorName);
597
762
  }
@@ -613,31 +778,173 @@ class BehavioralControlGraph extends Base {
613
778
  }
614
779
 
615
780
  /**
616
- * An object representing an engine written in Design
617
- * abstraction language. It exposes functions
618
- * configure the engine through the DAL specification.
781
+ * Class representing a collection of atomic graphs.
782
+ *
783
+ * These graphs together represent the design. Within each graph, every node
784
+ * must be part of the same tree. There cannot be multiple disconnected trees,
785
+ * if there are, then it is a separate graph in this collection. Each graph
786
+ * is identified by a unique name and is selected as the active graph when the
787
+ * atomic behavior is observed. The atomic behavior is not transitioned to from
788
+ * any other behavior, it is the root of the tree.
789
+ */
790
+ class Graphs {
791
+ constructor () {
792
+ this._graphs = {};
793
+ this.addGraph("default graph");
794
+ }
795
+
796
+ /**
797
+ * Load the graphs from file.
798
+ *
799
+ * @param {String} jsonText JSON text representing the collection of graphs.
800
+ */
801
+ loadFromJson (jsonText) {
802
+ const parsed = JSON.parse(jsonText);
803
+ Object.keys(parsed._graphs).forEach(graphId => {
804
+ this._graphs[graphId] = new BehavioralControlGraph(parsed._graphs[graphId]);
805
+ });
806
+ this._activeGraph = this._graphs[Object.keys(this._graphs)[0]];
807
+ }
808
+
809
+ /**
810
+ * Given a graph ID, creates the graph and adds it to the collection.
811
+ *
812
+ * @param {String} graphId ID of the graph.
813
+ * @returns {BehavioralControlGraph} The graph that was added.
814
+ * @throws {GraphWithNameExistsError} Raised when a graph with the provided
815
+ * name already exists in the collection of graphs.
816
+ */
817
+ addGraph (graphId) {
818
+ if (graphId in this._graphs) {
819
+ throw new GraphWithNameExistsError(graphId);
820
+ }
821
+ this._graphs[graphId] = new BehavioralControlGraph({name: graphId});
822
+ this._activeGraph = this._graphs[graphId];
823
+ return this._activeGraph;
824
+ }
825
+
826
+ /**
827
+ * Returns the graph with the given graphID from the collection.
828
+ *
829
+ * @param {String} graphId ID of the graph to return.
830
+ * @returns {BehavioralControlGraph} The graph with the given graphId.
831
+ * @throws {UnknownGraph} Raised when a graph with the provided graphId does
832
+ * not exist in the collection of graphs.
833
+ */
834
+ getGraph (graphId) {
835
+ if (graphId in this._graphs) {
836
+ return this._graphs[graphId];
837
+ } else {
838
+ throw new UnknownGraph(graphId);
839
+ }
840
+ }
841
+
842
+ /**
843
+ * Removes a graph with the given id from the collection of graphs. After
844
+ * the graph is removed, it selects the first graph in the collection of
845
+ * graphs. If there are no graphs left in the collection, it creates a new
846
+ * graph with the name "default graph" and selects it as the active graph.
847
+ *
848
+ * @param {String} graphId ID of the graph to remove.
849
+ * @throws {UnknownGraph} Raised when the provided graphId does not exist
850
+ * in the collection of graphs.
851
+ */
852
+ removeGraph (graphId) {
853
+ if (graphId in this._graphs) {
854
+ delete this._graphs[graphId];
855
+ } else {
856
+ throw new UnknownGraph(graphId);
857
+ }
858
+ // TODO: Is there a better policy for which graph to select
859
+ // after the current graph is removed?
860
+ if (Object.keys(this._graphs).length === 0) {
861
+ this.addGraph("default graph");
862
+ } else {
863
+ this._activeGraph = this._graphs[Object.keys(this._graphs)[0]];
864
+ }
865
+ }
866
+
867
+ /**
868
+ * Finds the graph with the given graphId and sets it as the active graph.
869
+ *
870
+ * @param {String} graphId Id of the graph to set as active.
871
+ * @returns {BehavioralControlGraph} The currently active graph.
872
+ * @throws {UnknownGraph} Raised when the provided graphId does not exist
873
+ * in the collection of graphs.
874
+ */
875
+ setActiveGraph (graphId) {
876
+ if (graphId in this._graphs) {
877
+ this._activeGraph = this._graphs[graphId];
878
+ } else {
879
+ throw new UnknownGraph(graphId);
880
+ }
881
+ return this._activeGraph;
882
+ }
883
+
884
+ /**
885
+ * Returns the active graph.
886
+ *
887
+ * @returns {BehavioralControlGraph} The currently active graph.
888
+ */
889
+ getActiveGraph () {
890
+ return this._activeGraph;
891
+ }
892
+
893
+ /**
894
+ * Returns a collection of all the graphs in the design.
895
+ *
896
+ * @returns {Object} The collection of all graphs in the design.
897
+ */
898
+ getGraphs () {
899
+ return this._graphs;
900
+ }
901
+
902
+ /**
903
+ * Returns a list of all the graph names in the design.
904
+ *
905
+ * @returns {Array} A list of all graph names in the design.
906
+ */
907
+ getGraphNames () {
908
+ return Object.keys(this._graphs);
909
+ }
910
+ }
911
+
912
+ /**
913
+ * This engine can be used to define and execute designs defined in a
914
+ * Design Abstraction Language (DAL).
915
+ *
916
+ * This class is the main interface for users to interact with the
917
+ * engine. It exposes functions to configure the engine and to execute
918
+ * the design. It also exposes functions to serialize and deserialize
919
+ * the engine to and from JSON text.
619
920
  *
620
- * The design specified in this engine is mapped onto
621
- * the implementation using abstraction ids. The
622
- * implementation is then instrumented and the resulting
623
- * execution trace is fed back into the engine and is
624
- * automatically debugged by transforming the execution
625
- * into the behavior of the design and enforcing the
626
- * invariants.
921
+ * A design can consist of multiple atomic behavioral control graphs and
922
+ * this class allows users to create, select, and delete graphs. It also
923
+ * allows users to add nodes to the graph and to transition between
924
+ * behaviors in the graph. It also allows users to create participants,
925
+ * behaviors, and invariants and assign them to nodes in the graph.
926
+ *
927
+ * The selected graph is determined by the atomic behavior that is observed.
928
+ * The design is executed by transitioning between behaviors in the graph.
929
+ * The values of the participants are set from the observed values and the
930
+ * invariants are checked at each transition to recognize if the design has
931
+ * entered a semantically invalid state.
627
932
  */
628
933
  class DALEngine {
629
934
  constructor (args) {
630
- this.graph = new BehavioralControlGraph();
631
- this.atomicGraphs = [];
632
- this.loadArgs(args);
935
+ this.graphs = new Graphs();
936
+ this.graph = this.graphs.getActiveGraph();
937
+ this._loadArgs(args);
633
938
  }
634
939
 
635
940
  /**
636
- * Loads the provided arguments.
637
- * @throws {MissingAttributes} Thrown when required attr is not present.
638
- * @param {Object} args
941
+ * Sets the provided arguments to the engine.
942
+ *
943
+ * @throws {MissingAttributes} Thrown when required attributes are
944
+ * not present.
945
+ * @param {Object} args Arguments to load.
639
946
  */
640
- loadArgs (args) {
947
+ _loadArgs (args) {
641
948
  const expectedAttributes = ["name"];
642
949
  if (typeof args !== "object" || args === null || Array.isArray(args)) {
643
950
  // Not an object, so all attributes are missing.
@@ -652,98 +959,189 @@ class DALEngine {
652
959
  }
653
960
 
654
961
  /**
655
- * Exports the behavioral control graph to JSON text.
656
- * @returns {String}
962
+ * Serializes the behavioral control graphs and returns the JSON text.
963
+ *
964
+ * @returns {String} Returns JSON string representing the control graphs.
657
965
  */
658
966
  serialize () {
659
- return JSON.stringify(this.graph);
967
+ return JSON.stringify(this.graphs);
660
968
  }
661
969
 
662
970
  /**
663
- * Import the behavioral control graph from JSON text.
664
- * @param {String} jsonText
971
+ * Loads the behavioral control graphs from JSON text and sets
972
+ * the active graph to the first graph in the collection of graphs.
973
+ *
974
+ * @param {String} serializedText JSON text representing the control
975
+ * graphs.
976
+ * @throws {SyntaxError|TypeError} Thrown when the JSON text is invalid.
665
977
  */
666
- deserialize (jsonText) {
667
- this.graph = new BehavioralControlGraph(JSON.parse(jsonText));
978
+ deserialize (serializedText) {
979
+ // TODO: Improve validation to throw specific error.
980
+ this.graphs = new Graphs();
981
+ this.graphs.loadFromJson(serializedText);
982
+ this.graph = this.graphs.getActiveGraph();
668
983
  }
669
984
 
670
985
  /**
671
- * Creates a participant.
672
- * @param {Object} args
673
- * @returns {Participant}
986
+ * Creates a graph with the given name and sets it as the active graph.
987
+ *
988
+ * @param {String} name Name of the graph to create.
989
+ * @throws {GraphWithNameExistsError} Thrown when a graph with the
990
+ * provided name already exists.
991
+ */
992
+ createGraph (name) {
993
+ this.graph = this.graphs.addGraph(name);
994
+ }
995
+
996
+ /**
997
+ * Sets the active graph to the graph with the given graphId.
998
+ *
999
+ * @param {String} graphId ID of the graph to set as active
1000
+ * @throws {UnknownGraph} Thrown when the provided graphId does
1001
+ * not exist in the collection of graphs.
1002
+ */
1003
+ selectGraph (graphId) {
1004
+ this.graph = this.graphs.setActiveGraph(graphId);
1005
+ }
1006
+
1007
+ /**
1008
+ * Removes the graph with the given graphId
1009
+ *
1010
+ * @param {String} graphId Id of graph to remove.
1011
+ * @throws {UnknownGraph} Thrown when the provided graphId does not
1012
+ * exist in the collection of graphs.
1013
+ */
1014
+ removeGraph (graphId) {
1015
+ this.graphs.removeGraph(graphId);
1016
+ this.graph = this.graphs.getActiveGraph();
1017
+ }
1018
+
1019
+ /**
1020
+ * Returns the names of all the graphs in the design.
1021
+ *
1022
+ * @returns {Array} Returns an array of graph names.
1023
+ */
1024
+ getSelectableGraphs () {
1025
+ return this.graphs.getGraphNames();
1026
+ }
1027
+
1028
+ /**
1029
+ * Creates a participant with the provided args and returns it.
1030
+ *
1031
+ * @param {Object} args Arguments to create the participant with.
1032
+ * @returns {Participant} Returns the created participant.
1033
+ * @throws {MissingAttributes} Thrown when required attributes are not
1034
+ * present in the args. See Participant class for required attributes.
674
1035
  */
675
1036
  createParticipant (args) {
676
1037
  return new Participant(args);
677
1038
  }
678
1039
 
679
1040
  /**
680
- * Creates a behavior.
681
- * @param {Object} args
682
- * @returns {Behavior}
1041
+ * Creates a behavior with the provided args and returns it.
1042
+ *
1043
+ * @param {Object} args Arguments to create the behavior with.
1044
+ * @returns {Behavior} Returns the created behavior.
1045
+ * @throws {MissingAttributes} Thrown when required attributes are not
1046
+ * present in the args. See Behavior class for required attributes.
683
1047
  */
684
1048
  createBehavior (args) {
685
1049
  return new Behavior(args);
686
1050
  }
687
1051
 
688
1052
  /**
689
- * Creates an invariant.
690
- * @param {Object} args
691
- * @returns {Invariant}
1053
+ * Creates an invariant with the provided args and returns it.
1054
+ *
1055
+ * @param {Object} args Arguments to create the invariant with.
1056
+ * @returns {Invariant} Returns the created invariant.
1057
+ * @throws {MissingAttributes} Raised when required attributes are not
1058
+ * present in the args. See Invariant class for required attributes.
692
1059
  */
693
1060
  createInvariant (args) {
694
1061
  return new Invariant(args);
695
1062
  }
696
1063
 
697
-
698
1064
  /**
699
1065
  * Returns the node in the graph with the given behavior name.
700
- * @param {String} behaviorId
701
- * @returns {GraphNode}
1066
+ *
1067
+ * @param {String} behaviorId ID of the behavior.
1068
+ * @returns {GraphNode} Returns the node in the graph with the
1069
+ * given behavior name.
1070
+ * @throws {UnknownBehaviorError} Thrown when the provided behaviorId is not
1071
+ * a valid behavior in the graph.
702
1072
  */
703
1073
  getNode (behaviorId) {
704
- return this.graph._findNode(behaviorId);
1074
+ return this.graph.findNode(behaviorId);
705
1075
  }
706
1076
 
707
1077
  /**
708
1078
  * Adds a node to the graph with the given behaviorId and goToBehaviors.
709
- * @param {String} behaviorId
710
- * @param {Array} goToBehaviorsIds
711
- * @param {Boolean} isAtomic
712
- * @returns {GraphNode}
1079
+ *
1080
+ * @param {String} behaviorId ID of the behavior for the node.
1081
+ * @param {Array} goToBehaviorIds IDs of the behaviors that this node
1082
+ * transitions to.
1083
+ * @param {Boolean} isAtomic Flag to indicate if this node contains an
1084
+ * atomic behavior.
1085
+ * @param {Boolean} isDesignFork Flag to indicate if this node
1086
+ * is a fork in the design.
1087
+ * @returns {GraphNode} Returns the created graph node.
1088
+ * @throws {BehaviorAlreadyExistsError} Raised when a node with the provided
1089
+ * behaviorId already exists in the graph.
713
1090
  */
714
- addNode (behaviorId, goToBehaviorsIds, isAtomic) {
715
- return this.graph._addNode(behaviorId, goToBehaviorsIds, isAtomic);
1091
+ addNode (behaviorId, goToBehaviorIds, isAtomic, isDesignFork) {
1092
+ return this.graph.addNode(
1093
+ behaviorId,
1094
+ goToBehaviorIds,
1095
+ isAtomic,
1096
+ isDesignFork
1097
+ );
716
1098
  }
717
1099
 
718
1100
  /**
719
1101
  * Deletes a node from the graph with the given behaviorId and
720
1102
  * removes it from the goToBehavior list of all other nodes.
721
- * @param {String} behaviorId
1103
+ *
1104
+ * @param {String} behaviorId Behavior ID of the node to delete.
1105
+ * @returns {GraphNode} Returns the deleted graph node.
1106
+ * @throws {UnknownBehaviorError} Thrown when the provided behaviorId is not
1107
+ * a valid behavior in the graph.
722
1108
  */
723
1109
  removeNode (behaviorId) {
724
- const node = this.graph._findNode(behaviorId);
1110
+ // TODO: Move this to graph class, this class shouldn't
1111
+ // modifiy the graph structure directly.
1112
+ const node = this.graph.findNode(behaviorId);
725
1113
  const nodeIndex = this.graph.nodes.indexOf(node);
726
- this.graph.nodes.splice(nodeIndex, 1);
1114
+ const removedNode = this.graph.nodes.splice(nodeIndex, 1);
727
1115
  for (const node of this.graph.nodes) {
728
1116
  node.removeGoToBehavior(behaviorId);
729
1117
  }
1118
+ return removedNode[0];
730
1119
  }
731
1120
 
732
1121
  /**
733
- * Sets the current behavior in the graph.
734
- * @param {String} behaviorId
1122
+ * Sets the current behavior in the graph. Since the behavior is not
1123
+ * being transitioned from another, it must be an atomic behavior.
1124
+ *
1125
+ * @param {String} behaviorId ID of the behavior to set as current.
1126
+ * @throws {UnknownBehaviorError} Thrown when the provided behavior is not
1127
+ * a valid behavior in the graph.
735
1128
  */
736
1129
  setCurrentBehavior (behaviorId) {
737
- this.graph._setCurrentBehavior(behaviorId);
1130
+ this.graph.setCurrentBehavior(behaviorId);
738
1131
  }
739
1132
 
740
1133
  /**
741
- * Transitions the graph to the given behavior if it
742
- * is a valid transition from the current behavior.
1134
+ * For the current node in the graph, transitions to the node with the given
1135
+ * behaviorId if it is a valid transition.
1136
+ *
743
1137
  * @param {String} nextBehaviorId ID of the next behavior.
1138
+ * @throws {UnknownBehaviorError} Thrown when the provided behavior is not
1139
+ * a valid behavior in the graph.
1140
+ * @throws {InvalidTransitionError} Thrown when the provided behavior is not
1141
+ * a valid transition from the current node.
744
1142
  */
745
1143
  goToBehavior (nextBehaviorId) {
746
- this.graph._goToBehavior(nextBehaviorId);
1144
+ this.graph.goToBehavior(nextBehaviorId);
747
1145
  }
748
1146
  }
749
1147