Skip to content

Commit afd8945

Browse files
authored
Propagate Namespace mutation to its types (#4937)
- Expose mutateSubgraphWithNamespace which allows Namespace mutations - mutateSubgraph doesn't allow Namespace mutators. We want to keep namespace mutator hidden as it is a more advanced use case.
1 parent 7aa6b68 commit afd8945

File tree

5 files changed

+88
-16
lines changed

5 files changed

+88
-16
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@typespec/compiler"
5+
---
6+
7+
Add mutateSubgraphWithNamespace as a separate API

packages/compiler/src/experimental/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ export {
66
MutatorFn as unsafe_MutatorFn,
77
MutatorRecord as unsafe_MutatorRecord,
88
MutatorReplaceFn as unsafe_MutatorReplaceFn,
9+
MutatorWithNamespace as unsafe_MutatorWithNamespace,
910
mutateSubgraph as unsafe_mutateSubgraph,
11+
mutateSubgraphWithNamespace as unsafe_mutateSubgraphWithNamespace,
1012
} from "./mutators.js";
1113
export { Realm as unsafe_Realm } from "./realm.js";
1214
export { unsafe_useStateMap, unsafe_useStateSet } from "./state-accessor.js";

packages/compiler/src/experimental/mutators.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,15 @@ export interface Mutator {
7373
ScalarConstructor?: MutatorRecord<ScalarConstructor>;
7474
StringTemplate?: MutatorRecord<StringTemplate>;
7575
StringTemplateSpan?: MutatorRecord<StringTemplateSpan>;
76-
Namespace?: MutatorRecord<Namespace>;
7776
}
7877

78+
/**
79+
* @experimental - This is a type that extends Mutator with a Namespace property.
80+
*/
81+
export type MutatorWithNamespace = Mutator & {
82+
Namespace: MutatorRecord<Namespace>;
83+
};
84+
7985
/** @experimental */
8086
export enum MutatorFlow {
8187
MutateAndRecurse = 0,
@@ -93,7 +99,10 @@ export type MutableType = Exclude<
9399
| FunctionParameter
94100
| ObjectType
95101
| Projection
102+
| Namespace
96103
>;
104+
/** @experimental */
105+
export type MutableTypeWithNamespace = MutableType | Namespace;
97106
const typeId = CustomKeyMap.objectKeyer();
98107
const mutatorId = CustomKeyMap.objectKeyer();
99108
const seen = new CustomKeyMap<[MutableType, Set<Mutator> | Mutator[]], Type>(([type, mutators]) => {
@@ -103,6 +112,21 @@ const seen = new CustomKeyMap<[MutableType, Set<Mutator> | Mutator[]], Type>(([t
103112
return key;
104113
});
105114

115+
/**
116+
* Mutate the type graph with some namespace mutation.
117+
* **Warning** this will most likely end up mutating the entire TypeGraph
118+
* as every type relate to namespace in some way or another
119+
* causing parent navigation which in turn would mutate everything in that namespace.
120+
* @experimental
121+
*/
122+
export function mutateSubgraphWithNamespace<T extends MutableTypeWithNamespace>(
123+
program: Program,
124+
mutators: MutatorWithNamespace[],
125+
type: T,
126+
): { realm: Realm | null; type: MutableTypeWithNamespace } {
127+
return mutateSubgraph(program, mutators, type as any);
128+
}
129+
106130
/** @experimental */
107131
export function mutateSubgraph<T extends MutableType>(
108132
program: Program,

packages/compiler/src/experimental/typekit/kits/type.ts

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { Enum, Model, Type } from "../../../core/types.js";
2-
import { defineKit } from "../define-kit.js";
1+
import { type Namespace, type Type } from "../../../core/types.js";
2+
import { $, defineKit } from "../define-kit.js";
33
import { copyMap } from "../utils.js";
44

55
/** @experimental */
@@ -70,20 +70,33 @@ defineKit<BaseTypeKit>({
7070
clone = this.program.checker.createType({
7171
...type,
7272
decorators: [...type.decorators],
73-
decoratorDeclarations: new Map(type.decoratorDeclarations),
74-
models: new Map<string, Model>(type.models),
75-
enums: new Map<string, Enum>(type.enums),
76-
functionDeclarations: new Map(type.functionDeclarations),
7773
instantiationParameters: type.instantiationParameters
7874
? [...type.instantiationParameters]
7975
: undefined,
80-
interfaces: new Map(type.interfaces),
81-
namespaces: new Map(type.namespaces),
82-
operations: new Map(type.operations),
8376
projections: [...type.projections],
84-
scalars: new Map(type.scalars),
85-
unions: new Map(type.unions),
8677
});
78+
const clonedNamespace = clone as Namespace;
79+
clonedNamespace.decoratorDeclarations = cloneTypeCollection(type.decoratorDeclarations, {
80+
namespace: clonedNamespace,
81+
});
82+
clonedNamespace.models = cloneTypeCollection(type.models, { namespace: clonedNamespace });
83+
clonedNamespace.enums = cloneTypeCollection(type.enums, { namespace: clonedNamespace });
84+
clonedNamespace.functionDeclarations = cloneTypeCollection(type.functionDeclarations, {
85+
namespace: clonedNamespace,
86+
});
87+
clonedNamespace.interfaces = cloneTypeCollection(type.interfaces, {
88+
namespace: clonedNamespace,
89+
});
90+
clonedNamespace.namespaces = cloneTypeCollection(type.namespaces, {
91+
namespace: clonedNamespace,
92+
});
93+
clonedNamespace.operations = cloneTypeCollection(type.operations, {
94+
namespace: clonedNamespace,
95+
});
96+
clonedNamespace.scalars = cloneTypeCollection(type.scalars, {
97+
namespace: clonedNamespace,
98+
});
99+
clonedNamespace.unions = cloneTypeCollection(type.unions, { namespace: clonedNamespace });
87100
break;
88101
default:
89102
clone = this.program.checker.createType({
@@ -97,3 +110,18 @@ defineKit<BaseTypeKit>({
97110
},
98111
},
99112
});
113+
114+
function cloneTypeCollection<T extends Type>(
115+
collection: Map<string, T>,
116+
options: { namespace?: Namespace } = {},
117+
): Map<string, T> {
118+
const cloneCollection = new Map<string, T>();
119+
for (const [key, type] of collection) {
120+
const clone = $.type.clone(type);
121+
if ("namespace" in clone && options.namespace) {
122+
clone.namespace = options.namespace;
123+
}
124+
cloneCollection.set(key, clone);
125+
}
126+
return cloneCollection;
127+
}

packages/compiler/test/experimental/mutator.test.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { beforeEach, expect, it } from "vitest";
2-
import { mutateSubgraph, Mutator, MutatorFlow } from "../../src/experimental/mutators.js";
2+
import {
3+
mutateSubgraph,
4+
mutateSubgraphWithNamespace,
5+
Mutator,
6+
MutatorFlow,
7+
MutatorWithNamespace,
8+
} from "../../src/experimental/mutators.js";
39
import { Model, Namespace } from "../../src/index.js";
410
import { createTestHost } from "../../src/testing/test-host.js";
511
import { createTestWrapper } from "../../src/testing/test-utils.js";
@@ -85,23 +91,28 @@ it("removes model reference from namespace", async () => {
8591
`;
8692

8793
const { Foo } = (await runner.compile(code)) as { Foo: Namespace; Bar: Model; Baz: Model };
88-
const mutator: Mutator = {
94+
const mutator: MutatorWithNamespace = {
8995
name: "test",
9096
Namespace: {
91-
mutate: (ns, clone, p, realm) => {
97+
mutate: (_ns, clone) => {
9298
clone.models.delete("Bar");
9399
},
94100
},
95101
};
96102

97-
const { type } = mutateSubgraph(runner.program, [mutator], Foo);
103+
const { type } = mutateSubgraphWithNamespace(runner.program, [mutator], Foo);
98104

99105
const mutatedNs = type as Namespace;
100106

101107
//Original namespace should have Bar model
102108
expect(Foo.models.has("Bar")).toBeTruthy();
103109
// Mutated namespace should not have Bar model
104110
expect(mutatedNs.models.has("Bar")).toBeFalsy();
111+
// Mutated namespace is propagated to the models
112+
expect(mutatedNs.models.get("Baz")!.namespace?.models.get("Bar")).toBeUndefined();
113+
// Original should be unchanged
114+
expect(Foo.models.get("Baz")!.namespace?.models.get("Bar")).toBeDefined();
115+
expect(Foo.models.get("Baz")!.namespace).toBe(Foo);
105116
});
106117

107118
it("do not recurse the model", async () => {

0 commit comments

Comments
 (0)