dal-engine-core-js-lib-dev 0.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,111 @@
1
+ import Base from "../Base";
2
+ import InvalidTransitionError from "../Errors/InvalidTransitionError";
3
+ import UnknownBehaviorError from "../Errors/UnknownBehaviorError";
4
+ import ENGINE_TYPES from "../TYPES";
5
+ import GraphNode from "./GraphNode";
6
+
7
+ /**
8
+ * Class representing the behavioral control graph.
9
+ */
10
+ class BehavioralControlGraph extends Base {
11
+ /**
12
+ * Initialize the behavioral control graph.
13
+ * @param {String} name
14
+ * @param args
15
+ */
16
+ constructor (args) {
17
+ super();
18
+ this.type = ENGINE_TYPES.BEHAVIORAL_CONTROL_GRAPH;
19
+ this.nodes = [];
20
+ if (typeof args === "object" && args !== null) {
21
+ if (Object.hasOwn(args, "uid")) {
22
+ this.loadGraphFromJSON(args);
23
+ } else {
24
+ this.name = args.name;
25
+ }
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Loads the graph from a JSON object..
31
+ * @param {Object} graphJson
32
+ */
33
+ loadGraphFromJSON (graphJson) {
34
+ for (const [key, value] of Object.entries(graphJson)) {
35
+ if (key === "nodes") {
36
+ value.forEach(node => this.nodes.push(new GraphNode(node)));
37
+ } else {
38
+ this[key] = graphJson[key];
39
+ }
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Adds a node to the graph.
45
+ * @param {Behavior} behavior
46
+ * @param {Array} goToBehaviors
47
+ * @returns
48
+ */
49
+ addNode (behavior, goToBehaviors) {
50
+ const node = new GraphNode({
51
+ behavior: behavior,
52
+ goToBehaviors: goToBehaviors,
53
+ });
54
+ this.nodes.push(node);
55
+ return node;
56
+ }
57
+
58
+
59
+ /**
60
+ * Finds the given node given the behavior name.
61
+ * @param {String} behaviorName
62
+ * @throws {UnknownBehaviorError} Raised when the provided behavior
63
+ * does not exist in the graph.
64
+ * @returns
65
+ */
66
+ findNode (behaviorName) {
67
+ for (let i = 0; i < this.nodes.length; i++) {
68
+ const behavior = this.nodes[i].behavior;
69
+ if (behavior.name === behaviorName) {
70
+ return this.nodes[i];
71
+ }
72
+ }
73
+ throw new UnknownBehaviorError(behaviorName);
74
+ }
75
+
76
+ /**
77
+ * Sets the active node given the behavior name.
78
+ * The execution provides the next observed
79
+ * behavior and the active node indicates if
80
+ * if it is a valid transition.
81
+ *
82
+ *
83
+ * @param {String} behaviorName
84
+ */
85
+ setCurrentBehavior (behaviorName) {
86
+ const node = this.findNode(behaviorName);
87
+ /**
88
+ * TODO: Ensure it is atomic because the execution
89
+ * will only set a behavior when its the first one.
90
+ * It will walk using goToBehavior after that.
91
+ */
92
+ this.currentNode = node;
93
+ }
94
+
95
+ /**
96
+ * Check if the observed behavior is a valid transition
97
+ * given the current node.
98
+ * @param {String} nextBehaviorName
99
+ * @throws {InvalidTransitionError} Raised when the provided
100
+ * behavior is not a valid transition.
101
+ */
102
+ goToBehavior (nextBehaviorName) {
103
+ if (this.currentNode.isValidGoToBehavior(nextBehaviorName)) {
104
+ this.currentNode = this.findNode(nextBehaviorName);
105
+ } else {
106
+ throw new InvalidTransitionError(this.currentNode.behavior.name, nextBehaviorName);
107
+ }
108
+ }
109
+ }
110
+
111
+ export default BehavioralControlGraph;
@@ -0,0 +1,62 @@
1
+ import Base from "../Base";
2
+ import Behavior from "../Members/Behavior";
3
+ import ENGINE_TYPES from "../TYPES";
4
+ /**
5
+ * Class representing a behavioral control graph node.
6
+ */
7
+ class GraphNode extends Base {
8
+ /**
9
+ * Initialize the node.
10
+ * @param {String} name
11
+ * @param args
12
+ */
13
+ constructor (args) {
14
+ super();
15
+ this.type = ENGINE_TYPES.GRAPH_NODE;
16
+ this.goToBehaviors = [];
17
+ if (typeof args === "object" && args !== null) {
18
+ if (Object.hasOwn(args, "uid")) {
19
+ this.loadNodeFromJSON(args);
20
+ } else {
21
+ this.behavior = args.behavior;
22
+ this.goToBehaviors = args.goToBehaviors;
23
+ }
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Loads the nodes from a JSON object.
29
+ * @param {Object} nodesJSON
30
+ */
31
+ loadNodeFromJSON (nodesJSON) {
32
+ for (const [key, value] of Object.entries(nodesJSON)) {
33
+ if (key === "behavior") {
34
+ this.behavior = new Behavior(value);
35
+ } else if (key === "goToBehaviors") {
36
+ value.forEach(node => this.goToBehaviors.push(new Behavior(node)));
37
+ } else {
38
+ this[key] = nodesJSON[key];
39
+ }
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Checks if the provided behavior name is a valid
45
+ * behavior that the control flow selects as a
46
+ * result of the this nodes state transformation. i.e.
47
+ * is this behavior in the goToBehavior list.
48
+ * @param {String} behaviorName
49
+ * @returns {Boolean}
50
+ */
51
+ isValidGoToBehavior (behaviorName) {
52
+ for (let i = 0; i < this.goToBehaviors.length; i++) {
53
+ const behavior = this.goToBehaviors[i];
54
+ if (behavior.name === behaviorName) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+ }
61
+
62
+ export default GraphNode;
@@ -0,0 +1,73 @@
1
+ import BehavioralControlGraph from "./BehavioralControlGraph/BehavioralControlGraph";
2
+ import Behavior from "./Members/Behavior";
3
+ import Invariant from "./Members/Invariant";
4
+ import Participant from "./Members/Participant";
5
+
6
+ /**
7
+ * An object representing an engine written in Design
8
+ * abstraction language. It exposes functions
9
+ * configure the engine through the DAL specification.
10
+ *
11
+ * The execution of an program instrumented with the
12
+ * same design be used to step through the design
13
+ * while the engine automatically debugs the execution.
14
+ */
15
+ export class DALEngine {
16
+ constructor (args) {
17
+ this.graph = new BehavioralControlGraph();
18
+ for (const [key, value] of Object.entries(args)) {
19
+ this[key] = value;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Exports the behavioral control graph to JSON text.
25
+ * @returns {String}
26
+ */
27
+ serialize () {
28
+ return JSON.stringify(this.graph);
29
+ }
30
+
31
+ /**
32
+ * Import the behavioral control graph from JSON text.
33
+ * @param {String} jsonText
34
+ */
35
+ deserialize (jsonText) {
36
+ this.graph = new BehavioralControlGraph();
37
+ this.graph.loadGraphFromJSON(JSON.parse(jsonText));
38
+ }
39
+
40
+ /**
41
+ * Creates a participant.
42
+ * @param {Object} args
43
+ * @returns {Participant}
44
+ */
45
+ createParticipant (args) {
46
+ // TODO: Validate that the args have the necessary keys
47
+ // and raise custom error if they are missing.
48
+ // Perhaps it is better to do that in the class itself.
49
+ return new Participant(args);
50
+ }
51
+
52
+ /**
53
+ * Creates a behavior.
54
+ * @param {Object} args
55
+ * @returns {Behavior}
56
+ */
57
+ createBehavior (args) {
58
+ // TODO: Validate that the args have the necessary keys
59
+ // and raise custom error if they are missing.
60
+ return new Behavior(args);
61
+ }
62
+
63
+ /**
64
+ * Creates an invariant.
65
+ * @param {Object} args
66
+ * @returns {Invariant}
67
+ */
68
+ createInvariant (args) {
69
+ // TODO: Validate that the args have the necessary keys
70
+ // and raise custom error if they are missing.
71
+ return new Invariant(args);
72
+ }
73
+ }
@@ -0,0 +1,7 @@
1
+ class DALEngineError extends Error {
2
+ constructor (message) {
3
+ super(message);
4
+ }
5
+ }
6
+
7
+ export default DALEngineError;
@@ -0,0 +1,13 @@
1
+ import DALEngineError from "./DALEngineError";
2
+
3
+ class InvalidTransitionError extends DALEngineError {
4
+ constructor (from, to) {
5
+ super(`Invalid transition from "${from}" to "${to}"`);
6
+
7
+ this.name = "InvalidTransitionError";
8
+ this.prevBehavior = from;
9
+ this.nextBehavior = to;
10
+ }
11
+ }
12
+
13
+ export default InvalidTransitionError;
@@ -0,0 +1,12 @@
1
+ import DALEngineError from "./DALEngineError";
2
+
3
+ class UnknownBehaviorError extends DALEngineError {
4
+ constructor (behaviorName) {
5
+ super(`The behavior named "${behaviorName}" was not found in graph.`);
6
+
7
+ this.name = "UnknownBehaviorError";
8
+ this.behaviorName = behaviorName;
9
+ }
10
+ }
11
+
12
+ export default UnknownBehaviorError;
@@ -0,0 +1,70 @@
1
+ import Base from "../Base";
2
+ import ENGINE_TYPES from "../TYPES";
3
+ import Participant from "./Participant";
4
+ /**
5
+ * Class representing a Behavior in the design.
6
+ */
7
+ class Behavior extends Base {
8
+ /**
9
+ * Initialize the Behavior.
10
+ * @param {String} name
11
+ * @param args
12
+ */
13
+ constructor (args) {
14
+ super();
15
+ this.type = ENGINE_TYPES.BEHAVIOR;
16
+ this.participants = [];
17
+ this.invalidWorldState = false;
18
+ if (typeof args === "object" && args !== null) {
19
+ if (Object.hasOwn(args, "uid")) {
20
+ this.loadBehaviorFromJSON(args);
21
+ } else {
22
+ this.name = args.name;
23
+ }
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Loads the behavior from a JSON object.
29
+ * @param {Object} behaviorJSON
30
+ */
31
+ loadBehaviorFromJSON (behaviorJSON) {
32
+ for (const [key, value] of Object.entries(behaviorJSON)) {
33
+ if (key === "participants") {
34
+ value.forEach(node => this.participants.push(new Participant(node)));
35
+ } else {
36
+ this[key] = behaviorJSON[key];
37
+ }
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Adds a participant to the behavior.
43
+ * @param {Participant} participant
44
+ * @returns
45
+ */
46
+ addParticpant (participant) {
47
+ this.participants.push(participant);
48
+ return participant;
49
+ }
50
+
51
+ /**
52
+ * Set the participant value.
53
+ * @param {String} participantName
54
+ * @param {*} value
55
+ */
56
+ setParticipantValue (participantName, value) {
57
+ for (let i = 0; i < this.participants.length; i++) {
58
+ const participant = this.participants[i];
59
+ if (participant.name === participantName) {
60
+ participant.value = value;
61
+ const violation = participant.enforceInvariants();
62
+ if (violation) {
63
+ this.invalidWorldState = true;
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ export default Behavior;
@@ -0,0 +1,69 @@
1
+ import Base from "../Base";
2
+ import ENGINE_TYPES from "../TYPES";
3
+ /**
4
+ * Class representing a Invariant in the design.
5
+ */
6
+ class Invariant extends Base {
7
+ /**
8
+ * Initialize the Invariant.
9
+ * @param {String} name
10
+ * @param args
11
+ */
12
+ constructor (args) {
13
+ super();
14
+ this.type = ENGINE_TYPES.INVARIANT;
15
+ this.invariantViolated = false
16
+ if (typeof args === "object" && args !== null) {
17
+ if (Object.hasOwn(args, "uid")) {
18
+ this.loadInvariantFromJSON(args);
19
+ } else {
20
+ this.name = args.name;
21
+ this.rule = args.rule;
22
+ }
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Loads the participant from a JSON object.
28
+ * @param {Object} invariantJSON
29
+ */
30
+ loadInvariantFromJSON (invariantJSON) {
31
+ for (const [key, value] of Object.entries(invariantJSON)) {
32
+ this[key] = value;
33
+ };
34
+ // Reset these because they are set by the execution
35
+ this.invariantViolated = null;
36
+ this.value = null;
37
+ }
38
+
39
+ /**
40
+ * Evaluate the invariant.
41
+ * @param {*} value
42
+ * @returns {Boolean}
43
+ */
44
+ evaluate (value) {
45
+ this.invariantViolated = false;
46
+ if (this.rule.type === "minLength") {
47
+ this.enforceMinLength(value);
48
+ }
49
+ return this.invariantViolated;
50
+ }
51
+
52
+ /**
53
+ * Enforce the string min length invariant
54
+ * @param value
55
+ */
56
+ enforceMinLength (value) {
57
+ if ("keys" in this.rule) {
58
+ for (let i = 0; i < this.rule["keys"].length; i++) {
59
+ console.log(this.rule["keys"][i], value)
60
+ value = value[this.rule["keys"][i]];
61
+ }
62
+ };
63
+ if (value === null || typeof value !== "string" || value.length < this.rule.value) {
64
+ this.invariantViolated = true;
65
+ }
66
+ }
67
+ }
68
+
69
+ export default Invariant;
@@ -0,0 +1,76 @@
1
+ import Base from "../Base";
2
+ import ENGINE_TYPES from "../TYPES";
3
+ import Invariant from "./Invariant";
4
+ /**
5
+ * Class representing a participant in the design.
6
+ */
7
+ class Participant extends Base {
8
+ /**
9
+ * Initialize the semantic participant.
10
+ * @param {String} name
11
+ * @param args
12
+ */
13
+ constructor (args) {
14
+ super();
15
+ this.type = ENGINE_TYPES.INVARIANT;
16
+ this.invariants = [];
17
+ this.invariantViolated = false;
18
+ if (typeof args === "object" && args !== null) {
19
+ if (Object.hasOwn(args, "uid")) {
20
+ this.loadParticipantFromJSON(args);
21
+ } else {
22
+ this.name = args.name;
23
+ }
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Loads the participant from a JSON object.
29
+ * @param {Object} participantJSON
30
+ */
31
+ loadParticipantFromJSON (participantJSON) {
32
+ for (const [key, value] of Object.entries(participantJSON)) {
33
+ if (key === "invariants") {
34
+ value.forEach(node => this.invariants.push(new Invariant(node)));
35
+ } else {
36
+ this[key] = participantJSON[key];
37
+ }
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Adds an invariant to the participant.
43
+ * @param {Invariant} invariant
44
+ * @returns
45
+ */
46
+ addInvariant (invariant) {
47
+ this.invariants.push(invariant);
48
+ return invariant;
49
+ }
50
+
51
+ /**
52
+ * Sets the value of the participant.
53
+ * @param {*} value
54
+ */
55
+ setValue (value) {
56
+ this.value = value;
57
+ }
58
+
59
+ /**
60
+ * Enforces the particiants invariants.
61
+ * @returns {Boolean}
62
+ */
63
+ enforceInvariants () {
64
+ this.invariantViolated = false
65
+ this.invariantViolationCount = 0;
66
+ for (let i = 0; i < this.invariants.length; i++) {
67
+ if (this.invariants[i].evaluate(this.value)) {
68
+ this.invariantViolated = true;
69
+ this.invariantViolationCount++;
70
+ }
71
+ }
72
+ return this.invariantViolated;
73
+ }
74
+ }
75
+
76
+ export default Participant;
package/src/TYPES.js ADDED
@@ -0,0 +1,11 @@
1
+ let ENGINE_TYPES = {
2
+ BEHAVIOR: 1,
3
+ INVARIANT: 2,
4
+ PARTICIPANT: 3,
5
+ PRIMITIVE: 4,
6
+ BEHAVIORAL_CONTROL_GRAPH: 5,
7
+ GRAPH_NODE: 6,
8
+ };
9
+ ENGINE_TYPES = Object.freeze(ENGINE_TYPES);
10
+
11
+ export default ENGINE_TYPES;
@@ -0,0 +1,130 @@
1
+ import {readFile, unlink, writeFile} from "fs/promises"
2
+ import {resolve} from "path"
3
+ import {describe, expect, it} from "vitest";
4
+
5
+ import {DALEngine} from "../src/DALEngine.js";
6
+ import InvalidTransitionError from "../src/Errors/InvalidTransitionError.js";
7
+ import UnknownBehaviorError from "../src/Errors/UnknownBehaviorError.js";
8
+ import ENGINE_TYPES from "../src/TYPES.js";
9
+
10
+ describe("DALEngine", () => {
11
+ it("sets the name correctly", () => {
12
+ const dalInstance = new DALEngine({name: "Library Manager"});
13
+ expect(dalInstance.name).toBe("Library Manager");
14
+ });
15
+
16
+ it("adds node to graph", () => {
17
+ const d = new DALEngine({name: "Library Manager"});
18
+ const behavior1 = d.createBehavior({name: "AcceptBookFromUser"});
19
+ const behavior2 = d.createBehavior({name: "AddBookToBasket"});
20
+ const goToBehaviors = [behavior2];
21
+
22
+ const node = d.graph.addNode(behavior1, goToBehaviors)
23
+
24
+ const nodeType = node.type;
25
+ expect(nodeType).toBe(ENGINE_TYPES.GRAPH_NODE);
26
+ expect(node.behavior).toStrictEqual(behavior1);
27
+ expect(node.goToBehaviors).toStrictEqual([behavior2]);
28
+
29
+ const foundNode = d.graph.findNode("AcceptBookFromUser");
30
+ expect(foundNode).toStrictEqual(node);
31
+ });
32
+
33
+ it("find node that was added using behavior name", () => {
34
+ const d = new DALEngine({name: "Library Manager"});
35
+ const behavior1 = d.createBehavior({name: "AcceptBookFromUser"});
36
+ const behavior2 = d.createBehavior({name: "AddBookToBasket"});
37
+ const node = d.graph.addNode(behavior1, [behavior2])
38
+
39
+ const foundNode = d.graph.findNode("AcceptBookFromUser");
40
+ expect(foundNode).toStrictEqual(node);
41
+ });
42
+
43
+ it("find node and check if observed behavior is valid transition", () => {
44
+ const d = new DALEngine({name: "Library Manager"});
45
+ const behavior1 = d.createBehavior({name: "AcceptBookFromUser"});
46
+ const behavior2 = d.createBehavior({name: "AddBookToBasket"});
47
+ const behavior3 = d.createBehavior({name: "AnotherBehavior"});
48
+ d.graph.addNode(behavior1, [behavior2, behavior3])
49
+ d.graph.addNode(behavior2, [])
50
+ d.graph.addNode(behavior3, [])
51
+
52
+ // Misspell behavior name to trigger unknown behavior error
53
+ expect(() => {
54
+ d.graph.setCurrentBehavior("AcceptBookromUser");
55
+ }).toThrow(UnknownBehaviorError);
56
+
57
+ d.graph.setCurrentBehavior("AcceptBookFromUser");
58
+ expect(d.graph.currentNode.behavior).toBe(behavior1);
59
+
60
+ d.graph.goToBehavior("AddBookToBasket")
61
+ expect(d.graph.currentNode.behavior).toBe(behavior2);
62
+
63
+ // Reset current behavior so transition is valid
64
+ d.graph.setCurrentBehavior("AcceptBookFromUser");
65
+ d.graph.goToBehavior("AnotherBehavior")
66
+ expect(d.graph.currentNode.behavior).toBe(behavior3);
67
+
68
+ // Raises error because current behavior is "AnotherBehavior"
69
+ // and it does not transition to itself.
70
+ expect(() => {
71
+ d.graph.goToBehavior("AnotherBehavior")
72
+ }).toThrow(InvalidTransitionError);
73
+
74
+ // Reset the current behavior and then go to a behavior
75
+ // which is misspelled and expect an invalid transition error
76
+ expect(() => {
77
+ d.graph.setCurrentBehavior("AcceptBookFromUser");
78
+ d.graph.goToBehavior("AddBookToasket")
79
+ }).toThrow(InvalidTransitionError);
80
+ });
81
+
82
+ it("add invariant to participant", () => {
83
+ const d = new DALEngine({name: "Library Manager"});
84
+ const book = d.createParticipant({name: "book"});
85
+ const invariant = d.createInvariant({name: "minLength"});
86
+
87
+ book.addInvariant(invariant);
88
+
89
+ const lastInvariant = book.invariants[book.invariants.length - 1];
90
+ expect(lastInvariant).toBe(invariant);
91
+ });
92
+
93
+ it("serialize to file and deseralize from file", async () => {
94
+ let d = new DALEngine({name: "Library Manager"});
95
+ const book = d.createParticipant({name: "book"});
96
+ const invariant = d.createInvariant(
97
+ {
98
+ "name": "MinLengthConstraint",
99
+ "rule": {
100
+ "type": "minLength",
101
+ "keys": ["value", "name"],
102
+ "value": 1,
103
+ },
104
+ }
105
+ );
106
+ book.addInvariant(invariant);
107
+
108
+ const behavior1 = d.createBehavior({name: "AcceptBookFromUser"});
109
+ behavior1.addParticpant(book);
110
+ const behavior2 = d.createBehavior({name: "AddBookToBasket"});
111
+ const behavior3 = d.createBehavior({name: "AnotherBehavior"});
112
+ d.graph.addNode(behavior1, [behavior2, behavior3]);
113
+ d.graph.addNode(behavior2, []);
114
+ d.graph.addNode(behavior3, []);
115
+
116
+ const filePath = resolve(__dirname, "./temp/inspectSerializeTemp.json")
117
+ await writeFile(filePath, d.serialize())
118
+
119
+ d = new DALEngine({name: "Library Manager"});
120
+ d.deserialize(await readFile(filePath, "utf-8"));
121
+ expect(d.graph.nodes.length).toBe(3);
122
+
123
+ // Intentionally not cleaning up the file because I want to inspect
124
+ // await unlink(filePath)
125
+
126
+ // This is a temporary file I create for my own inspection
127
+ const filePath2 = resolve(__dirname, "./temp/inspectDeseralizeTemp.json")
128
+ await writeFile(filePath2, d.serialize())
129
+ });
130
+ });