@typespec/mutator-framework 0.12.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.
Files changed (221) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/index.d.ts +3 -0
  3. package/dist/src/index.d.ts.map +1 -0
  4. package/dist/src/index.js +4 -0
  5. package/dist/src/index.js.map +1 -0
  6. package/dist/src/mutation/index.d.ts +13 -0
  7. package/dist/src/mutation/index.d.ts.map +1 -0
  8. package/dist/src/mutation/index.js +13 -0
  9. package/dist/src/mutation/index.js.map +1 -0
  10. package/dist/src/mutation/interface.d.ts +11 -0
  11. package/dist/src/mutation/interface.d.ts.map +1 -0
  12. package/dist/src/mutation/interface.js +17 -0
  13. package/dist/src/mutation/interface.js.map +1 -0
  14. package/dist/src/mutation/intrinsic.d.ts +9 -0
  15. package/dist/src/mutation/intrinsic.d.ts.map +1 -0
  16. package/dist/src/mutation/intrinsic.js +11 -0
  17. package/dist/src/mutation/intrinsic.js.map +1 -0
  18. package/dist/src/mutation/literal.d.ts +9 -0
  19. package/dist/src/mutation/literal.d.ts.map +1 -0
  20. package/dist/src/mutation/literal.js +11 -0
  21. package/dist/src/mutation/literal.js.map +1 -0
  22. package/dist/src/mutation/model-property.d.ts +12 -0
  23. package/dist/src/mutation/model-property.d.ts.map +1 -0
  24. package/dist/src/mutation/model-property.js +18 -0
  25. package/dist/src/mutation/model-property.js.map +1 -0
  26. package/dist/src/mutation/model.d.ts +18 -0
  27. package/dist/src/mutation/model.d.ts.map +1 -0
  28. package/dist/src/mutation/model.js +34 -0
  29. package/dist/src/mutation/model.js.map +1 -0
  30. package/dist/src/mutation/mutation-engine.d.ts +80 -0
  31. package/dist/src/mutation/mutation-engine.d.ts.map +1 -0
  32. package/dist/src/mutation/mutation-engine.js +165 -0
  33. package/dist/src/mutation/mutation-engine.js.map +1 -0
  34. package/dist/src/mutation/mutation-engine.test.d.ts +2 -0
  35. package/dist/src/mutation/mutation-engine.test.d.ts.map +1 -0
  36. package/dist/src/mutation/mutation-engine.test.js +145 -0
  37. package/dist/src/mutation/mutation-engine.test.js.map +1 -0
  38. package/dist/src/mutation/mutation.d.ts +45 -0
  39. package/dist/src/mutation/mutation.d.ts.map +1 -0
  40. package/dist/src/mutation/mutation.js +29 -0
  41. package/dist/src/mutation/mutation.js.map +1 -0
  42. package/dist/src/mutation/operation.d.ts +13 -0
  43. package/dist/src/mutation/operation.d.ts.map +1 -0
  44. package/dist/src/mutation/operation.js +20 -0
  45. package/dist/src/mutation/operation.js.map +1 -0
  46. package/dist/src/mutation/scalar.d.ts +11 -0
  47. package/dist/src/mutation/scalar.d.ts.map +1 -0
  48. package/dist/src/mutation/scalar.js +17 -0
  49. package/dist/src/mutation/scalar.js.map +1 -0
  50. package/dist/src/mutation/simple-mutation-engine.d.ts +8 -0
  51. package/dist/src/mutation/simple-mutation-engine.d.ts.map +1 -0
  52. package/dist/src/mutation/simple-mutation-engine.js +11 -0
  53. package/dist/src/mutation/simple-mutation-engine.js.map +1 -0
  54. package/dist/src/mutation/union-variant.d.ts +10 -0
  55. package/dist/src/mutation/union-variant.d.ts.map +1 -0
  56. package/dist/src/mutation/union-variant.js +12 -0
  57. package/dist/src/mutation/union-variant.js.map +1 -0
  58. package/dist/src/mutation/union.d.ts +11 -0
  59. package/dist/src/mutation/union.d.ts.map +1 -0
  60. package/dist/src/mutation/union.js +18 -0
  61. package/dist/src/mutation/union.js.map +1 -0
  62. package/dist/src/mutation-node/enum-member.d.ts +7 -0
  63. package/dist/src/mutation-node/enum-member.d.ts.map +1 -0
  64. package/dist/src/mutation-node/enum-member.js +6 -0
  65. package/dist/src/mutation-node/enum-member.js.map +1 -0
  66. package/dist/src/mutation-node/enum-member.test.d.ts +2 -0
  67. package/dist/src/mutation-node/enum-member.test.d.ts.map +1 -0
  68. package/dist/src/mutation-node/enum-member.test.js +25 -0
  69. package/dist/src/mutation-node/enum-member.test.js.map +1 -0
  70. package/dist/src/mutation-node/enum.d.ts +8 -0
  71. package/dist/src/mutation-node/enum.d.ts.map +1 -0
  72. package/dist/src/mutation-node/enum.js +30 -0
  73. package/dist/src/mutation-node/enum.js.map +1 -0
  74. package/dist/src/mutation-node/enum.test.d.ts +2 -0
  75. package/dist/src/mutation-node/enum.test.d.ts.map +1 -0
  76. package/dist/src/mutation-node/enum.test.js +25 -0
  77. package/dist/src/mutation-node/enum.test.js.map +1 -0
  78. package/dist/src/mutation-node/factory.d.ts +17 -0
  79. package/dist/src/mutation-node/factory.d.ts.map +1 -0
  80. package/dist/src/mutation-node/factory.js +45 -0
  81. package/dist/src/mutation-node/factory.js.map +1 -0
  82. package/dist/src/mutation-node/index.d.ts +15 -0
  83. package/dist/src/mutation-node/index.d.ts.map +1 -0
  84. package/dist/src/mutation-node/index.js +16 -0
  85. package/dist/src/mutation-node/index.js.map +1 -0
  86. package/dist/src/mutation-node/interface.d.ts +8 -0
  87. package/dist/src/mutation-node/interface.d.ts.map +1 -0
  88. package/dist/src/mutation-node/interface.js +30 -0
  89. package/dist/src/mutation-node/interface.js.map +1 -0
  90. package/dist/src/mutation-node/intrinsic.d.ts +7 -0
  91. package/dist/src/mutation-node/intrinsic.d.ts.map +1 -0
  92. package/dist/src/mutation-node/intrinsic.js +6 -0
  93. package/dist/src/mutation-node/intrinsic.js.map +1 -0
  94. package/dist/src/mutation-node/literal.d.ts +7 -0
  95. package/dist/src/mutation-node/literal.d.ts.map +1 -0
  96. package/dist/src/mutation-node/literal.js +6 -0
  97. package/dist/src/mutation-node/literal.js.map +1 -0
  98. package/dist/src/mutation-node/model-property.d.ts +10 -0
  99. package/dist/src/mutation-node/model-property.d.ts.map +1 -0
  100. package/dist/src/mutation-node/model-property.js +48 -0
  101. package/dist/src/mutation-node/model-property.js.map +1 -0
  102. package/dist/src/mutation-node/model-property.test.d.ts +2 -0
  103. package/dist/src/mutation-node/model-property.test.d.ts.map +1 -0
  104. package/dist/src/mutation-node/model-property.test.js +108 -0
  105. package/dist/src/mutation-node/model-property.test.js.map +1 -0
  106. package/dist/src/mutation-node/model.d.ts +10 -0
  107. package/dist/src/mutation-node/model.d.ts.map +1 -0
  108. package/dist/src/mutation-node/model.js +82 -0
  109. package/dist/src/mutation-node/model.js.map +1 -0
  110. package/dist/src/mutation-node/model.test.d.ts +2 -0
  111. package/dist/src/mutation-node/model.test.d.ts.map +1 -0
  112. package/dist/src/mutation-node/model.test.js +133 -0
  113. package/dist/src/mutation-node/model.test.js.map +1 -0
  114. package/dist/src/mutation-node/mutation-edge.d.ts +18 -0
  115. package/dist/src/mutation-node/mutation-edge.d.ts.map +1 -0
  116. package/dist/src/mutation-node/mutation-edge.js +31 -0
  117. package/dist/src/mutation-node/mutation-edge.js.map +1 -0
  118. package/dist/src/mutation-node/mutation-node.d.ts +29 -0
  119. package/dist/src/mutation-node/mutation-node.d.ts.map +1 -0
  120. package/dist/src/mutation-node/mutation-node.js +82 -0
  121. package/dist/src/mutation-node/mutation-node.js.map +1 -0
  122. package/dist/src/mutation-node/mutation-node.test.d.ts +2 -0
  123. package/dist/src/mutation-node/mutation-node.test.d.ts.map +1 -0
  124. package/dist/src/mutation-node/mutation-node.test.js +88 -0
  125. package/dist/src/mutation-node/mutation-node.test.js.map +1 -0
  126. package/dist/src/mutation-node/mutation-subgraph.d.ts +17 -0
  127. package/dist/src/mutation-node/mutation-subgraph.d.ts.map +1 -0
  128. package/dist/src/mutation-node/mutation-subgraph.js +45 -0
  129. package/dist/src/mutation-node/mutation-subgraph.js.map +1 -0
  130. package/dist/src/mutation-node/operation.d.ts +9 -0
  131. package/dist/src/mutation-node/operation.d.ts.map +1 -0
  132. package/dist/src/mutation-node/operation.js +44 -0
  133. package/dist/src/mutation-node/operation.js.map +1 -0
  134. package/dist/src/mutation-node/scalar.d.ts +8 -0
  135. package/dist/src/mutation-node/scalar.d.ts.map +1 -0
  136. package/dist/src/mutation-node/scalar.js +28 -0
  137. package/dist/src/mutation-node/scalar.js.map +1 -0
  138. package/dist/src/mutation-node/scalar.test.d.ts +2 -0
  139. package/dist/src/mutation-node/scalar.test.d.ts.map +1 -0
  140. package/dist/src/mutation-node/scalar.test.js +40 -0
  141. package/dist/src/mutation-node/scalar.test.js.map +1 -0
  142. package/dist/src/mutation-node/tuple.d.ts +9 -0
  143. package/dist/src/mutation-node/tuple.d.ts.map +1 -0
  144. package/dist/src/mutation-node/tuple.js +32 -0
  145. package/dist/src/mutation-node/tuple.js.map +1 -0
  146. package/dist/src/mutation-node/tuple.test.d.ts +2 -0
  147. package/dist/src/mutation-node/tuple.test.d.ts.map +1 -0
  148. package/dist/src/mutation-node/tuple.test.js +29 -0
  149. package/dist/src/mutation-node/tuple.test.js.map +1 -0
  150. package/dist/src/mutation-node/union-variant.d.ts +8 -0
  151. package/dist/src/mutation-node/union-variant.d.ts.map +1 -0
  152. package/dist/src/mutation-node/union-variant.js +23 -0
  153. package/dist/src/mutation-node/union-variant.js.map +1 -0
  154. package/dist/src/mutation-node/union-variant.test.d.ts +2 -0
  155. package/dist/src/mutation-node/union-variant.test.d.ts.map +1 -0
  156. package/dist/src/mutation-node/union-variant.test.js +27 -0
  157. package/dist/src/mutation-node/union-variant.test.js.map +1 -0
  158. package/dist/src/mutation-node/union.d.ts +8 -0
  159. package/dist/src/mutation-node/union.d.ts.map +1 -0
  160. package/dist/src/mutation-node/union.js +30 -0
  161. package/dist/src/mutation-node/union.js.map +1 -0
  162. package/dist/src/mutation-node/union.test.d.ts +2 -0
  163. package/dist/src/mutation-node/union.test.d.ts.map +1 -0
  164. package/dist/src/mutation-node/union.test.js +25 -0
  165. package/dist/src/mutation-node/union.test.js.map +1 -0
  166. package/dist/test/test-host.d.ts +2 -0
  167. package/dist/test/test-host.d.ts.map +1 -0
  168. package/dist/test/test-host.js +6 -0
  169. package/dist/test/test-host.js.map +1 -0
  170. package/dist/test/utils.d.ts +4 -0
  171. package/dist/test/utils.d.ts.map +1 -0
  172. package/dist/test/utils.js +8 -0
  173. package/dist/test/utils.js.map +1 -0
  174. package/package.json +40 -0
  175. package/package.json.bak +42 -0
  176. package/readme.md +339 -0
  177. package/src/index.ts +4 -0
  178. package/src/mutation/index.ts +12 -0
  179. package/src/mutation/interface.ts +38 -0
  180. package/src/mutation/intrinsic.ts +23 -0
  181. package/src/mutation/literal.ts +29 -0
  182. package/src/mutation/model-property.ts +35 -0
  183. package/src/mutation/model.ts +58 -0
  184. package/src/mutation/mutation-engine.test.ts +202 -0
  185. package/src/mutation/mutation-engine.ts +288 -0
  186. package/src/mutation/mutation.ts +90 -0
  187. package/src/mutation/operation.ts +40 -0
  188. package/src/mutation/scalar.ts +36 -0
  189. package/src/mutation/simple-mutation-engine.ts +21 -0
  190. package/src/mutation/union-variant.ts +30 -0
  191. package/src/mutation/union.ts +39 -0
  192. package/src/mutation-node/enum-member.test.ts +26 -0
  193. package/src/mutation-node/enum-member.ts +8 -0
  194. package/src/mutation-node/enum.test.ts +26 -0
  195. package/src/mutation-node/enum.ts +33 -0
  196. package/src/mutation-node/factory.ts +95 -0
  197. package/src/mutation-node/index.ts +16 -0
  198. package/src/mutation-node/interface.ts +33 -0
  199. package/src/mutation-node/intrinsic.ts +8 -0
  200. package/src/mutation-node/literal.ts +10 -0
  201. package/src/mutation-node/model-property.test.ts +136 -0
  202. package/src/mutation-node/model-property.ts +53 -0
  203. package/src/mutation-node/model.test.ts +151 -0
  204. package/src/mutation-node/model.ts +89 -0
  205. package/src/mutation-node/mutation-edge.ts +43 -0
  206. package/src/mutation-node/mutation-node.test.ts +94 -0
  207. package/src/mutation-node/mutation-node.ts +110 -0
  208. package/src/mutation-node/mutation-subgraph.ts +59 -0
  209. package/src/mutation-node/operation.ts +49 -0
  210. package/src/mutation-node/scalar.test.ts +44 -0
  211. package/src/mutation-node/scalar.ts +32 -0
  212. package/src/mutation-node/tuple.test.ts +30 -0
  213. package/src/mutation-node/tuple.ts +35 -0
  214. package/src/mutation-node/union-variant.test.ts +28 -0
  215. package/src/mutation-node/union-variant.ts +26 -0
  216. package/src/mutation-node/union.test.ts +26 -0
  217. package/src/mutation-node/union.ts +33 -0
  218. package/test/test-host.ts +6 -0
  219. package/test/utils.ts +9 -0
  220. package/tsconfig.json +19 -0
  221. package/vitest.config.ts +4 -0
