@azure-tools/typespec-ts 0.56.1-dev.1 → 0.57.0-dev.2
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/README.md +6 -0
- package/dist/src/framework/hooks/sdk-types.d.ts.map +1 -1
- package/dist/src/framework/hooks/sdk-types.js +2 -0
- package/dist/src/framework/hooks/sdk-types.js.map +1 -1
- package/dist/src/interfaces.d.ts +4 -0
- package/dist/src/interfaces.d.ts.map +1 -1
- package/dist/src/interfaces.js.map +1 -1
- package/dist/src/lib.d.ts +1 -0
- package/dist/src/lib.d.ts.map +1 -1
- package/dist/src/lib.js +5 -0
- package/dist/src/lib.js.map +1 -1
- package/dist/src/modular/helpers/visibility-helpers.d.ts +7 -0
- package/dist/src/modular/helpers/visibility-helpers.d.ts.map +1 -0
- package/dist/src/modular/helpers/visibility-helpers.js +353 -0
- package/dist/src/modular/helpers/visibility-helpers.js.map +1 -0
- package/dist/src/transform/transform-client-options.d.ts.map +1 -1
- package/dist/src/transform/transform-client-options.js +2 -0
- package/dist/src/transform/transform-client-options.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/framework/hooks/sdk-types.ts +2 -0
- package/src/interfaces.ts +4 -0
- package/src/lib.ts +7 -0
- package/src/modular/helpers/visibility-helpers.ts +471 -0
- package/src/transform/transform-client-options.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-tools/typespec-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0-dev.2",
|
|
4
4
|
"description": "A TypeSpec emitter for TypeScript",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@azure-rest/core-client": "^2.7.0",
|
|
21
|
-
"@azure-tools/azure-http-specs": "^0.1.0-alpha.44 || >= 0.1.0-alpha.45-dev.
|
|
21
|
+
"@azure-tools/azure-http-specs": "^0.1.0-alpha.44 || >= 0.1.0-alpha.45-dev.4",
|
|
22
22
|
"@azure-tools/typespec-autorest": "^0.71.0 || >= 0.72.0-dev.0",
|
|
23
23
|
"@azure-tools/typespec-azure-core": "^0.71.0 || >= 0.72.0-dev.0",
|
|
24
24
|
"@azure-tools/typespec-azure-resource-manager": "^0.71.0 || >= 0.72.0-dev.1",
|
|
@@ -39,26 +39,26 @@
|
|
|
39
39
|
"@microsoft/api-extractor": "^7.58.1",
|
|
40
40
|
"@types/pluralize": "^0.0.33",
|
|
41
41
|
"yaml": "^2.8.3",
|
|
42
|
-
"@azure-tools/spector-runner": "^0.1.0",
|
|
43
42
|
"@typespec/spector": "^0.1.0-alpha.28",
|
|
43
|
+
"@azure-tools/spector-runner": "^0.1.0",
|
|
44
44
|
"@typespec/spec-api": "^0.1.0-alpha.16",
|
|
45
|
-
"@typespec/http-specs": "^0.1.0-alpha.41",
|
|
46
45
|
"@typespec/tspd": "^0.77.0",
|
|
47
|
-
"@typespec/compiler": "^1.15.0",
|
|
48
46
|
"@typespec/http": "^1.15.0",
|
|
49
|
-
"@typespec/
|
|
47
|
+
"@typespec/compiler": "^1.15.0",
|
|
50
48
|
"@typespec/openapi": "^1.15.0",
|
|
49
|
+
"@typespec/rest": "^0.85.0",
|
|
50
|
+
"@typespec/versioning": "^0.85.0",
|
|
51
51
|
"@typespec/xml": "^0.85.0",
|
|
52
|
-
"@typespec/
|
|
52
|
+
"@typespec/http-specs": "^0.1.0-alpha.41"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@azure-tools/typespec-azure-core": "^0.71.0 || >= 0.72.0-dev.0",
|
|
56
56
|
"@azure-tools/typespec-client-generator-core": "^0.71.2 || >= 0.72.0-dev.3",
|
|
57
|
-
"@typespec/compiler": "^1.15.0",
|
|
58
57
|
"@typespec/http": "^1.15.0",
|
|
59
58
|
"@typespec/rest": "^0.85.0",
|
|
60
59
|
"@typespec/xml": "^0.85.0",
|
|
61
|
-
"@typespec/versioning": "^0.85.0"
|
|
60
|
+
"@typespec/versioning": "^0.85.0",
|
|
61
|
+
"@typespec/compiler": "^1.15.0"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"fast-xml-parser": "^5.10.1",
|
|
@@ -13,6 +13,7 @@ import { provideContext, useContext } from "../../context-manager.js";
|
|
|
13
13
|
import { reportDiagnostic } from "../../lib.js";
|
|
14
14
|
import { visitPackageTypes } from "../../modular/emit-models.js";
|
|
15
15
|
import { getAllAncestors, getAllProperties } from "../../modular/helpers/operation-helpers.js";
|
|
16
|
+
import { applyVisibilityModelSplit } from "../../modular/helpers/visibility-helpers.js";
|
|
16
17
|
import { normalizeModelPropertyName } from "../../modular/type-expressions/get-type-expression.js";
|
|
17
18
|
import type { SdkContext } from "../../utils/interfaces.js";
|
|
18
19
|
import { NameType, normalizeName } from "../../utils/name-utils.js";
|
|
@@ -92,6 +93,7 @@ export function provideSdkTypes(context: SdkContext) {
|
|
|
92
93
|
types: new Map<Type, SdkType>(),
|
|
93
94
|
flattenProperties: new Map<SdkModelPropertyType, SdkFlattenPropertyContext>(),
|
|
94
95
|
};
|
|
96
|
+
applyVisibilityModelSplit(context);
|
|
95
97
|
visitPackageTypes(context);
|
|
96
98
|
enrichFlattenProperties(context, sdkTypesContext.flattenProperties, flattenPropertyModelMap);
|
|
97
99
|
for (const sdkModel of emitQueue) {
|
package/src/interfaces.ts
CHANGED
|
@@ -260,6 +260,10 @@ export interface ClientOptions {
|
|
|
260
260
|
* Defaults to false.
|
|
261
261
|
*/
|
|
262
262
|
generateReactNativeTarget?: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Generates visibility-specific request models. Experimental and disabled by default.
|
|
265
|
+
*/
|
|
266
|
+
experimentalSplitModelsByVisibility?: boolean;
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
export interface ServiceInfo {
|
package/src/lib.ts
CHANGED
|
@@ -91,6 +91,7 @@ export interface EmitterOptions {
|
|
|
91
91
|
* package.json exports). Defaults to `false`.
|
|
92
92
|
*/
|
|
93
93
|
"generate-react-native-target"?: boolean;
|
|
94
|
+
"experimental-split-models-by-visibility"?: boolean;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
export const EmitterOptionsSchema: JSONSchemaType<EmitterOptions> = {
|
|
@@ -312,6 +313,12 @@ export const EmitterOptionsSchema: JSONSchemaType<EmitterOptions> = {
|
|
|
312
313
|
description:
|
|
313
314
|
"When set to true, generates React Native build targets (tsconfig, warp target, package.json exports). Defaults to `false`.",
|
|
314
315
|
},
|
|
316
|
+
"experimental-split-models-by-visibility": {
|
|
317
|
+
type: "boolean",
|
|
318
|
+
nullable: true,
|
|
319
|
+
description:
|
|
320
|
+
"Whether to project request-body models to their write visibility (e.g. WidgetCreate/WidgetCreateOrUpdate) so read-only properties do not leak into input types. Defaults to false",
|
|
321
|
+
},
|
|
315
322
|
},
|
|
316
323
|
required: [],
|
|
317
324
|
};
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SdkHttpOperation,
|
|
3
|
+
SdkModelPropertyType,
|
|
4
|
+
SdkModelType,
|
|
5
|
+
SdkServiceMethod,
|
|
6
|
+
SdkType,
|
|
7
|
+
} from "@azure-tools/typespec-client-generator-core";
|
|
8
|
+
import { UsageFlags } from "@azure-tools/typespec-client-generator-core";
|
|
9
|
+
import {
|
|
10
|
+
Visibility,
|
|
11
|
+
getVisibilitySuffix,
|
|
12
|
+
isVisible,
|
|
13
|
+
resolveRequestVisibility,
|
|
14
|
+
} from "@typespec/http";
|
|
15
|
+
|
|
16
|
+
import { getAllOperationsFromClient } from "../../framework/hooks/sdk-types.js";
|
|
17
|
+
import type { SdkContext } from "../../utils/interfaces.js";
|
|
18
|
+
import { getDirectSubtypes } from "./type-helpers.js";
|
|
19
|
+
|
|
20
|
+
const responseUsageFlags =
|
|
21
|
+
UsageFlags.Output |
|
|
22
|
+
UsageFlags.Exception |
|
|
23
|
+
UsageFlags.LroInitial |
|
|
24
|
+
UsageFlags.LroPolling |
|
|
25
|
+
UsageFlags.LroFinalEnvelope;
|
|
26
|
+
|
|
27
|
+
interface SplitState {
|
|
28
|
+
context: SdkContext;
|
|
29
|
+
/**
|
|
30
|
+
* Every reachable `(model, visibility)` pair in the write-body graph, keyed by
|
|
31
|
+
* an emitter-local model identity plus visibility.
|
|
32
|
+
*/
|
|
33
|
+
nodes: Map<string, ModelNode>;
|
|
34
|
+
/**
|
|
35
|
+
* The write clone materialized for each node that `needsClone`, keyed the same
|
|
36
|
+
* way as `nodes`.
|
|
37
|
+
*/
|
|
38
|
+
cloneByKey: Map<string, SdkModelType>;
|
|
39
|
+
/** Stable emitter-local IDs preserving `SdkModelType` object identity. */
|
|
40
|
+
modelIds: WeakMap<SdkModelType, number>;
|
|
41
|
+
nextModelId: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A `(model, visibility)` vertex in the write-body reference graph. */
|
|
45
|
+
interface ModelNode {
|
|
46
|
+
model: SdkModelType;
|
|
47
|
+
visibility: Visibility;
|
|
48
|
+
/**
|
|
49
|
+
* Keys of referenced nodes. Discriminated hierarchy links are bidirectional
|
|
50
|
+
* so the whole hierarchy is projected together.
|
|
51
|
+
*/
|
|
52
|
+
refKeys: Set<string>;
|
|
53
|
+
/** True if the model's own properties differ between `visibility` and Read. */
|
|
54
|
+
ownShapeDiffers: boolean;
|
|
55
|
+
/** True if this node can reach any shape-differing node. */
|
|
56
|
+
needsClone: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Projects request models whose write graph differs from their read graph.
|
|
61
|
+
* This must run before `visitPackageTypes` builds the emit queue.
|
|
62
|
+
*/
|
|
63
|
+
export function applyVisibilityModelSplit(context: SdkContext): void {
|
|
64
|
+
if (!context.emitterOptions?.experimentalSplitModelsByVisibility) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const state = createSplitState(context);
|
|
69
|
+
const methods = context.sdkPackage.clients.flatMap((client) =>
|
|
70
|
+
getAllOperationsFromClient(client),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Phase 1 — collect the reachable `(model, visibility)` reference graph.
|
|
74
|
+
for (const method of methods) {
|
|
75
|
+
collectMethodRoots(state, method);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Phase 2 — mark models whose write graph differs from their read graph.
|
|
79
|
+
markNodesNeedingClones(state);
|
|
80
|
+
|
|
81
|
+
// Phase 3 — materialize projected models and wire their references.
|
|
82
|
+
buildClones(state);
|
|
83
|
+
|
|
84
|
+
// Phase 4 — project the original response models to the canonical Read view.
|
|
85
|
+
projectResponseModelsToRead(state);
|
|
86
|
+
|
|
87
|
+
// Phase 5 — repoint operation parameters and payloads to projected models.
|
|
88
|
+
for (const method of methods) {
|
|
89
|
+
repointMethodBody(state, method);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Register projected models for the subsequent emit queue traversal.
|
|
93
|
+
for (const clone of state.cloneByKey.values()) {
|
|
94
|
+
context.sdkPackage.models.push(clone);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Creates the graph state using Read as the canonical model visibility. */
|
|
99
|
+
function createSplitState(context: SdkContext): SplitState {
|
|
100
|
+
return {
|
|
101
|
+
context,
|
|
102
|
+
nodes: new Map<string, ModelNode>(),
|
|
103
|
+
cloneByKey: new Map<string, SdkModelType>(),
|
|
104
|
+
modelIds: new WeakMap<SdkModelType, number>(),
|
|
105
|
+
nextModelId: 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Stable key identifying a `(model, visibility)` node (and its clone). */
|
|
110
|
+
function nodeKeyFor(state: SplitState, model: SdkModelType, visibility: Visibility): string {
|
|
111
|
+
let modelId = state.modelIds.get(model);
|
|
112
|
+
if (modelId === undefined) {
|
|
113
|
+
modelId = state.nextModelId++;
|
|
114
|
+
state.modelIds.set(model, modelId);
|
|
115
|
+
}
|
|
116
|
+
return `${modelId}|${visibility}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Collects the write-body roots of a single operation. The root of each
|
|
121
|
+
* `methodParameterSegments` entry is the client-method parameter.
|
|
122
|
+
*/
|
|
123
|
+
function collectMethodRoots(state: SplitState, method: SdkServiceMethod<SdkHttpOperation>): void {
|
|
124
|
+
const operation = method.operation;
|
|
125
|
+
const bodyParam = operation?.bodyParam;
|
|
126
|
+
if (!bodyParam) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const visibility = resolveRequestVisibility(
|
|
131
|
+
state.context.program,
|
|
132
|
+
operation.__raw.operation,
|
|
133
|
+
operation.verb,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
for (const segment of bodyParam.methodParameterSegments) {
|
|
137
|
+
const methodParam = segment[0];
|
|
138
|
+
if (!methodParam) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
collectReferencedModels(state, methodParam.type, visibility, new Set<string>());
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Cycle-safe DFS that records a `(model, visibility)` node and its outgoing
|
|
147
|
+
* references. Nodes are registered before recursion so cyclic graphs terminate.
|
|
148
|
+
*/
|
|
149
|
+
function collectNode(state: SplitState, model: SdkModelType, visibility: Visibility): string {
|
|
150
|
+
const key = nodeKeyFor(state, model, visibility);
|
|
151
|
+
if (state.nodes.has(key)) {
|
|
152
|
+
return key;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const node: ModelNode = {
|
|
156
|
+
model,
|
|
157
|
+
visibility,
|
|
158
|
+
refKeys: new Set<string>(),
|
|
159
|
+
ownShapeDiffers: false,
|
|
160
|
+
needsClone: false,
|
|
161
|
+
};
|
|
162
|
+
// Seed before recursing so cycles terminate at the already-seeded node.
|
|
163
|
+
state.nodes.set(key, node);
|
|
164
|
+
|
|
165
|
+
for (const property of model.properties) {
|
|
166
|
+
if (property.kind === "property" && property.__raw) {
|
|
167
|
+
const visibleInWrite = isVisible(state.context.program, property.__raw, visibility);
|
|
168
|
+
const visibleInRead = isVisible(state.context.program, property.__raw, Visibility.Read);
|
|
169
|
+
if (visibleInWrite !== visibleInRead) {
|
|
170
|
+
node.ownShapeDiffers = true;
|
|
171
|
+
}
|
|
172
|
+
if (!visibleInWrite) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (property.kind === "property") {
|
|
178
|
+
collectReferencedModels(state, property.type, visibility, node.refKeys);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (model.additionalProperties) {
|
|
183
|
+
collectReferencedModels(state, model.additionalProperties, visibility, node.refKeys);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (model.baseModel) {
|
|
187
|
+
node.refKeys.add(collectNode(state, model.baseModel, visibility));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Link base and subtypes bidirectionally so the projected hierarchy stays together.
|
|
191
|
+
if (findDiscriminatedRoot(model)) {
|
|
192
|
+
for (const neighbor of discriminatedNeighbors(model)) {
|
|
193
|
+
const neighborKey = collectNode(state, neighbor, visibility);
|
|
194
|
+
node.refKeys.add(neighborKey);
|
|
195
|
+
state.nodes.get(neighborKey)!.refKeys.add(key);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return key;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Collects model references nested in supported container types. */
|
|
203
|
+
function collectReferencedModels(
|
|
204
|
+
state: SplitState,
|
|
205
|
+
type: SdkType,
|
|
206
|
+
visibility: Visibility,
|
|
207
|
+
refKeys: Set<string>,
|
|
208
|
+
): void {
|
|
209
|
+
if (type.kind === "model") {
|
|
210
|
+
refKeys.add(collectNode(state, type, visibility));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
switch (type.kind) {
|
|
214
|
+
case "array":
|
|
215
|
+
case "dict":
|
|
216
|
+
collectReferencedModels(state, type.valueType, visibility, refKeys);
|
|
217
|
+
break;
|
|
218
|
+
case "nullable":
|
|
219
|
+
collectReferencedModels(state, type.type, visibility, refKeys);
|
|
220
|
+
break;
|
|
221
|
+
case "tuple":
|
|
222
|
+
for (const valueType of type.valueTypes) {
|
|
223
|
+
collectReferencedModels(state, valueType, visibility, refKeys);
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
case "union":
|
|
227
|
+
for (const variantType of type.variantTypes) {
|
|
228
|
+
collectReferencedModels(state, variantType, visibility, refKeys);
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The base model and direct subtypes that share a node's discriminated tree. */
|
|
235
|
+
function discriminatedNeighbors(model: SdkModelType): SdkModelType[] {
|
|
236
|
+
const neighbors: SdkModelType[] = [];
|
|
237
|
+
if (model.baseModel) {
|
|
238
|
+
neighbors.push(model.baseModel);
|
|
239
|
+
}
|
|
240
|
+
neighbors.push(...getDirectSubtypes(model));
|
|
241
|
+
return neighbors;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Walks up the inheritance chain and returns the top-most ancestor that
|
|
246
|
+
* declares a discriminator.
|
|
247
|
+
*/
|
|
248
|
+
function findDiscriminatedRoot(model: SdkModelType): SdkModelType | undefined {
|
|
249
|
+
let root: SdkModelType | undefined;
|
|
250
|
+
let current: SdkModelType | undefined = model;
|
|
251
|
+
while (current) {
|
|
252
|
+
if (current.discriminatedSubtypes) {
|
|
253
|
+
root = current;
|
|
254
|
+
}
|
|
255
|
+
current = current.baseModel;
|
|
256
|
+
}
|
|
257
|
+
return root;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Marks nodes whose write graph differs from the read graph by
|
|
262
|
+
* reverse-propagating from nodes whose own property membership differs.
|
|
263
|
+
*/
|
|
264
|
+
function markNodesNeedingClones(state: SplitState): void {
|
|
265
|
+
// Reverse edges propagate a nested difference back to every parent.
|
|
266
|
+
const predecessors = new Map<string, string[]>();
|
|
267
|
+
for (const [key, node] of state.nodes) {
|
|
268
|
+
for (const referencedKey of node.refKeys) {
|
|
269
|
+
let referrers = predecessors.get(referencedKey);
|
|
270
|
+
if (!referrers) {
|
|
271
|
+
referrers = [];
|
|
272
|
+
predecessors.set(referencedKey, referrers);
|
|
273
|
+
}
|
|
274
|
+
referrers.push(key);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const queue: string[] = [];
|
|
279
|
+
for (const [key, node] of state.nodes) {
|
|
280
|
+
if (node.ownShapeDiffers) {
|
|
281
|
+
node.needsClone = true;
|
|
282
|
+
queue.push(key);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
while (queue.length > 0) {
|
|
286
|
+
const key = queue.shift()!;
|
|
287
|
+
for (const predecessorKey of predecessors.get(key) ?? []) {
|
|
288
|
+
const predecessor = state.nodes.get(predecessorKey)!;
|
|
289
|
+
if (!predecessor.needsClone) {
|
|
290
|
+
predecessor.needsClone = true;
|
|
291
|
+
queue.push(predecessorKey);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Creates projected model shells, then wires properties and inheritance after
|
|
299
|
+
* every shell exists. This ordering lets forward, cyclic, and mutually recursive
|
|
300
|
+
* references resolve to projected models instead of the original read models.
|
|
301
|
+
*/
|
|
302
|
+
function buildClones(state: SplitState): void {
|
|
303
|
+
// Pass 1: create every shell before wiring references.
|
|
304
|
+
for (const [key, node] of state.nodes) {
|
|
305
|
+
if (!node.needsClone) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const clone: SdkModelType = {
|
|
309
|
+
...node.model,
|
|
310
|
+
name: `${node.model.name}${getVisibilitySuffix(node.visibility, Visibility.Read)}`,
|
|
311
|
+
properties: [],
|
|
312
|
+
// Write projections are request-only: preserve serialization flags, force Input,
|
|
313
|
+
// and remove inherited response roles.
|
|
314
|
+
usage: (node.model.usage | UsageFlags.Input) & ~responseUsageFlags,
|
|
315
|
+
};
|
|
316
|
+
state.cloneByKey.set(key, clone);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Pass 2: remove dropped properties and wire surviving references.
|
|
320
|
+
for (const [key, node] of state.nodes) {
|
|
321
|
+
const clone = state.cloneByKey.get(key);
|
|
322
|
+
if (!clone) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const { model, visibility } = node;
|
|
326
|
+
|
|
327
|
+
const properties: SdkModelPropertyType[] = [];
|
|
328
|
+
for (const property of model.properties) {
|
|
329
|
+
if (
|
|
330
|
+
property.kind === "property" &&
|
|
331
|
+
property.__raw &&
|
|
332
|
+
!isVisible(state.context.program, property.__raw, visibility)
|
|
333
|
+
) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
let wiredProperty = property;
|
|
337
|
+
if (property.kind === "property") {
|
|
338
|
+
const projectedType = projectTypeReferences(state, property.type, visibility);
|
|
339
|
+
if (projectedType !== property.type) {
|
|
340
|
+
wiredProperty = { ...property, type: projectedType };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
properties.push(wiredProperty);
|
|
344
|
+
}
|
|
345
|
+
clone.properties = properties;
|
|
346
|
+
|
|
347
|
+
if (model.additionalProperties) {
|
|
348
|
+
clone.additionalProperties = projectTypeReferences(
|
|
349
|
+
state,
|
|
350
|
+
model.additionalProperties,
|
|
351
|
+
visibility,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (model.baseModel) {
|
|
356
|
+
clone.baseModel =
|
|
357
|
+
state.cloneByKey.get(nodeKeyFor(state, model.baseModel, visibility)) ?? model.baseModel;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (findDiscriminatedRoot(model)) {
|
|
361
|
+
if (model.discriminatedSubtypes) {
|
|
362
|
+
clone.discriminatedSubtypes = Object.fromEntries(
|
|
363
|
+
Object.entries(model.discriminatedSubtypes).map(([discriminatorValue, subtype]) => [
|
|
364
|
+
discriminatorValue,
|
|
365
|
+
state.cloneByKey.get(nodeKeyFor(state, subtype, visibility)) ?? subtype,
|
|
366
|
+
]),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Projects response-used models to the canonical Read visibility. */
|
|
374
|
+
function projectResponseModelsToRead(state: SplitState): void {
|
|
375
|
+
for (const model of state.context.sdkPackage.models) {
|
|
376
|
+
if ((model.usage & responseUsageFlags) === 0) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
model.properties = model.properties.filter(
|
|
380
|
+
(property) =>
|
|
381
|
+
property.kind !== "property" ||
|
|
382
|
+
!property.__raw ||
|
|
383
|
+
isVisible(state.context.program, property.__raw, Visibility.Read),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Replaces model references nested in supported container types. */
|
|
389
|
+
function projectTypeReferences(state: SplitState, type: SdkType, visibility: Visibility): SdkType {
|
|
390
|
+
if (type.kind === "model") {
|
|
391
|
+
return state.cloneByKey.get(nodeKeyFor(state, type, visibility)) ?? type;
|
|
392
|
+
}
|
|
393
|
+
switch (type.kind) {
|
|
394
|
+
case "array":
|
|
395
|
+
case "dict": {
|
|
396
|
+
const projectedValueType = projectTypeReferences(state, type.valueType, visibility);
|
|
397
|
+
return projectedValueType === type.valueType
|
|
398
|
+
? type
|
|
399
|
+
: { ...type, valueType: projectedValueType };
|
|
400
|
+
}
|
|
401
|
+
case "nullable": {
|
|
402
|
+
const projectedType = projectTypeReferences(state, type.type, visibility);
|
|
403
|
+
return projectedType === type.type ? type : { ...type, type: projectedType };
|
|
404
|
+
}
|
|
405
|
+
case "tuple": {
|
|
406
|
+
const projectedValueTypes = type.valueTypes.map((valueType) =>
|
|
407
|
+
projectTypeReferences(state, valueType, visibility),
|
|
408
|
+
);
|
|
409
|
+
return projectedValueTypes.every((valueType, index) => valueType === type.valueTypes[index])
|
|
410
|
+
? type
|
|
411
|
+
: { ...type, valueTypes: projectedValueTypes };
|
|
412
|
+
}
|
|
413
|
+
case "union": {
|
|
414
|
+
const projectedVariantTypes = type.variantTypes.map((variantType) =>
|
|
415
|
+
projectTypeReferences(state, variantType, visibility),
|
|
416
|
+
);
|
|
417
|
+
return projectedVariantTypes.every(
|
|
418
|
+
(variantType, index) => variantType === type.variantTypes[index],
|
|
419
|
+
)
|
|
420
|
+
? type
|
|
421
|
+
: { ...type, variantTypes: projectedVariantTypes };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return type;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Repoints client-method parameters and their HTTP payload representation to
|
|
429
|
+
* projected models.
|
|
430
|
+
*/
|
|
431
|
+
function repointMethodBody(state: SplitState, method: SdkServiceMethod<SdkHttpOperation>): void {
|
|
432
|
+
const operation = method.operation;
|
|
433
|
+
const bodyParam = operation?.bodyParam;
|
|
434
|
+
if (!bodyParam) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const visibility = resolveRequestVisibility(
|
|
439
|
+
state.context.program,
|
|
440
|
+
operation.__raw.operation,
|
|
441
|
+
operation.verb,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
for (const segment of bodyParam.methodParameterSegments) {
|
|
445
|
+
const methodParam = segment[0];
|
|
446
|
+
if (!methodParam) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const originalType = methodParam.type;
|
|
451
|
+
const projectedType = projectTypeReferences(state, originalType, visibility);
|
|
452
|
+
if (projectedType === originalType) {
|
|
453
|
+
// The write view is identical to the read model (collapse).
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
methodParam.type = projectedType;
|
|
458
|
+
|
|
459
|
+
if (originalType === bodyParam.type) {
|
|
460
|
+
// A non-spread body uses the method parameter model as the payload model.
|
|
461
|
+
bodyParam.type = projectedType;
|
|
462
|
+
} else if (bodyParam.type.kind === "model") {
|
|
463
|
+
// A spread body uses a synthesized wrapper serialized property by property.
|
|
464
|
+
for (const bodyProperty of bodyParam.type.properties) {
|
|
465
|
+
if (bodyProperty.type === originalType) {
|
|
466
|
+
bodyProperty.type = projectedType;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -57,6 +57,8 @@ function extractClientOptions(
|
|
|
57
57
|
const ignoreNullableOnOptional = getIgnoreNullableOnOptional(emitterOptions);
|
|
58
58
|
const wrapNonModelReturn = getWrapNonModelReturn(emitterOptions);
|
|
59
59
|
const isMultiService = (dpgContext.allServiceNamespaces?.length ?? 0) > 1;
|
|
60
|
+
const experimentalSplitModelsByVisibility =
|
|
61
|
+
emitterOptions["experimental-split-models-by-visibility"] === true;
|
|
60
62
|
|
|
61
63
|
return {
|
|
62
64
|
...credentialInfo,
|
|
@@ -85,6 +87,7 @@ function extractClientOptions(
|
|
|
85
87
|
enableStorageCompat,
|
|
86
88
|
treatUnknownAsRecord,
|
|
87
89
|
generateReactNativeTarget,
|
|
90
|
+
experimentalSplitModelsByVisibility,
|
|
88
91
|
};
|
|
89
92
|
}
|
|
90
93
|
|