Skip to content

Commit a941cb1

Browse files
authored
Add EnumMutation and EnumMemberMutation to the Mutator Framework (#9133)
### Summary Completes enum support in the mutator framework by adding the missing high-level mutation classes. The low-level `EnumMutationNode` and `EnumMemberMutationNode` were added in #8822, but the corresponding `EnumMutation` and `EnumMemberMutation` classes were not implemented. ### Changes - Add `EnumMutation` class for customizing enum mutations - Add `EnumMemberMutation` class for customizing enum member mutations - Register both classes in `MutationEngine` and `DefaultMutationClasses` - Add exports to `mutation/index.ts` - Add integration test demonstrating custom enum mutations ### Testing - Added test for custom `EnumMutation` that renames enums and their members - Expanded existing `mutation-node/enum.test.ts` with tests for: - Basic member mutation - Member deletion
1 parent f1fa49a commit a941cb1

File tree

10 files changed

+186
-7
lines changed

10 files changed

+186
-7
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
3+
changeKind: feature
4+
packages:
5+
- "@typespec/mutator-framework"
6+
---
7+
8+
Add `EnumMutation` and `EnumMemberMutation` to the Mutator Framework
Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# Changelog - @typespec/mutator-framework
22

3-
4-
53
## 0.13.0
64

75
### Features
@@ -11,4 +9,3 @@
119
### Bump dependencies
1210

1311
- [#8823](https://github.com/microsoft/typespec/pull/8823) Upgrade dependencies
14-

packages/mutator-framework/src/mutation-node/enum-member.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { t, type TesterInstance } from "@typespec/compiler/testing";
22
import { beforeEach, expect, it } from "vitest";
33
import { Tester } from "../../test/test-host.js";
44
import { getSubgraph } from "../../test/utils.js";
5+
56
let runner: TesterInstance;
67
beforeEach(async () => {
78
runner = await Tester.createInstance();

packages/mutator-framework/src/mutation-node/enum.test.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { t, type TesterInstance } from "@typespec/compiler/testing";
22
import { beforeEach, expect, it } from "vitest";
33
import { Tester } from "../../test/test-host.js";
44
import { getSubgraph } from "../../test/utils.js";
5+
56
let runner: TesterInstance;
67
beforeEach(async () => {
78
runner = await Tester.createInstance();
@@ -15,12 +16,47 @@ it("handles mutation of members", async () => {
1516
}
1617
`);
1718

19+
const subgraph = getSubgraph(program);
20+
const fooNode = subgraph.getNode(Foo);
21+
const aNode = subgraph.getNode(a);
22+
aNode.mutate();
23+
expect(aNode.isMutated).toBe(true);
24+
expect(fooNode.isMutated).toBe(true);
25+
expect(fooNode.mutatedType.members.get("a") === aNode.mutatedType).toBe(true);
26+
});
27+
28+
it("handles mutation of members with name change", async () => {
29+
const { program, Foo, a } = await runner.compile(t.code`
30+
enum ${t.enum("Foo")} {
31+
${t.enumMember("a")};
32+
b;
33+
}
34+
`);
35+
1836
const subgraph = getSubgraph(program);
1937
const fooNode = subgraph.getNode(Foo);
2038
const aNode = subgraph.getNode(a);
2139
aNode.mutate((clone) => (clone.name = "aRenamed"));
2240
expect(aNode.isMutated).toBe(true);
2341
expect(fooNode.isMutated).toBe(true);
24-
expect(fooNode.mutatedType.members.get("a") === undefined).toBeTruthy();
25-
expect(fooNode.mutatedType.members.get("aRenamed") === aNode.mutatedType).toBeTruthy();
42+
expect(fooNode.mutatedType.members.get("a")).toBeUndefined();
43+
expect(fooNode.mutatedType.members.get("aRenamed") === aNode.mutatedType).toBe(true);
44+
});
45+
46+
it("handles deletion of members", async () => {
47+
const { program, Foo, a } = await runner.compile(t.code`
48+
enum ${t.enum("Foo")} {
49+
${t.enumMember("a")};
50+
b;
51+
}
52+
`);
53+
54+
const subgraph = getSubgraph(program);
55+
const fooNode = subgraph.getNode(Foo);
56+
const aNode = subgraph.getNode(a);
57+
aNode.delete();
58+
expect(aNode.isDeleted).toBe(true);
59+
expect(fooNode.isMutated).toBe(true);
60+
expect(fooNode.mutatedType.members.get("a")).toBeUndefined();
61+
expect(fooNode.mutatedType.members.size).toBe(1);
2662
});

packages/mutator-framework/src/mutation-node/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./mutation-node.js";
22

3+
export * from "./enum-member.js";
34
export * from "./enum.js";
45
export * from "./interface.js";
56
export * from "./intrinsic.js";
@@ -13,4 +14,3 @@ export * from "./scalar.js";
1314
export * from "./tuple.js";
1415
export * from "./union-variant.js";
1516
export * from "./union.js";
16-
//export * from "./enum-member.js";
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { EnumMember, MemberType } from "@typespec/compiler";
2+
import type { CustomMutationClasses, MutationEngine, MutationOptions } from "./mutation-engine.js";
3+
import { Mutation } from "./mutation.js";
4+
5+
export class EnumMemberMutation<
6+
TOptions extends MutationOptions,
7+
TCustomMutations extends CustomMutationClasses,
8+
TEngine extends MutationEngine<TCustomMutations> = MutationEngine<TCustomMutations>,
9+
> extends Mutation<EnumMember, TCustomMutations, TOptions, TEngine> {
10+
readonly kind = "EnumMember";
11+
12+
constructor(
13+
engine: TEngine,
14+
sourceType: EnumMember,
15+
referenceTypes: MemberType[] = [],
16+
options: TOptions,
17+
) {
18+
super(engine, sourceType, referenceTypes, options);
19+
}
20+
21+
mutate() {
22+
// EnumMember is a leaf type with no children to mutate
23+
}
24+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Enum, MemberType } from "@typespec/compiler";
2+
import type {
3+
CustomMutationClasses,
4+
MutationEngine,
5+
MutationFor,
6+
MutationOptions,
7+
} from "./mutation-engine.js";
8+
import { Mutation } from "./mutation.js";
9+
10+
export class EnumMutation<
11+
TOptions extends MutationOptions,
12+
TCustomMutations extends CustomMutationClasses,
13+
TEngine extends MutationEngine<TCustomMutations> = MutationEngine<TCustomMutations>,
14+
> extends Mutation<Enum, TCustomMutations, TOptions, TEngine> {
15+
readonly kind = "Enum";
16+
members: Map<string, MutationFor<TCustomMutations, "EnumMember">> = new Map();
17+
18+
constructor(
19+
engine: TEngine,
20+
sourceType: Enum,
21+
referenceTypes: MemberType[] = [],
22+
options: TOptions,
23+
) {
24+
super(engine, sourceType, referenceTypes, options);
25+
}
26+
27+
protected mutateMembers() {
28+
for (const member of this.sourceType.members.values()) {
29+
this.members.set(member.name, this.engine.mutate(member, this.options));
30+
}
31+
}
32+
33+
mutate() {
34+
this.mutateMembers();
35+
}
36+
}

packages/mutator-framework/src/mutation/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export * from "./enum-member.js";
2+
export * from "./enum.js";
13
export * from "./interface.js";
24
export * from "./intrinsic.js";
35
export * from "./literal.js";

packages/mutator-framework/src/mutation/mutation-engine.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import type { Model } from "@typespec/compiler";
1+
import type { Enum, Model } from "@typespec/compiler";
22
import { t } from "@typespec/compiler/testing";
33
import { $, type Typekit } from "@typespec/compiler/typekit";
44
import { expect, it } from "vitest";
55
import { Tester } from "../../test/test-host.js";
66
import type { MutationSubgraph } from "../mutation-node/mutation-subgraph.js";
7+
import { EnumMemberMutation } from "./enum-member.js";
8+
import { EnumMutation } from "./enum.js";
79
import { ModelPropertyMutation } from "./model-property.js";
810
import { ModelMutation } from "./model.js";
911
import { MutationEngine, MutationOptions } from "./mutation-engine.js";
@@ -200,3 +202,70 @@ it("mutates model properties into unions", async () => {
200202
const mutatedFoo = fooMutation.unionified;
201203
expect(tk.union.is(mutatedFoo.properties.get("prop")!.type)).toBe(true);
202204
});
205+
206+
interface RenameEnumMutations {
207+
Enum: RenameEnumMutation;
208+
EnumMember: RenameEnumMemberMutation;
209+
}
210+
211+
class RenameEnumMutation extends EnumMutation<
212+
RenameMutationOptions,
213+
RenameEnumMutations,
214+
MutationEngine<RenameEnumMutations>
215+
> {
216+
get withPrefix() {
217+
return this.getMutatedType();
218+
}
219+
220+
mutate() {
221+
this.mutateType((e) => (e.name = `${this.options.prefix}${this.sourceType.name}`));
222+
super.mutate();
223+
}
224+
}
225+
226+
class RenameEnumMemberMutation extends EnumMemberMutation<
227+
RenameMutationOptions,
228+
RenameEnumMutations,
229+
MutationEngine<RenameEnumMutations>
230+
> {
231+
get withPrefix() {
232+
return this.getMutatedType();
233+
}
234+
235+
mutate() {
236+
this.mutateType((m) => (m.name = `${this.options.prefix}${this.sourceType.name}`));
237+
super.mutate();
238+
}
239+
}
240+
241+
it("mutates enums and enum members", async () => {
242+
const runner = await Tester.createInstance();
243+
const { Status, program } = await runner.compile(t.code`
244+
enum ${t.enum("Status")} {
245+
Active,
246+
Inactive,
247+
}
248+
`);
249+
250+
const tk = $(program);
251+
const engine = new SimpleMutationEngine(tk, {
252+
Enum: RenameEnumMutation,
253+
EnumMember: RenameEnumMemberMutation,
254+
});
255+
256+
const options = new RenameMutationOptions("Pre", "Suf");
257+
const enumMutation = engine.mutate(Status, options) as RenameEnumMutation;
258+
259+
// Verify the enum and its members are mutated
260+
expect(enumMutation.withPrefix.name).toBe("PreStatus");
261+
262+
const activeMutation = enumMutation.members.get("Active") as RenameEnumMemberMutation;
263+
expect(activeMutation.withPrefix.name).toBe("PreActive");
264+
265+
// Verify the mutated enum has the renamed members
266+
const mutatedEnum = enumMutation.withPrefix as Enum;
267+
expect(mutatedEnum.members.has("PreActive")).toBe(true);
268+
expect(mutatedEnum.members.has("PreInactive")).toBe(true);
269+
expect(mutatedEnum.members.has("Active")).toBe(false);
270+
expect(mutatedEnum.members.has("Inactive")).toBe(false);
271+
});

packages/mutator-framework/src/mutation/mutation-engine.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { MemberType, Type } from "@typespec/compiler";
22
import type { Typekit } from "@typespec/compiler/typekit";
33
import type { MutationNodeForType } from "../mutation-node/factory.js";
44
import { MutationSubgraph } from "../mutation-node/mutation-subgraph.js";
5+
import { EnumMemberMutation } from "./enum-member.js";
6+
import { EnumMutation } from "./enum.js";
57
import { InterfaceMutation } from "./interface.js";
68
import { IntrinsicMutation } from "./intrinsic.js";
79
import { LiteralMutation } from "./literal.js";
@@ -24,6 +26,8 @@ export interface DefaultMutationClasses<TCustomMutations extends CustomMutationC
2426
ModelProperty: ModelPropertyMutation<MutationOptions, TCustomMutations>;
2527
Union: UnionMutation<MutationOptions, TCustomMutations>;
2628
UnionVariant: UnionVariantMutation<MutationOptions, TCustomMutations>;
29+
Enum: EnumMutation<MutationOptions, TCustomMutations>;
30+
EnumMember: EnumMemberMutation<MutationOptions, TCustomMutations>;
2731
String: LiteralMutation<MutationOptions, TCustomMutations>;
2832
Number: LiteralMutation<MutationOptions, TCustomMutations>;
2933
Boolean: LiteralMutation<MutationOptions, TCustomMutations>;
@@ -72,6 +76,8 @@ export class MutationEngine<TCustomMutations extends CustomMutationClasses> {
7276
ModelProperty: mutatorClasses.ModelProperty ?? ModelPropertyMutation,
7377
Union: mutatorClasses.Union ?? UnionMutation,
7478
UnionVariant: mutatorClasses.UnionVariant ?? UnionVariantMutation,
79+
Enum: mutatorClasses.Enum ?? EnumMutation,
80+
EnumMember: mutatorClasses.EnumMember ?? EnumMemberMutation,
7581
String: mutatorClasses.String ?? LiteralMutation,
7682
Number: mutatorClasses.Number ?? LiteralMutation,
7783
Boolean: mutatorClasses.Boolean ?? LiteralMutation,

0 commit comments

Comments
 (0)