@@ -0,0 +1,89 @@
1
+ import type { Model, ModelProperty, Type } from "@typespec/compiler";
2
+ import { MutationEdge } from "./mutation-edge.js";
3
+ import { MutationNode } from "./mutation-node.js";
4
+
5
+ export class ModelMutationNode extends MutationNode<Model> {
6
+ readonly kind = "Model";
7
+
8
+ traverse() {
9
+ if (this.sourceType.baseModel) {
10
+ const baseNode = this.subgraph.getNode(this.sourceType.baseModel);
11
+ this.connectToBase(baseNode);
12
+ }
13
+
14
+ for (const [propName, prop] of this.sourceType.properties) {
15
+ const propNode = this.subgraph.getNode(prop);
16
+ this.connectProperty(propNode, propName);
17
+ }
18
+
19
+ if (this.sourceType.indexer) {
20
+ const indexerNode = this.subgraph.getNode(this.sourceType.indexer.value);
21
+ this.connectIndexerValue(indexerNode);
22
+ }
23
+ }
24
+
25
+ connectToBase(baseNode: MutationNode<Model>) {
26
+ MutationEdge.create(this, baseNode, {
27
+ onTailMutation: () => {
28
+ this.mutatedType!.baseModel = baseNode.mutatedType;
29
+ },
30
+ onTailDeletion: () => {
31
+ this.mutatedType.baseModel = undefined;
32
+ },
33
+ onTailReplaced: (newTail) => {
34
+ if (newTail.mutatedType.kind !== "Model") {
35
+ throw new Error("Cannot replace base model with non-model type");
36
+ }
37
+ this.mutatedType.baseModel = newTail.mutatedType;
38
+ },
39
+ });
40
+ }
41
+
42
+ connectProperty(propNode: MutationNode<ModelProperty>, sourcePropName: string) {
43
+ MutationEdge.create(this, propNode, {
44
+ onTailMutation: () => {
45
+ this.mutatedType.properties.delete(sourcePropName);
46
+ this.mutatedType.properties.set(propNode.mutatedType.name, propNode.mutatedType);
47
+ },
48
+ onTailDeletion: () => {
49
+ this.mutatedType.properties.delete(sourcePropName);
50
+ },
51
+ onTailReplaced: (newTail) => {
52
+ if (newTail.mutatedType.kind !== "ModelProperty") {
53
+ throw new Error("Cannot replace model property with non-model property type");
54
+ }
55
+ this.mutatedType.properties.delete(sourcePropName);
56
+ this.mutatedType.properties.set(newTail.mutatedType.name, newTail.mutatedType);
57
+ },
58
+ });
59
+ }
60
+
61
+ connectIndexerValue(indexerNode: MutationNode<Type>) {
62
+ MutationEdge.create(this, indexerNode, {
63
+ onTailMutation: () => {
64
+ if (this.mutatedType.indexer) {
65
+ this.mutatedType.indexer = {
66
+ key: this.mutatedType.indexer.key,
67
+ value: indexerNode.mutatedType,
68
+ };
69
+ }
70
+ },
71
+ onTailDeletion: () => {
72
+ if (this.mutatedType.indexer) {
73
+ this.mutatedType.indexer = {
74
+ key: this.mutatedType.indexer.key,
75
+ value: this.$.intrinsic.any,
76
+ };
77
+ }
78
+ },
79
+ onTailReplaced: (newTail) => {
80
+ if (this.mutatedType.indexer) {
81
+ this.mutatedType.indexer = {
82
+ key: this.mutatedType.indexer.key,
83
+ value: newTail.mutatedType,
84
+ };
85
+ }
86
+ },
87
+ });
88
+ }
89
+ }
@@ -0,0 +1,43 @@
1
+ import type { Type } from "@typespec/compiler";
2
+ import { MutationNode } from "./mutation-node.js";
3
+
4
+ export interface MutationEdgeOptions {
5
+ onTailMutation: () => void;
6
+ onTailDeletion: () => void;
7
+ onTailReplaced: (newTail: MutationNode<Type>) => void;
8
+ }
9
+
10
+ export class MutationEdge<THead extends Type, TTail extends Type> {
11
+ public head: MutationNode<THead>;
12
+ public tail: MutationNode<TTail>;
13
+ #options: MutationEdgeOptions;
14
+
15
+ constructor(head: MutationNode<THead>, tail: MutationNode<TTail>, options: MutationEdgeOptions) {
16
+ this.head = head;
17
+ this.tail = tail;
18
+ this.#options = options;
19
+ this.tail.addInEdge(this);
20
+ }
21
+
22
+ static create(head: MutationNode<Type>, tail: MutationNode<Type>, options: MutationEdgeOptions) {
23
+ return new MutationEdge(head, tail, options);
24
+ }
25
+
26
+ tailMutated(): void {
27
+ this.head.mutate();
28
+ this.#options.onTailMutation();
29
+ }
30
+
31
+ tailDeleted() {
32
+ this.head.mutate();
33
+ this.#options.onTailDeletion();
34
+ }
35
+
36
+ tailReplaced(newTail: MutationNode<TTail>) {
37
+ this.head.mutate();
38
+ this.tail.deleteInEdge(this);
39
+ this.tail = newTail;
40
+ this.tail.addInEdge(this);
41
+ this.#options.onTailReplaced(newTail);
42
+ }
43
+ }
@@ -0,0 +1,94 @@
1
+ import { t, type TesterInstance } from "@typespec/compiler/testing";
2
+ import { beforeEach, expect, it } from "vitest";
3
+ import { Tester } from "../../test/test-host.js";
4
+ import { getSubgraph } from "../../test/utils.js";
5
+
6
+ let runner: TesterInstance;
7
+ beforeEach(async () => {
8
+ runner = await Tester.createInstance();
9
+ });
10
+
11
+ it("Subgraph#getNode returns the same node for the same type when called", async () => {
12
+ const { Foo, program } = await runner.compile(t.code`
13
+ model ${t.model("Foo")} {
14
+ prop: string;
15
+ }
16
+ `);
17
+ const subgraph = getSubgraph(program);
18
+ const fooNode1 = subgraph.getNode(Foo);
19
+ const fooNode2 = subgraph.getNode(Foo);
20
+ expect(fooNode1 === fooNode2).toBe(true);
21
+ });
22
+
23
+ it("Creates the same node when constructing the subgraph and coming back to the same type", async () => {
24
+ const { Foo, Bar, Baz, program } = await runner.compile(t.code`
25
+ model ${t.model("Foo")} {
26
+ prop: string;
27
+ }
28
+
29
+ model ${t.model("Bar")} {
30
+ foo: Foo;
31
+ }
32
+
33
+ model ${t.model("Baz")} {
34
+ foo: Foo;
35
+ }
36
+ `);
37
+ const subgraph = getSubgraph(program);
38
+ const fooNode = subgraph.getNode(Foo);
39
+ subgraph.getNode(Bar);
40
+ subgraph.getNode(Baz);
41
+
42
+ expect(fooNode.inEdges.size).toBe(2);
43
+ });
44
+
45
+ it("starts with the mutatedType and sourceType being the same", async () => {
46
+ const { Foo, program } = await runner.compile(t.code`
47
+ model ${t.model("Foo")} {
48
+ prop: string;
49
+ }
50
+ `);
51
+ const subgraph = getSubgraph(program);
52
+ const fooNode = subgraph.getNode(Foo);
53
+ expect(fooNode.isMutated).toBe(false);
54
+ expect(fooNode.sourceType === fooNode.mutatedType).toBe(true);
55
+ });
56
+
57
+ it("clones the source type when mutating and sets isMutated to true", async () => {
58
+ const { Foo, program } = await runner.compile(t.code`
59
+ model ${t.model("Foo")} {
60
+ prop: string;
61
+ }
62
+ `);
63
+ const subgraph = getSubgraph(program);
64
+ const fooNode = subgraph.getNode(Foo);
65
+ expect(fooNode.isMutated).toBe(false);
66
+ expect(fooNode.sourceType === fooNode.mutatedType).toBe(true);
67
+ fooNode.mutate();
68
+ expect(fooNode.isMutated).toBe(true);
69
+ expect(fooNode.sourceType === fooNode.mutatedType).toBe(false);
70
+ expect(fooNode.sourceType.name).toEqual(fooNode.mutatedType.name);
71
+ });
72
+
73
+ it("invokes whenMutated callbacks when mutating", async () => {
74
+ const { Foo, program } = await runner.compile(t.code`
75
+ model ${t.model("Foo")} {
76
+ prop: Bar;
77
+ }
78
+
79
+ model ${t.model("Bar")} {
80
+ prop: string;
81
+ }
82
+ `);
83
+ const subgraph = getSubgraph(program);
84
+ const fooNode = subgraph.getNode(Foo);
85
+ const barNode = subgraph.getNode(Foo);
86
+ let called = false;
87
+ fooNode.whenMutated((mutatedType) => {
88
+ called = true;
89
+ expect(mutatedType).toBe(fooNode.mutatedType);
90
+ });
91
+ expect(called).toBe(false);
92
+ barNode.mutate();
93
+ expect(called).toBe(true);
94
+ });
@@ -0,0 +1,110 @@
1
+ import type { MemberType, Type } from "@typespec/compiler";
2
+ import type { Typekit } from "@typespec/compiler/typekit";
3
+ import { mutationNodeFor, type MutationNodeForType } from "./factory.js";
4
+ import type { MutationEdge } from "./mutation-edge.js";
5
+ import type { MutationSubgraph } from "./mutation-subgraph.js";
6
+
7
+ let nextId = 0;
8
+
9
+ export abstract class MutationNode<T extends Type> {
10
+ abstract readonly kind: string;
11
+
12
+ id = nextId++;
13
+ sourceType: T;
14
+ mutatedType: T;
15
+ isMutated: boolean = false;
16
+ isDeleted: boolean = false;
17
+ isReplaced: boolean = false;
18
+ replacementNode: MutationNodeForType<Type> | null = null;
19
+ inEdges: Set<MutationEdge<Type, Type>> = new Set();
20
+ subgraph: MutationSubgraph;
21
+ referenceType: MemberType | null = null;
22
+ $: Typekit;
23
+
24
+ #whenMutatedCallbacks: ((mutatedType: Type | null) => void)[] = [];
25
+
26
+ constructor(subgraph: MutationSubgraph, sourceNode: T) {
27
+ this.subgraph = subgraph;
28
+ this.$ = this.subgraph.engine.$;
29
+ this.sourceType = sourceNode;
30
+ this.mutatedType = sourceNode;
31
+ }
32
+
33
+ abstract traverse(): void;
34
+
35
+ addInEdge(edge: MutationEdge<Type, Type>) {
36
+ this.inEdges.add(edge);
37
+ }
38
+
39
+ deleteInEdge(edge: MutationEdge<Type, Type>) {
40
+ this.inEdges.delete(edge);
41
+ }
42
+
43
+ whenMutated(cb: (mutatedType: T | null) => void) {
44
+ this.#whenMutatedCallbacks.push(cb as any);
45
+ }
46
+
47
+ mutate(initializeMutation?: (type: T) => void) {
48
+ if (this.isMutated || this.isDeleted || this.isReplaced) {
49
+ return;
50
+ }
51
+
52
+ this.mutatedType = this.$.type.clone(this.sourceType);
53
+ this.isMutated = true;
54
+ initializeMutation?.(this.mutatedType);
55
+ for (const cb of this.#whenMutatedCallbacks) {
56
+ cb(this.mutatedType);
57
+ }
58
+
59
+ for (const edge of this.inEdges) {
60
+ edge.tailMutated();
61
+ }
62
+
63
+ this.$.type.finishType(this.mutatedType);
64
+ }
65
+
66
+ delete() {
67
+ if (this.isMutated || this.isDeleted || this.isReplaced) {
68
+ return;
69
+ }
70
+
71
+ this.isDeleted = true;
72
+
73
+ for (const cb of this.#whenMutatedCallbacks) {
74
+ cb(null);
75
+ }
76
+
77
+ this.mutatedType = this.$.intrinsic.never as T;
78
+
79
+ for (const edge of this.inEdges) {
80
+ edge.tailDeleted();
81
+ }
82
+ }
83
+
84
+ replace(newType: Type) {
85
+ if (this.isMutated || this.isDeleted || this.isReplaced) {
86
+ return this;
87
+ }
88
+
89
+ // We need to make a new node because different types need to handle edge mutations differently.
90
+
91
+ this.isReplaced = true;
92
+ this.replacementNode = mutationNodeFor(this.subgraph, newType);
93
+ this.replacementNode.traverse();
94
+ // we don't need to do the clone stuff with this node, but we mark it as
95
+ // mutated because we don't want to allow further mutations on it.
96
+ this.replacementNode.isMutated = true;
97
+
98
+ if (this.referenceType) {
99
+ this.subgraph.replaceReferenceNode(this.referenceType, this.replacementNode);
100
+ } else {
101
+ this.subgraph.replaceNode(this, this.replacementNode);
102
+ }
103
+
104
+ for (const edge of this.inEdges) {
105
+ edge.tailReplaced(this.replacementNode);
106
+ }
107
+
108
+ return this.replacementNode;
109
+ }
110
+ }
@@ -0,0 +1,59 @@
1
+ import type { MemberType, Type } from "@typespec/compiler";
2
+ import type { MutationEngine } from "../mutation/mutation-engine.js";
3
+ import { mutationNodeFor, type MutationNodeForType } from "./factory.js";
4
+ import type { MutationNode } from "./mutation-node.js";
5
+
6
+ /**
7
+ * A subgraph of mutation nodes such that there is one node per type in the graph.
8
+ */
9
+ export class MutationSubgraph {
10
+ #seenNodes = new Map<Type, MutationNode<Type>>();
11
+ #seenReferenceNodes = new Map<MemberType, MutationNode<Type>>();
12
+
13
+ engine: MutationEngine<any>;
14
+
15
+ constructor(engine: MutationEngine<any>) {
16
+ this.engine = engine;
17
+ }
18
+
19
+ getNode<T extends Type>(type: T, memberReferences: MemberType[] = []): MutationNodeForType<T> {
20
+ if (memberReferences.length > 0) {
21
+ return this.getReferenceNode(memberReferences[0]!) as any;
22
+ }
23
+
24
+ if (this.#seenNodes.has(type)) {
25
+ return this.#seenNodes.get(type)! as MutationNodeForType<T>;
26
+ }
27
+
28
+ const node = mutationNodeFor(this, type);
29
+ this.#seenNodes.set(type, node);
30
+ node.traverse();
31
+
32
+ return node;
33
+ }
34
+
35
+ getReferenceNode(memberType: MemberType): MutationNode<Type> {
36
+ if (this.#seenReferenceNodes.has(memberType)) {
37
+ return this.#seenReferenceNodes.get(memberType)!;
38
+ }
39
+
40
+ let referencedType: Type = memberType;
41
+ while (referencedType.kind === "ModelProperty" || referencedType.kind === "UnionVariant") {
42
+ referencedType = referencedType.type;
43
+ }
44
+ const node = mutationNodeFor(this, referencedType);
45
+ node.referenceType = memberType;
46
+ this.#seenReferenceNodes.set(memberType, node);
47
+ node.traverse();
48
+
49
+ return node;
50
+ }
51
+
52
+ replaceNode(oldNode: MutationNode<Type>, newNode: MutationNode<Type>) {
53
+ this.#seenNodes.set(oldNode.sourceType, newNode);
54
+ }
55
+
56
+ replaceReferenceNode(referenceType: MemberType, newNode: MutationNode<Type>) {
57
+ this.#seenReferenceNodes.set(referenceType, newNode);
58
+ }
59
+ }
@@ -0,0 +1,49 @@
1
+ import type { Model, Operation, Type } from "@typespec/compiler";
2
+ import { MutationEdge } from "./mutation-edge.js";
3
+ import { MutationNode } from "./mutation-node.js";
4
+
5
+ export class OperationMutationNode extends MutationNode<Operation> {
6
+ readonly kind = "Operation";
7
+
8
+ traverse() {
9
+ const parameterNode = this.subgraph.getNode(this.sourceType.parameters);
10
+ this.connectParameters(parameterNode);
11
+
12
+ const returnTypeNode = this.subgraph.getNode(this.sourceType.returnType);
13
+ this.connectReturnType(returnTypeNode);
14
+ }
15
+
16
+ connectParameters(baseNode: MutationNode<Model>) {
17
+ MutationEdge.create(this, baseNode, {
18
+ onTailMutation: () => {
19
+ this.mutatedType!.parameters = baseNode.mutatedType;
20
+ },
21
+ onTailDeletion: () => {
22
+ this.mutatedType.parameters = this.$.model.create({
23
+ name: "",
24
+ properties: {},
25
+ });
26
+ },
27
+ onTailReplaced: (newTail) => {
28
+ if (newTail.mutatedType.kind !== "Model") {
29
+ throw new Error("Cannot replace parameters with non-model type");
30
+ }
31
+ this.mutatedType.parameters = newTail.mutatedType;
32
+ },
33
+ });
34
+ }
35
+
36
+ connectReturnType(typeNode: MutationNode<Type>) {
37
+ MutationEdge.create(this, typeNode, {
38
+ onTailMutation: () => {
39
+ this.mutatedType!.returnType = typeNode.mutatedType;
40
+ },
41
+ onTailDeletion: () => {
42
+ this.mutatedType.returnType = this.$.intrinsic.void;
43
+ },
44
+ onTailReplaced: (newTail) => {
45
+ this.mutatedType.returnType = newTail.mutatedType;
46
+ },
47
+ });
48
+ }
49
+ }
@@ -0,0 +1,44 @@
1
+ import { t, type TesterInstance } from "@typespec/compiler/testing";
2
+ import { $ } from "@typespec/compiler/typekit";
3
+ import { beforeEach, expect, it } from "vitest";
4
+ import { Tester } from "../../test/test-host.js";
5
+ import { getSubgraph } from "../../test/utils.js";
6
+ let runner: TesterInstance;
7
+ beforeEach(async () => {
8
+ runner = await Tester.createInstance();
9
+ });
10
+ it("handles mutation of base scalars", async () => {
11
+ const { program, Base, Derived } = await runner.compile(t.code`
12
+ scalar ${t.scalar("Base")};
13
+ scalar ${t.scalar("Derived")} extends Base;
14
+ `);
15
+ const subgraph = getSubgraph(program);
16
+ const baseNode = subgraph.getNode(Base);
17
+ const derivedNode = subgraph.getNode(Derived);
18
+
19
+ baseNode.mutate();
20
+ expect(baseNode.isMutated).toBe(true);
21
+ expect(derivedNode.isMutated).toBe(true);
22
+ expect(derivedNode.mutatedType.baseScalar === baseNode.mutatedType).toBeTruthy();
23
+ });
24
+
25
+ it("handles replacement of scalars", async () => {
26
+ const { program, Base, Derived } = await runner.compile(t.code`
27
+ scalar ${t.scalar("Base")};
28
+ scalar ${t.scalar("Derived")} extends Base;
29
+ `);
30
+ const subgraph = getSubgraph(program);
31
+ const baseNode = subgraph.getNode(Base);
32
+ const derivedNode = subgraph.getNode(Derived);
33
+
34
+ const replacedNode = baseNode.replace($(program).builtin.string);
35
+ expect(replacedNode.isMutated).toBe(true);
36
+ expect(baseNode.isReplaced).toBe(true);
37
+
38
+ // subgraph is updated
39
+ expect(replacedNode === subgraph.getNode(Base)).toBe(true);
40
+
41
+ // derived node is updated
42
+ expect(derivedNode.isMutated).toBe(true);
43
+ expect(derivedNode.mutatedType.baseScalar === replacedNode.sourceType).toBe(true);
44
+ });
@@ -0,0 +1,32 @@
1
+ import type { Scalar } from "@typespec/compiler";
2
+ import { MutationEdge } from "./mutation-edge.js";
3
+ import { MutationNode } from "./mutation-node.js";
4
+
5
+ export class ScalarMutationNode extends MutationNode<Scalar> {
6
+ readonly kind = "Scalar";
7
+
8
+ traverse() {
9
+ if (this.sourceType.baseScalar) {
10
+ const baseScalarNode = this.subgraph.getNode(this.sourceType.baseScalar);
11
+ this.connectBaseScalar(baseScalarNode);
12
+ }
13
+ }
14
+
15
+ connectBaseScalar(baseScalar: MutationNode<Scalar>) {
16
+ MutationEdge.create(this, baseScalar, {
17
+ onTailReplaced: (newTail) => {
18
+ if (!this.$.scalar.is(newTail.mutatedType)) {
19
+ throw new Error("Cannot replace base scalar with non-scalar type");
20
+ }
21
+
22
+ this.mutatedType.baseScalar = newTail.mutatedType;
23
+ },
24
+ onTailMutation: () => {
25
+ this.mutatedType.baseScalar = baseScalar.mutatedType;
26
+ },
27
+ onTailDeletion: () => {
28
+ this.mutatedType.baseScalar = undefined;
29
+ },
30
+ });
31
+ }
32
+ }
@@ -0,0 +1,30 @@
1
+ import type { Tuple } from "@typespec/compiler";
2
+ import { t, type TesterInstance } from "@typespec/compiler/testing";
3
+ import { $ } from "@typespec/compiler/typekit";
4
+ import { beforeEach, expect, it } from "vitest";
5
+ import { Tester } from "../../test/test-host.js";
6
+ import { getSubgraph } from "../../test/utils.js";
7
+ let runner: TesterInstance;
8
+ beforeEach(async () => {
9
+ runner = await Tester.createInstance();
10
+ });
11
+
12
+ it("handles mutation of element types", async () => {
13
+ const { program, Foo, prop, Bar } = await runner.compile(t.code`
14
+ model ${t.model("Foo")} {
15
+ ${t.modelProperty("prop")}: [Bar, string];
16
+ }
17
+ model ${t.model("Bar")} {}
18
+
19
+ `);
20
+ const subgraph = getSubgraph(program);
21
+ const fooNode = subgraph.getNode(Foo);
22
+ const propNode = subgraph.getNode(prop);
23
+ const barNode = subgraph.getNode(Bar);
24
+ barNode.mutate();
25
+ expect(barNode.isMutated).toBe(true);
26
+ expect(propNode.isMutated).toBe(true);
27
+ expect(fooNode.isMutated).toBe(true);
28
+ expect((propNode.mutatedType.type as Tuple).values[0] === barNode.mutatedType).toBeTruthy();
29
+ expect((propNode.mutatedType.type as Tuple).values[1] === $(program).builtin.string).toBeTruthy();
30
+ });
@@ -0,0 +1,35 @@
1
+ import type { Tuple, Type } from "@typespec/compiler";
2
+ import { MutationEdge } from "./mutation-edge.js";
3
+ import { MutationNode } from "./mutation-node.js";
4
+
5
+ export class TupleMutationNode extends MutationNode<Tuple> {
6
+ readonly kind = "Tuple";
7
+ #indexMap: number[] = [];
8
+
9
+ traverse() {
10
+ for (let i = 0; i < this.sourceType.values.length; i++) {
11
+ const elemType = this.sourceType.values[i];
12
+ const elemNode = this.subgraph.getNode(elemType);
13
+ this.#indexMap[i] = i;
14
+ this.connectElement(elemNode, i);
15
+ }
16
+ }
17
+
18
+ connectElement(elemNode: MutationNode<Type>, index: number) {
19
+ MutationEdge.create(this, elemNode, {
20
+ onTailMutation: () => {
21
+ this.mutatedType.values[this.#indexMap[index]] = elemNode.mutatedType;
22
+ },
23
+ onTailDeletion: () => {
24
+ const spliceIndex = this.#indexMap[index];
25
+ this.mutatedType.values.splice(spliceIndex, 1);
26
+ for (let i = spliceIndex + 1; i < this.#indexMap.length; i++) {
27
+ this.#indexMap[i]--;
28
+ }
29
+ },
30
+ onTailReplaced: (newTail) => {
31
+ this.mutatedType.values[this.#indexMap[index]] = newTail.mutatedType;
32
+ },
33
+ });
34
+ }
35
+ }
@@ -0,0 +1,28 @@
1
+ import { t, type TesterInstance } from "@typespec/compiler/testing";
2
+ import { $ } from "@typespec/compiler/typekit";
3
+ import { beforeEach, expect, it } from "vitest";
4
+ import { Tester } from "../../test/test-host.js";
5
+ import { getSubgraph } from "../../test/utils.js";
6
+ let runner: TesterInstance;
7
+ beforeEach(async () => {
8
+ runner = await Tester.createInstance();
9
+ });
10
+
11
+ it("handles mutation of variant types", async () => {
12
+ const { program, Foo, v1 } = await runner.compile(t.code`
13
+ union ${t.union("Foo")} {
14
+ ${t.unionVariant("v1")}: string;
15
+ v2: int32;
16
+ }
17
+ `);
18
+
19
+ const subgraph = getSubgraph(program);
20
+ const fooNode = subgraph.getNode(Foo);
21
+ const v1Node = subgraph.getNode(v1);
22
+ const stringNode = subgraph.getNode($(program).builtin.string);
23
+ stringNode.mutate();
24
+ expect(stringNode.isMutated).toBe(true);
25
+ expect(v1Node.isMutated).toBe(true);
26
+ expect(fooNode.isMutated).toBe(true);
27
+ expect(v1Node.mutatedType.type === stringNode.mutatedType).toBeTruthy();
28
+ });
@@ -0,0 +1,26 @@
1
+ import type { Type, UnionVariant } from "@typespec/compiler";
2
+ import { MutationEdge } from "./mutation-edge.js";
3
+ import { MutationNode } from "./mutation-node.js";
4
+
5
+ export class UnionVariantMutationNode extends MutationNode<UnionVariant> {
6
+ readonly kind = "UnionVariant";
7
+
8
+ traverse() {
9
+ const typeNode = this.subgraph.getNode(this.sourceType.type);
10
+ this.connectType(typeNode);
11
+ }
12
+
13
+ connectType(typeNode: MutationNode<Type>) {
14
+ MutationEdge.create(this, typeNode, {
15
+ onTailMutation: () => {
16
+ this.mutatedType.type = typeNode.mutatedType;
17
+ },
18
+ onTailDeletion: () => {
19
+ this.mutatedType.type = this.$.intrinsic.any;
20
+ },
21
+ onTailReplaced: (newTail) => {
22
+ this.mutatedType.type = newTail.mutatedType;
23
+ },
24
+ });
25
+ }
26
+ }
@@ -0,0 +1,26 @@
1
+ import { t, type TesterInstance } from "@typespec/compiler/testing";
2
+ import { beforeEach, expect, it } from "vitest";
3
+ import { Tester } from "../../test/test-host.js";
4
+ import { getSubgraph } from "../../test/utils.js";
5
+ let runner: TesterInstance;
6
+ beforeEach(async () => {
7
+ runner = await Tester.createInstance();
8
+ });
9
+
10
+ it("handles mutation of variants", async () => {
11
+ const { program, Foo, v1 } = await runner.compile(t.code`
12
+ union ${t.union("Foo")} {
13
+ ${t.unionVariant("v1")}: string;
14
+ v2: int32;
15
+ }
16
+ `);
17
+
18
+ const subgraph = getSubgraph(program);
19
+ const fooNode = subgraph.getNode(Foo);
20
+ const v1Node = subgraph.getNode(v1);
21
+ v1Node.mutate((clone) => (clone.name = "v1Renamed"));
22
+ expect(v1Node.isMutated).toBe(true);
23
+ expect(fooNode.isMutated).toBe(true);
24
+ expect(fooNode.mutatedType.variants.get("v1") === undefined).toBeTruthy();
25
+ expect(fooNode.mutatedType.variants.get("v1Renamed") === v1Node.mutatedType).toBeTruthy();
26
+ });