typespec-typescript-emitter 1.2.0 → 2.0.1
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/.husky/pre-commit +2 -1
- package/.prettierignore +2 -1
- package/CHANGELOG.md +19 -24
- package/CODE_OF_CONDUCT.md +128 -0
- package/README.md +362 -219
- package/dist/src/emit_routedTypemap.d.ts +4 -0
- package/dist/src/emit_routedTypemap.js +83 -0
- package/dist/src/emit_routedTypemap.js.map +1 -0
- package/dist/src/emit_routes.d.ts +3 -2
- package/dist/src/emit_routes.js +73 -47
- package/dist/src/emit_routes.js.map +1 -1
- package/dist/src/emit_types.d.ts +3 -9
- package/dist/src/emit_types.js +109 -74
- package/dist/src/emit_types.js.map +1 -1
- package/dist/src/emitter.d.ts +2 -6
- package/dist/src/emitter.js +18 -76
- package/dist/src/emitter.js.map +1 -1
- package/dist/src/helpers/appendableString.d.ts +15 -0
- package/dist/src/helpers/appendableString.js +41 -0
- package/dist/src/helpers/appendableString.js.map +1 -0
- package/dist/src/helpers/arrays.d.ts +3 -0
- package/dist/src/helpers/arrays.js +23 -0
- package/dist/src/helpers/arrays.js.map +1 -0
- package/dist/src/{helper_autogenerateWarning.d.ts → helpers/autogenerateWarning.d.ts} +1 -1
- package/dist/src/{helper_autogenerateWarning.js → helpers/autogenerateWarning.js} +1 -2
- package/dist/src/helpers/autogenerateWarning.js.map +1 -0
- package/dist/src/helpers/buildTypeMap.d.ts +12 -0
- package/dist/src/helpers/buildTypeMap.js +44 -0
- package/dist/src/helpers/buildTypeMap.js.map +1 -0
- package/dist/src/helpers/diagnostics.d.ts +4 -0
- package/dist/src/helpers/diagnostics.js +15 -0
- package/dist/src/helpers/diagnostics.js.map +1 -0
- package/dist/src/helpers/getImports.d.ts +2 -0
- package/dist/src/helpers/getImports.js +3 -0
- package/dist/src/helpers/getImports.js.map +1 -0
- package/dist/src/helpers/namespaces.d.ts +4 -0
- package/dist/src/helpers/namespaces.js +14 -0
- package/dist/src/helpers/namespaces.js.map +1 -0
- package/dist/src/helpers/visibilityHelperFile.d.ts +4 -0
- package/dist/src/helpers/visibilityHelperFile.js +57 -0
- package/dist/src/helpers/visibilityHelperFile.js.map +1 -0
- package/dist/src/lib.d.ts +4 -1
- package/dist/src/lib.js +14 -3
- package/dist/src/lib.js.map +1 -1
- package/dist/src/parseOptions.d.ts +8 -0
- package/dist/src/parseOptions.js +26 -0
- package/dist/src/parseOptions.js.map +1 -0
- package/dist/src/resolve/Resolvable.d.ts +32 -0
- package/dist/src/resolve/Resolvable.js +180 -0
- package/dist/src/resolve/Resolvable.js.map +1 -0
- package/dist/src/resolve/Resolvable_helpers.d.ts +42 -0
- package/dist/src/resolve/Resolvable_helpers.js +19 -0
- package/dist/src/resolve/Resolvable_helpers.js.map +1 -0
- package/dist/src/resolve/operationTypemap.d.ts +21 -0
- package/dist/src/resolve/operationTypemap.js +138 -0
- package/dist/src/resolve/operationTypemap.js.map +1 -0
- package/dist/src/resolve/types/Enum.d.ts +21 -0
- package/dist/src/resolve/types/Enum.js +61 -0
- package/dist/src/resolve/types/Enum.js.map +1 -0
- package/dist/src/resolve/types/Model.Indexed.d.ts +12 -0
- package/dist/src/resolve/types/Model.Indexed.js +69 -0
- package/dist/src/resolve/types/Model.Indexed.js.map +1 -0
- package/dist/src/resolve/types/Model.Shaped.d.ts +17 -0
- package/dist/src/resolve/types/Model.Shaped.js +210 -0
- package/dist/src/resolve/types/Model.Shaped.js.map +1 -0
- package/dist/src/resolve/types/Scalar.d.ts +9 -0
- package/dist/src/resolve/types/Scalar.js +109 -0
- package/dist/src/resolve/types/Scalar.js.map +1 -0
- package/dist/src/resolve/types/Simple.d.ts +11 -0
- package/dist/src/resolve/types/Simple.js +49 -0
- package/dist/src/resolve/types/Simple.js.map +1 -0
- package/dist/src/resolve/types/Tuple.d.ts +9 -0
- package/dist/src/resolve/types/Tuple.js +38 -0
- package/dist/src/resolve/types/Tuple.js.map +1 -0
- package/dist/src/resolve/types/Union.d.ts +9 -0
- package/dist/src/resolve/types/Union.js +36 -0
- package/dist/src/resolve/types/Union.js.map +1 -0
- package/eslint.config.js +12 -4
- package/package.json +2 -2
- package/src/emit_routedTypemap.ts +128 -0
- package/src/emit_routes.ts +108 -61
- package/src/emit_types.ts +137 -92
- package/src/emitter.ts +19 -107
- package/src/helpers/appendableString.ts +52 -0
- package/src/helpers/arrays.ts +21 -0
- package/src/{helper_autogenerateWarning.ts → helpers/autogenerateWarning.ts} +0 -1
- package/src/helpers/buildTypeMap.ts +72 -0
- package/src/helpers/diagnostics.ts +19 -0
- package/src/helpers/getImports.ts +9 -0
- package/src/helpers/namespaces.ts +18 -0
- package/src/helpers/visibilityHelperFile.ts +63 -0
- package/src/lib.ts +25 -4
- package/src/parseOptions.ts +26 -0
- package/src/resolve/Resolvable.ts +267 -0
- package/src/resolve/Resolvable_helpers.ts +65 -0
- package/src/resolve/operationTypemap.ts +212 -0
- package/src/resolve/types/Enum.ts +92 -0
- package/src/resolve/types/Model.Indexed.ts +113 -0
- package/src/resolve/types/Model.Shaped.ts +291 -0
- package/src/resolve/types/Scalar.ts +140 -0
- package/src/resolve/types/Simple.ts +88 -0
- package/src/resolve/types/Tuple.ts +56 -0
- package/src/resolve/types/Union.ts +52 -0
- package/test/helpers/integrationTest-novis.tsp +51 -0
- package/test/helpers/integrationTest.tsp +53 -0
- package/test/helpers/largeModel.tsp +40 -0
- package/test/{runner.ts → helpers/runner.ts} +1 -1
- package/test/helpers/ts.ts +11 -0
- package/test/helpers/wrapper.ts +144 -0
- package/test/routes/routes.target.ts +35 -0
- package/test/routes/routes.test.ts +22 -0
- package/test/typeguards/combined.test.ts +78 -0
- package/test/typeguards/enum.test.ts +10 -0
- package/test/typeguards/model.indexed.test.ts +68 -0
- package/test/typeguards/model.shaped.test.ts +38 -0
- package/test/typeguards/scalar.test.ts +62 -0
- package/test/typeguards/simple.test.ts +35 -0
- package/test/typeguards/tuple.test.ts +34 -0
- package/test/typeguards/union.test.ts +29 -0
- package/test/typemap/typemap-novis.target.ts +38 -0
- package/test/typemap/typemap.target.ts +39 -0
- package/test/typemap/typemap.test.ts +48 -0
- package/test/types/combined.test.ts +71 -0
- package/test/types/enum.test.ts +57 -0
- package/test/types/model.indexed.test.ts +46 -0
- package/test/types/model.shaped.test.ts +23 -0
- package/test/types/scalar.test.ts +53 -0
- package/test/types/simple.test.ts +20 -0
- package/test/types/tuple.test.ts +29 -0
- package/test/types/union.test.ts +20 -0
- package/tsconfig.json +1 -0
- package/dist/src/emit_mapped_types.d.ts +0 -2
- package/dist/src/emit_mapped_types.js +0 -124
- package/dist/src/emit_mapped_types.js.map +0 -1
- package/dist/src/emit_types_resolve.d.ts +0 -22
- package/dist/src/emit_types_resolve.js +0 -217
- package/dist/src/emit_types_resolve.js.map +0 -1
- package/dist/src/emit_types_typeguards.d.ts +0 -17
- package/dist/src/emit_types_typeguards.js +0 -121
- package/dist/src/emit_types_typeguards.js.map +0 -1
- package/dist/src/helper_autogenerateWarning.js.map +0 -1
- package/src/emit_mapped_types.ts +0 -155
- package/src/emit_types_resolve.ts +0 -280
- package/src/emit_types_typeguards.ts +0 -178
- package/test/main.test.ts +0 -83
- package/test/out/.gitkeep +0 -0
- package/test/targets/enum.routed-types.ts +0 -59
- package/test/targets/enum.routes.ts +0 -29
- package/test/targets/enum.target.ts +0 -39
- package/test/targets/enum.tsp +0 -36
- package/test/targets/pr8.routed-types.ts +0 -131
- package/test/targets/pr8.routes.ts +0 -30
- package/test/targets/pr8.target.ts +0 -97
- package/test/targets/pr8.tsp +0 -62
- package/test/targets/simple-routes.routed-types.ts +0 -64
- package/test/targets/simple-routes.routes.ts +0 -29
- package/test/targets/simple-routes.target.ts +0 -21
- package/test/targets/simple-routes.tsp +0 -23
- package/test/targets/union.routed-types.ts +0 -59
- package/test/targets/union.routes.ts +0 -23
- package/test/targets/union.target.ts +0 -59
- package/test/targets/union.tsp +0 -38
- package/test/targets/visibility.routed-types.ts +0 -81
- package/test/targets/visibility.routes.ts +0 -38
- package/test/targets/visibility.target.ts +0 -36
- package/test/targets/visibility.tsp +0 -49
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { Model, Type } from "@typespec/compiler";
|
|
2
|
+
import { AppendableString } from "../helpers/appendableString.js";
|
|
3
|
+
import { compareArrays } from "../helpers/arrays.js";
|
|
4
|
+
import { TTypeMap } from "../helpers/buildTypeMap.js";
|
|
5
|
+
import { reportDiagnostic } from "../helpers/diagnostics.js";
|
|
6
|
+
import { namespaceListFromNamespace } from "../helpers/namespaces.js";
|
|
7
|
+
import {
|
|
8
|
+
Resolver,
|
|
9
|
+
ResolverOptions,
|
|
10
|
+
ResolverResult,
|
|
11
|
+
} from "./Resolvable_helpers.js";
|
|
12
|
+
|
|
13
|
+
export abstract class Resolvable<T extends Type> {
|
|
14
|
+
/** Recursively resolves a type. */
|
|
15
|
+
static async resolve<R extends Resolver>(
|
|
16
|
+
r: R,
|
|
17
|
+
t: Type,
|
|
18
|
+
opts: ResolverOptions<R>,
|
|
19
|
+
): Promise<ResolverResult<R>> {
|
|
20
|
+
const ret: ResolverResult<R> = {
|
|
21
|
+
resolved: new AppendableString(),
|
|
22
|
+
visibilityMap: "",
|
|
23
|
+
hasVisibility: false,
|
|
24
|
+
imports: [],
|
|
25
|
+
};
|
|
26
|
+
await (await Resolvable.for(r, t)).resolve(opts, ret);
|
|
27
|
+
return ret;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* To be called from within the resolution logic of a type:
|
|
32
|
+
* Resolves another type for the current type, handling required changes to the resolution options.
|
|
33
|
+
* The passed `out` object is not signifiantly changed, only the `hasVisibility` flag is managed.
|
|
34
|
+
*/
|
|
35
|
+
protected async resolveNested<R extends typeof this._r>(
|
|
36
|
+
t: Type,
|
|
37
|
+
opts: ResolverOptions<R>,
|
|
38
|
+
out: ResolverResult<R>,
|
|
39
|
+
nest: boolean = true,
|
|
40
|
+
): Promise<ResolverResult<R>> {
|
|
41
|
+
// prepare nested resolution
|
|
42
|
+
if (nest) opts.nestlevel++;
|
|
43
|
+
if (!opts.parents) opts.parents = [];
|
|
44
|
+
opts.parents.push(this._t);
|
|
45
|
+
|
|
46
|
+
const ret = await Resolvable.resolve(this._r, t, opts);
|
|
47
|
+
|
|
48
|
+
// revert to current level
|
|
49
|
+
if (nest) opts.nestlevel--;
|
|
50
|
+
opts.parents.pop();
|
|
51
|
+
|
|
52
|
+
return ret;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Creates the appropriate Resolvable instance for a given type. */
|
|
56
|
+
static async for<T extends Type>(r: Resolver, t: T): Promise<Resolvable<T>> {
|
|
57
|
+
// use lazy imports to break circular dependencies
|
|
58
|
+
const { ResolvableSimple } = await import("./types/Simple.js");
|
|
59
|
+
if (ResolvableSimple.AllowedTypeKinds.includes(t.kind as any)) {
|
|
60
|
+
return new ResolvableSimple(t, r) as any;
|
|
61
|
+
}
|
|
62
|
+
switch (t.kind) {
|
|
63
|
+
case "Enum": {
|
|
64
|
+
const { ResolvableEnum } = await import("./types/Enum.js");
|
|
65
|
+
return new ResolvableEnum(t, r) as any;
|
|
66
|
+
}
|
|
67
|
+
case "EnumMember": {
|
|
68
|
+
const { ResolvableEnumMember } = await import("./types/Enum.js");
|
|
69
|
+
return new ResolvableEnumMember(t, r) as any;
|
|
70
|
+
}
|
|
71
|
+
case "Model": {
|
|
72
|
+
const { IndexedModel } = await import("./types/Model.Indexed.js");
|
|
73
|
+
const { ShapedModel } = await import("./types/Model.Shaped.js");
|
|
74
|
+
switch ((t as Model).name) {
|
|
75
|
+
case "Array":
|
|
76
|
+
case "Record":
|
|
77
|
+
return new IndexedModel(t, r) as any;
|
|
78
|
+
|
|
79
|
+
default:
|
|
80
|
+
return new ShapedModel(t, r) as any;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
case "Scalar": {
|
|
84
|
+
const { ResolvableScalar } = await import("./types/Scalar.js");
|
|
85
|
+
return new ResolvableScalar(t, r) as any;
|
|
86
|
+
}
|
|
87
|
+
case "Tuple": {
|
|
88
|
+
const { ResolvableTuple } = await import("./types/Tuple.js");
|
|
89
|
+
return new ResolvableTuple(t, r) as any;
|
|
90
|
+
}
|
|
91
|
+
case "Union": {
|
|
92
|
+
const { ResolvableUnion } = await import("./types/Union.js");
|
|
93
|
+
return new ResolvableUnion(t, r) as any;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
default:
|
|
97
|
+
reportDiagnostic({
|
|
98
|
+
code: "resolve-unresolved",
|
|
99
|
+
message: `Could not resolve type ${t.kind}`,
|
|
100
|
+
severity: "error",
|
|
101
|
+
});
|
|
102
|
+
throw new TypeError(`Could not resolve type ${t.kind}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
constructor(t: T, r: Resolver) {
|
|
107
|
+
this._t = t;
|
|
108
|
+
this._r = r;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
protected abstract expectedTypeKind: string;
|
|
112
|
+
|
|
113
|
+
protected _t: T;
|
|
114
|
+
protected _r: Resolver;
|
|
115
|
+
|
|
116
|
+
/** Modifies passed output object in-place. */
|
|
117
|
+
protected abstract type(
|
|
118
|
+
opts: ResolverOptions<Resolver.Type>,
|
|
119
|
+
out: ResolverResult<Resolver.Type>,
|
|
120
|
+
): Promise<void>;
|
|
121
|
+
/** Modifies passed output object in-place. */
|
|
122
|
+
protected abstract typeguard(
|
|
123
|
+
opts: ResolverOptions<Resolver.Typeguard>,
|
|
124
|
+
out: ResolverResult<Resolver.Typeguard>,
|
|
125
|
+
): Promise<void>;
|
|
126
|
+
|
|
127
|
+
/** Resolves the type, modifying an output object in place. */
|
|
128
|
+
public async resolve(
|
|
129
|
+
opts: ResolverOptions<Resolver>,
|
|
130
|
+
out: ResolverResult<Resolver>,
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
this.validate();
|
|
133
|
+
|
|
134
|
+
const ownVisibility = await this.hasVisibility(opts, out);
|
|
135
|
+
if (ownVisibility) out.hasVisibility = true;
|
|
136
|
+
|
|
137
|
+
// check for known resolved type
|
|
138
|
+
if (opts.rootTypeReady && (this._t as any).name) {
|
|
139
|
+
// type without name can't be resolved
|
|
140
|
+
const currentNamespaces = namespaceListFromNamespace(
|
|
141
|
+
(this._t as any).namespace,
|
|
142
|
+
);
|
|
143
|
+
// find known resolved type
|
|
144
|
+
const foundKnownResolved = ((): TTypeMap[number] | null => {
|
|
145
|
+
const currentNamespaces = namespaceListFromNamespace(
|
|
146
|
+
(this._t as any).namespace,
|
|
147
|
+
);
|
|
148
|
+
if (currentNamespaces && currentNamespaces.length > 0) {
|
|
149
|
+
const found = opts.typemap.find(
|
|
150
|
+
(mt) =>
|
|
151
|
+
mt.type.kind === this._t.kind &&
|
|
152
|
+
mt.type.name === (this._t as any).name &&
|
|
153
|
+
compareArrays(mt.namespaces, currentNamespaces),
|
|
154
|
+
);
|
|
155
|
+
return found ?? null;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
})();
|
|
159
|
+
|
|
160
|
+
// if found, return name instead of full resolution
|
|
161
|
+
if (foundKnownResolved) {
|
|
162
|
+
if (
|
|
163
|
+
opts.rootType &&
|
|
164
|
+
compareArrays(currentNamespaces!, opts.rootType.namespaces)
|
|
165
|
+
) {
|
|
166
|
+
// if namespace of found is same as current (= same file), just return name
|
|
167
|
+
out.resolved.append(
|
|
168
|
+
this._r === Resolver.Type
|
|
169
|
+
? foundKnownResolved.type.name
|
|
170
|
+
: this._t.kind === "Enum"
|
|
171
|
+
? "true" // enums don't have typeguards
|
|
172
|
+
: `is${foundKnownResolved.type.name}(${(opts as ResolverOptions<Resolver.Typeguard>).accessor}${ownVisibility ? ", vis" : ""})`,
|
|
173
|
+
);
|
|
174
|
+
} else {
|
|
175
|
+
// found is in different file than current, prepare importing
|
|
176
|
+
out.resolved.append(
|
|
177
|
+
`${foundKnownResolved.namespaces.join("_")}.${
|
|
178
|
+
this._r === Resolver.Type
|
|
179
|
+
? foundKnownResolved.type.name
|
|
180
|
+
: this._t.kind === "Enum"
|
|
181
|
+
? "true" // enums don't have typeguards
|
|
182
|
+
: `is${foundKnownResolved.type.name}(${(opts as ResolverOptions<Resolver.Typeguard>).accessor}${ownVisibility ? ", vis" : ""})`
|
|
183
|
+
}`,
|
|
184
|
+
);
|
|
185
|
+
out.imports.push(foundKnownResolved.namespaces);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// optionally allow found type to modify output
|
|
189
|
+
if (this.transformKnownType !== undefined)
|
|
190
|
+
this.transformKnownType(opts, out);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// this prevents the root type from triggering known resolution
|
|
196
|
+
opts.rootTypeReady = true;
|
|
197
|
+
|
|
198
|
+
switch (this._r) {
|
|
199
|
+
case Resolver.Type:
|
|
200
|
+
return await this.type(
|
|
201
|
+
opts as ResolverOptions<Resolver.Type>,
|
|
202
|
+
out as ResolverResult<Resolver.Type>,
|
|
203
|
+
);
|
|
204
|
+
case Resolver.Typeguard:
|
|
205
|
+
return await this.typeguard(
|
|
206
|
+
opts as ResolverOptions<Resolver.Typeguard>,
|
|
207
|
+
out as ResolverResult<Resolver.Typeguard>,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected transformKnownType(
|
|
213
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
214
|
+
opts: ResolverOptions<Resolver>,
|
|
215
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
216
|
+
out: ResolverResult<Resolver>,
|
|
217
|
+
): void {}
|
|
218
|
+
|
|
219
|
+
/** Returns true if this type or anything it contains or references has visibility modifiers. */
|
|
220
|
+
public async hasVisibility(
|
|
221
|
+
opts: ResolverOptions<Resolver>,
|
|
222
|
+
out: ResolverResult<Resolver>,
|
|
223
|
+
): Promise<boolean> {
|
|
224
|
+
return out.hasVisibility;
|
|
225
|
+
}
|
|
226
|
+
/** Returns true if this type or anything it contains or references has visibility modifiers. */
|
|
227
|
+
protected static async hasVisibility(
|
|
228
|
+
t: Type,
|
|
229
|
+
opts: ResolverOptions<Resolver>,
|
|
230
|
+
out: ResolverResult<Resolver>,
|
|
231
|
+
current: Type,
|
|
232
|
+
): Promise<boolean> {
|
|
233
|
+
if (!opts.parents) opts.parents = [];
|
|
234
|
+
opts.parents.push(current);
|
|
235
|
+
const ret = await (
|
|
236
|
+
await Resolvable.for(Resolver.Type /** doesn't matter */, t)
|
|
237
|
+
).hasVisibility(opts, out);
|
|
238
|
+
opts.parents.pop();
|
|
239
|
+
return ret;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Checks its stored type to ensure the instance was created on a suitable type. */
|
|
243
|
+
protected validate(): void {
|
|
244
|
+
if (this._t.kind !== this.expectedTypeKind) {
|
|
245
|
+
this.diagnostic(
|
|
246
|
+
"typeclass-mismatch",
|
|
247
|
+
`Tried creating resolver for ${this.expectedTypeKind} on type ${this._t.kind}`,
|
|
248
|
+
"error",
|
|
249
|
+
);
|
|
250
|
+
throw new TypeError(
|
|
251
|
+
`Type-kind/class mismatch: instanced ${this.expectedTypeKind} on type ${this._t.kind}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
protected diagnostic(
|
|
257
|
+
code: string,
|
|
258
|
+
msg: string,
|
|
259
|
+
level: "error" | "warning",
|
|
260
|
+
): void {
|
|
261
|
+
reportDiagnostic({
|
|
262
|
+
code: `resolve-${this._r}-${code}`,
|
|
263
|
+
message: msg,
|
|
264
|
+
severity: level,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Program, Type } from "@typespec/compiler";
|
|
2
|
+
import { AppendableString } from "../helpers/appendableString.js";
|
|
3
|
+
import { compareArrays } from "../helpers/arrays.js";
|
|
4
|
+
import { TTypeMap } from "../helpers/buildTypeMap.js";
|
|
5
|
+
import { namespaceListFromNamespace } from "../helpers/namespaces.js";
|
|
6
|
+
import { EmitterOptions } from "../lib.js";
|
|
7
|
+
|
|
8
|
+
export enum Resolver {
|
|
9
|
+
Type,
|
|
10
|
+
Typeguard,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ResolverOptions<R extends Resolver> = {
|
|
14
|
+
program: Program;
|
|
15
|
+
options: EmitterOptions;
|
|
16
|
+
typemap: TTypeMap;
|
|
17
|
+
nestlevel: number;
|
|
18
|
+
/** The type the entire resolution chain started with. */
|
|
19
|
+
rootType: TTypeMap[number] | null;
|
|
20
|
+
/**
|
|
21
|
+
* This tracks whether or not resolving *to* the type currently being investigated
|
|
22
|
+
* is valid. This allows for recursive types.
|
|
23
|
+
*/
|
|
24
|
+
rootTypeReady?: boolean;
|
|
25
|
+
/** The types "up the chain" - "parent" types on nested resolution. */
|
|
26
|
+
parents?: Type[];
|
|
27
|
+
} & (R extends Resolver.Type
|
|
28
|
+
? {
|
|
29
|
+
emitDocs: boolean;
|
|
30
|
+
}
|
|
31
|
+
: {
|
|
32
|
+
/**
|
|
33
|
+
* This string describes how a function (part) of a typeguard can
|
|
34
|
+
* access the value currently being tested by it.
|
|
35
|
+
* Example: typeguard(t: any) on `n` for {n: string} -> this = "t"
|
|
36
|
+
*/
|
|
37
|
+
accessor: string;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** Tries to find type in known types map */
|
|
41
|
+
export const getKnownResolvedType = (
|
|
42
|
+
typemap: TTypeMap,
|
|
43
|
+
t: any,
|
|
44
|
+
): TTypeMap[number] | null => {
|
|
45
|
+
const currentNamespaces = namespaceListFromNamespace((t as any).namespace);
|
|
46
|
+
if (currentNamespaces && currentNamespaces.length > 0) {
|
|
47
|
+
const found = typemap.find(
|
|
48
|
+
(mt) =>
|
|
49
|
+
mt.type.kind === t.kind &&
|
|
50
|
+
mt.type.name === (t as any).name &&
|
|
51
|
+
compareArrays(mt.namespaces, currentNamespaces),
|
|
52
|
+
);
|
|
53
|
+
return found ?? null;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type ResolverResult<R extends Resolver> = {
|
|
59
|
+
readonly resolved: AppendableString;
|
|
60
|
+
visibilityMap: string;
|
|
61
|
+
imports: TTypeMap[number]["namespaces"][];
|
|
62
|
+
/** Whether any resolved part (nested types and base types included) have @visibility modifiers */
|
|
63
|
+
hasVisibility: boolean;
|
|
64
|
+
doc?: R extends Resolver.Type ? string : never;
|
|
65
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { Operation, Program, Type } from "@typespec/compiler";
|
|
2
|
+
import { getHttpOperation } from "@typespec/http";
|
|
3
|
+
import { TTypeMap } from "../helpers/buildTypeMap.js";
|
|
4
|
+
import { EmitterOptions } from "../lib.js";
|
|
5
|
+
import { Resolvable } from "./Resolvable.js";
|
|
6
|
+
import { Resolver } from "./Resolvable_helpers.js";
|
|
7
|
+
|
|
8
|
+
/** Maps a route path to its typemap definition and required imports */
|
|
9
|
+
export type TOperationTypemap = {
|
|
10
|
+
// "string" in these does not refer to the type "string"; it's the typescript code *as* string.
|
|
11
|
+
request: { content: string; hasVisibility: boolean };
|
|
12
|
+
response: {
|
|
13
|
+
content: Array<{
|
|
14
|
+
status: number | "unknown";
|
|
15
|
+
body: string;
|
|
16
|
+
hasVisibility: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const resolveOperationTypemap = async (
|
|
22
|
+
program: Program,
|
|
23
|
+
options: EmitterOptions,
|
|
24
|
+
typemap: TTypeMap,
|
|
25
|
+
op: Operation,
|
|
26
|
+
): Promise<{
|
|
27
|
+
types: TOperationTypemap;
|
|
28
|
+
imports: TTypeMap[number]["namespaces"][];
|
|
29
|
+
}> => {
|
|
30
|
+
const httpOp = getHttpOperation(program, op)[0];
|
|
31
|
+
const ret: Awaited<ReturnType<typeof resolveOperationTypemap>> = {
|
|
32
|
+
types: {
|
|
33
|
+
request: { content: "null", hasVisibility: false },
|
|
34
|
+
response: { content: [] },
|
|
35
|
+
},
|
|
36
|
+
imports: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// request
|
|
40
|
+
if (httpOp.parameters.body) {
|
|
41
|
+
const resolved = await Resolvable.resolve(
|
|
42
|
+
Resolver.Type,
|
|
43
|
+
httpOp.parameters.body.type,
|
|
44
|
+
{
|
|
45
|
+
program,
|
|
46
|
+
options,
|
|
47
|
+
emitDocs: false,
|
|
48
|
+
nestlevel: 3,
|
|
49
|
+
rootType: null,
|
|
50
|
+
typemap,
|
|
51
|
+
rootTypeReady: true,
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
ret.imports.push(...resolved.imports);
|
|
55
|
+
ret.types.request.content = replaceLifecycle(
|
|
56
|
+
resolved.resolved.value,
|
|
57
|
+
httpOp.verb.toUpperCase(),
|
|
58
|
+
resolved.hasVisibility,
|
|
59
|
+
);
|
|
60
|
+
if (resolved.hasVisibility) ret.types.request.hasVisibility = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// response
|
|
64
|
+
if (op.returnType) {
|
|
65
|
+
const getReturnType = async (
|
|
66
|
+
t: Type,
|
|
67
|
+
): Promise<{
|
|
68
|
+
content: {
|
|
69
|
+
status: number | "unknown";
|
|
70
|
+
body: string;
|
|
71
|
+
hasVisibility: boolean;
|
|
72
|
+
}[];
|
|
73
|
+
}> => {
|
|
74
|
+
const responseRet: TOperationTypemap["response"] = { content: [] };
|
|
75
|
+
|
|
76
|
+
switch (t.kind) {
|
|
77
|
+
case "Model": {
|
|
78
|
+
// If the return type is a model, it may either be a "blank" body or a fully
|
|
79
|
+
// qualified response with status and body.
|
|
80
|
+
const modelret: TOperationTypemap["response"]["content"][number] = {
|
|
81
|
+
status: 200,
|
|
82
|
+
body: "{}",
|
|
83
|
+
hasVisibility: false,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// check for fully qualified response or plain body
|
|
87
|
+
let wasFullyQualified = false;
|
|
88
|
+
for (const prop of t.properties) {
|
|
89
|
+
for (const decorator of prop[1].decorators) {
|
|
90
|
+
// find status code
|
|
91
|
+
if (
|
|
92
|
+
decorator.definition?.name === "@statusCode" &&
|
|
93
|
+
prop[1].type.kind === "Number"
|
|
94
|
+
)
|
|
95
|
+
modelret.status = prop[1].type.value;
|
|
96
|
+
// find body definiton
|
|
97
|
+
if (decorator.definition?.name === "@body") {
|
|
98
|
+
const resolved = await Resolvable.resolve(
|
|
99
|
+
Resolver.Type,
|
|
100
|
+
prop[1].type,
|
|
101
|
+
{
|
|
102
|
+
program,
|
|
103
|
+
options,
|
|
104
|
+
emitDocs: false,
|
|
105
|
+
nestlevel: 3,
|
|
106
|
+
rootType: null,
|
|
107
|
+
typemap,
|
|
108
|
+
rootTypeReady: true,
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
if (resolved.hasVisibility) modelret.hasVisibility = true;
|
|
112
|
+
ret.imports.push(...resolved.imports);
|
|
113
|
+
modelret.body = replaceLifecycle(
|
|
114
|
+
resolved.resolved.value,
|
|
115
|
+
"RETURN",
|
|
116
|
+
resolved.hasVisibility,
|
|
117
|
+
);
|
|
118
|
+
wasFullyQualified = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!wasFullyQualified) {
|
|
124
|
+
const resolved = await Resolvable.resolve(Resolver.Type, t, {
|
|
125
|
+
program,
|
|
126
|
+
options,
|
|
127
|
+
emitDocs: false,
|
|
128
|
+
nestlevel: 3,
|
|
129
|
+
rootType: null,
|
|
130
|
+
typemap,
|
|
131
|
+
rootTypeReady: true,
|
|
132
|
+
});
|
|
133
|
+
if (resolved.hasVisibility) modelret.hasVisibility = true;
|
|
134
|
+
ret.imports.push(...resolved.imports);
|
|
135
|
+
modelret.body = replaceLifecycle(
|
|
136
|
+
resolved.resolved.value,
|
|
137
|
+
"RETURN",
|
|
138
|
+
resolved.hasVisibility,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
responseRet.content.push(modelret);
|
|
143
|
+
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case "Union": {
|
|
148
|
+
// if return type is a union, each variant may be fully qualified or body-only
|
|
149
|
+
for (const variant of t.variants) {
|
|
150
|
+
const resolved = await getReturnType(variant[1].type);
|
|
151
|
+
responseRet.content.push(...resolved.content);
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
default: {
|
|
157
|
+
// return type does not have a qualified body; making one up and resolving that one
|
|
158
|
+
const resolved = await Resolvable.resolve(Resolver.Type, t, {
|
|
159
|
+
program,
|
|
160
|
+
options,
|
|
161
|
+
emitDocs: false,
|
|
162
|
+
nestlevel: 3,
|
|
163
|
+
rootType: null,
|
|
164
|
+
typemap,
|
|
165
|
+
rootTypeReady: true,
|
|
166
|
+
});
|
|
167
|
+
ret.imports.push(...resolved.imports);
|
|
168
|
+
responseRet.content.push({
|
|
169
|
+
status: 200,
|
|
170
|
+
body: replaceLifecycle(
|
|
171
|
+
resolved.resolved.value,
|
|
172
|
+
httpOp.verb.toUpperCase(),
|
|
173
|
+
resolved.hasVisibility,
|
|
174
|
+
),
|
|
175
|
+
hasVisibility: resolved.hasVisibility,
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return responseRet;
|
|
182
|
+
};
|
|
183
|
+
ret.types.response = await getReturnType(op.returnType);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return ret;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** Maps HTTP verbs to lifecycle states. See @typespec/compiler/.../visibility.tsp */
|
|
190
|
+
const VerbToLifecycle = {
|
|
191
|
+
RETURN: ["Read"], // for all return types
|
|
192
|
+
POST: ["Create"],
|
|
193
|
+
PUT: ["Create", "Update"],
|
|
194
|
+
PATCH: ["Update"],
|
|
195
|
+
DELETE: ["Delete"],
|
|
196
|
+
GET: ["Query"], // *parameters* of request, return type is still RETURN
|
|
197
|
+
Head: ["Query"],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// lifecycle assignment helper
|
|
201
|
+
const replaceLifecycle = (
|
|
202
|
+
resolved: string,
|
|
203
|
+
verb: string,
|
|
204
|
+
hasVisibility: boolean,
|
|
205
|
+
): string => {
|
|
206
|
+
const opLifecycle = VerbToLifecycle[verb as keyof typeof VerbToLifecycle];
|
|
207
|
+
if (!hasVisibility) return resolved;
|
|
208
|
+
return resolved.replaceAll(
|
|
209
|
+
"V>",
|
|
210
|
+
`V extends Lifecycle.All ? (${opLifecycle.map((l) => `Lifecycle.${l}`).join(" | ")}) : V>`,
|
|
211
|
+
);
|
|
212
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Enum, EnumMember, getDoc } from "@typespec/compiler";
|
|
2
|
+
import { compareArrays } from "../../helpers/arrays.js";
|
|
3
|
+
import { namespaceListFromNamespace } from "../../helpers/namespaces.js";
|
|
4
|
+
import { Resolvable } from "../Resolvable.js";
|
|
5
|
+
import {
|
|
6
|
+
Resolver,
|
|
7
|
+
ResolverOptions,
|
|
8
|
+
ResolverResult,
|
|
9
|
+
} from "../Resolvable_helpers.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* This resolves Enums completely, meaning it doesn't try to resolve
|
|
13
|
+
* members as EnumMember, because EnumMember resolution references the base Enum.
|
|
14
|
+
*/
|
|
15
|
+
export class ResolvableEnum extends Resolvable<Enum> {
|
|
16
|
+
protected expectedTypeKind = "Enum";
|
|
17
|
+
|
|
18
|
+
protected async type(
|
|
19
|
+
opts: ResolverOptions<Resolver.Type>,
|
|
20
|
+
out: ResolverResult<Resolver.Type>,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
if (opts.emitDocs) {
|
|
23
|
+
out.doc = getDoc(opts.program, this._t);
|
|
24
|
+
}
|
|
25
|
+
out.resolved.append("{\n");
|
|
26
|
+
let i = 1;
|
|
27
|
+
this._t.members.forEach((member) => {
|
|
28
|
+
const val = resolveEnumMemberValue(member, opts);
|
|
29
|
+
out.resolved.addLine(
|
|
30
|
+
`${member.name.includes("-") ? `'${member.name}'` : member.name}${val}${i < this._t.members.size ? "," : ""}`,
|
|
31
|
+
opts.nestlevel + 1,
|
|
32
|
+
);
|
|
33
|
+
i++;
|
|
34
|
+
});
|
|
35
|
+
out.resolved.addLine("}", opts.nestlevel, "continued");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected async typeguard(): Promise<void> {
|
|
39
|
+
// enums cannot have typeguards
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This cannot resolve members during Enum resolution, because this
|
|
46
|
+
* will try to reference the base Enum by name.
|
|
47
|
+
*/
|
|
48
|
+
export class ResolvableEnumMember extends Resolvable<EnumMember> {
|
|
49
|
+
protected expectedTypeKind = "EnumMember";
|
|
50
|
+
|
|
51
|
+
protected async type(
|
|
52
|
+
opts: ResolverOptions<Resolver.Type>,
|
|
53
|
+
out: ResolverResult<Resolver.Type>,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const foundParent = opts.typemap.find(
|
|
56
|
+
(mt) =>
|
|
57
|
+
mt.type.kind === "Enum" &&
|
|
58
|
+
mt.type.name === this._t.enum.name &&
|
|
59
|
+
compareArrays(
|
|
60
|
+
mt.namespaces,
|
|
61
|
+
namespaceListFromNamespace(this._t.enum.namespace) ?? [],
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
if (foundParent) {
|
|
65
|
+
out.resolved.append(`${foundParent.type.name}.${this._t.name}`);
|
|
66
|
+
out.imports.push(foundParent.namespaces);
|
|
67
|
+
} else {
|
|
68
|
+
out.resolved.append(resolveEnumMemberValue(this._t, opts));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected async typeguard(
|
|
73
|
+
opts: ResolverOptions<Resolver.Typeguard>,
|
|
74
|
+
out: ResolverResult<Resolver.Typeguard>,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
out.resolved.append("true");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const resolveEnumMemberValue = (
|
|
81
|
+
member: EnumMember,
|
|
82
|
+
opts: ResolverOptions<any>,
|
|
83
|
+
): string => {
|
|
84
|
+
return member.value === undefined
|
|
85
|
+
? opts.options["string-nominal-enums"]
|
|
86
|
+
? ` = '${member.name}'`
|
|
87
|
+
: ""
|
|
88
|
+
: " = " +
|
|
89
|
+
(typeof member.value === "string"
|
|
90
|
+
? `'${member.value}'`
|
|
91
|
+
: member.value.toString());
|
|
92
|
+
};
|