@nxgt/typespec 0.13.1 → 0.14.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.
- package/README.md +36 -1
- package/dist/components.d.ts +17 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/document-text.d.ts +7 -0
- package/dist/document-text.d.ts.map +1 -0
- package/dist/emitter.d.ts +3 -1
- package/dist/emitter.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +501 -38
- package/dist/index.js.map +13 -7
- package/dist/lib.d.ts +25 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/marks.d.ts +32 -0
- package/dist/marks.d.ts.map +1 -0
- package/dist/query-operations.d.ts +1 -6
- package/dist/query-operations.d.ts.map +1 -1
- package/dist/response-marks.d.ts +10 -0
- package/dist/response-marks.d.ts.map +1 -0
- package/dist/server-variables.d.ts +10 -0
- package/dist/server-variables.d.ts.map +1 -0
- package/dist/shared-components.d.ts +11 -0
- package/dist/shared-components.d.ts.map +1 -0
- package/docs/README.md +1 -1
- package/docs/guide/emitter.md +147 -1
- package/docs/guide/scalars.md +6 -3
- package/docs/roadmap.md +18 -3
- package/docs/troubleshooting.md +105 -7
- package/lib/components.tsp +65 -0
- package/lib/main.tsp +1 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
function eachHttpOperation(program, visit) {
|
|
7
|
-
const [services] = getAllHttpServices(program);
|
|
8
|
-
const visited = new Set;
|
|
9
|
-
for (const service of services) {
|
|
10
|
-
for (const operation of service.operations) {
|
|
11
|
-
if (visited.has(operation.operation))
|
|
12
|
-
continue;
|
|
13
|
-
visited.add(operation.operation);
|
|
14
|
-
visit(operation);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
1
|
+
// src/components.ts
|
|
2
|
+
import {
|
|
3
|
+
getTypeName
|
|
4
|
+
} from "@typespec/compiler";
|
|
5
|
+
import { setExtension } from "@typespec/openapi";
|
|
18
6
|
|
|
19
7
|
// src/lib.ts
|
|
20
8
|
import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler";
|
|
@@ -57,6 +45,54 @@ var $lib = createTypeSpecLibrary({
|
|
|
57
45
|
default: paramMessage`${"operation"} is marked @queryMethod: emit with @nxgt/typespec, which writes it as a QUERY; @typespec/openapi3 would write a POST.`
|
|
58
46
|
}
|
|
59
47
|
},
|
|
48
|
+
"shared-component-conflict": {
|
|
49
|
+
severity: "error",
|
|
50
|
+
messages: {
|
|
51
|
+
default: paramMessage`Two shared ${"kind"} named ${"name"} differ: a component is emitted once. Give each its own name, or make them the same.`
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"path-server-conflict": {
|
|
55
|
+
severity: "error",
|
|
56
|
+
messages: {
|
|
57
|
+
default: paramMessage`Two @pathServer of ${"path"} give the server ${"url"} a description or variables that differ. Give the path one definition of it.`
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"path-server-missing-variable": {
|
|
61
|
+
severity: "error",
|
|
62
|
+
messages: {
|
|
63
|
+
default: paramMessage`The server URL ${"url"} of ${"operation"} has {${"variable"}}, which is not a property of its parameters. Add the variable, or take the braces out.`
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"path-server-invalid-variable": {
|
|
67
|
+
severity: "error",
|
|
68
|
+
messages: {
|
|
69
|
+
default: paramMessage`The variable ${"variable"} of the @pathServer of ${"operation"} is not a string, a string enum or a union of strings: an OpenAPI server variable is a string.`
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"shared-header-misplaced": {
|
|
73
|
+
severity: "error",
|
|
74
|
+
messages: {
|
|
75
|
+
default: paramMessage`${"name"} is marked @sharedHeader outside a response header: it applies to a @header of a reply only, not to a request header or a body property.`
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"shared-response-merged": {
|
|
79
|
+
severity: "error",
|
|
80
|
+
messages: {
|
|
81
|
+
default: paramMessage`Operations sharing the route ${"verb"} ${"path"} reply ${"status"} with different responses, one of them a @sharedResponse: @sharedRoute merges them into one reply, which cannot be a component. Give them the same shared response, or none.`
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"invalid-component-name": {
|
|
85
|
+
severity: "error",
|
|
86
|
+
messages: {
|
|
87
|
+
default: paramMessage`${"name"} is not a name OpenAPI allows for a component: use letters, digits, '.', '_' and '-'.`
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"shared-needs-nxgt-emitter": {
|
|
91
|
+
severity: "error",
|
|
92
|
+
messages: {
|
|
93
|
+
default: paramMessage`${"target"} is marked ${"decorator"}: emit with @nxgt/typespec, which writes it; @typespec/openapi3 would write it inline or drop it.`
|
|
94
|
+
}
|
|
95
|
+
},
|
|
60
96
|
"openapi3-missing": {
|
|
61
97
|
severity: "error",
|
|
62
98
|
messages: {
|
|
@@ -81,6 +117,15 @@ var $lib = createTypeSpecLibrary({
|
|
|
81
117
|
queryMethod: {
|
|
82
118
|
description: "The operations marked with @queryMethod"
|
|
83
119
|
},
|
|
120
|
+
sharedResponses: {
|
|
121
|
+
description: "The models marked with @sharedResponse, to their component name"
|
|
122
|
+
},
|
|
123
|
+
sharedHeaders: {
|
|
124
|
+
description: "The properties marked with @sharedHeader"
|
|
125
|
+
},
|
|
126
|
+
pathServers: {
|
|
127
|
+
description: "The operations marked with @pathServer, to their servers"
|
|
128
|
+
},
|
|
84
129
|
operationIds: {
|
|
85
130
|
description: "The interfaces and namespaces marked with @operationIds"
|
|
86
131
|
},
|
|
@@ -90,6 +135,161 @@ var $lib = createTypeSpecLibrary({
|
|
|
90
135
|
}
|
|
91
136
|
});
|
|
92
137
|
|
|
138
|
+
// src/marks.ts
|
|
139
|
+
var COMPONENT_EXTENSION = "x-nxgt-component";
|
|
140
|
+
var SERVERS_EXTENSION = "x-nxgt-servers-";
|
|
141
|
+
var RESPONSES_EXTENSION = "x-nxgt-responses-";
|
|
142
|
+
var marksOf = (object, prefix) => Object.keys(object).filter((key) => key.startsWith(prefix));
|
|
143
|
+
var isMark = (key) => key === COMPONENT_EXTENSION || key.startsWith(SERVERS_EXTENSION) || key.startsWith(RESPONSES_EXTENSION);
|
|
144
|
+
|
|
145
|
+
// src/server-variables.ts
|
|
146
|
+
import {
|
|
147
|
+
getDoc,
|
|
148
|
+
serializeValueAsJson
|
|
149
|
+
} from "@typespec/compiler";
|
|
150
|
+
import { $ } from "@typespec/compiler/typekit";
|
|
151
|
+
function variableEnum(type) {
|
|
152
|
+
switch (type.kind) {
|
|
153
|
+
case "String":
|
|
154
|
+
return [type.value];
|
|
155
|
+
case "Enum":
|
|
156
|
+
return [...type.members.values()].map((member) => String(member.value ?? member.name));
|
|
157
|
+
case "Union": {
|
|
158
|
+
const values = [...type.variants.values()].map((variant) => variableEnum(variant.type));
|
|
159
|
+
return values.every((value) => value !== undefined) ? values.flat() : undefined;
|
|
160
|
+
}
|
|
161
|
+
default:
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function isStringType(program, type) {
|
|
166
|
+
switch (type.kind) {
|
|
167
|
+
case "String":
|
|
168
|
+
return true;
|
|
169
|
+
case "Scalar":
|
|
170
|
+
return $(program).type.isAssignableTo(type, $(program).builtin.string, type);
|
|
171
|
+
case "Enum":
|
|
172
|
+
return [...type.members.values()].every((member) => member.value === undefined || typeof member.value === "string");
|
|
173
|
+
case "Union":
|
|
174
|
+
return [...type.variants.values()].every((variant) => isStringType(program, variant.type));
|
|
175
|
+
default:
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function serverVariables(program, target, parameters) {
|
|
180
|
+
const variables = {};
|
|
181
|
+
for (const [name, property] of parameters.properties) {
|
|
182
|
+
if (!isStringType(program, property.type)) {
|
|
183
|
+
$lib.reportDiagnostic(program, {
|
|
184
|
+
code: "path-server-invalid-variable",
|
|
185
|
+
format: { operation: target.name, variable: name },
|
|
186
|
+
target: property
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const description = getDoc(program, property);
|
|
191
|
+
const values = variableEnum(property.type);
|
|
192
|
+
variables[name] = {
|
|
193
|
+
default: property.defaultValue === undefined ? "" : String(serializeValueAsJson(program, property.defaultValue, property.type)),
|
|
194
|
+
...description === undefined ? {} : { description },
|
|
195
|
+
...values === undefined ? {} : { enum: values }
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return variables;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/components.ts
|
|
202
|
+
var COMPONENT_NAME = /^[a-zA-Z0-9._-]+$/;
|
|
203
|
+
function checkedName(program, target, name) {
|
|
204
|
+
if (COMPONENT_NAME.test(name))
|
|
205
|
+
return name;
|
|
206
|
+
$lib.reportDiagnostic(program, {
|
|
207
|
+
code: "invalid-component-name",
|
|
208
|
+
format: { name },
|
|
209
|
+
target
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
function sharedResponse(context, target, name) {
|
|
214
|
+
const { program } = context;
|
|
215
|
+
const checked = checkedName(program, target, name ?? target.name);
|
|
216
|
+
if (checked === undefined)
|
|
217
|
+
return;
|
|
218
|
+
program.stateMap($lib.stateKeys.sharedResponses).set(target, checked);
|
|
219
|
+
}
|
|
220
|
+
function sharedHeader(context, target, name) {
|
|
221
|
+
const { program } = context;
|
|
222
|
+
const checked = checkedName(program, target, name ?? target.name);
|
|
223
|
+
if (checked === undefined)
|
|
224
|
+
return;
|
|
225
|
+
program.stateSet($lib.stateKeys.sharedHeaders).add(target);
|
|
226
|
+
setExtension(program, target, COMPONENT_EXTENSION, checked);
|
|
227
|
+
}
|
|
228
|
+
function pathServer(context, target, url, description, parameters) {
|
|
229
|
+
const { program } = context;
|
|
230
|
+
const variables = parameters === undefined ? {} : serverVariables(program, target, parameters);
|
|
231
|
+
for (const [, variable] of url.matchAll(/\{([^}]+)\}/g)) {
|
|
232
|
+
if (variable !== undefined && !(variable in variables)) {
|
|
233
|
+
if (parameters?.properties.has(variable))
|
|
234
|
+
continue;
|
|
235
|
+
$lib.reportDiagnostic(program, {
|
|
236
|
+
code: "path-server-missing-variable",
|
|
237
|
+
format: { operation: target.name, url, variable },
|
|
238
|
+
target: context.getArgumentTarget(0) ?? target
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const map = program.stateMap($lib.stateKeys.pathServers);
|
|
243
|
+
const servers = map.get(target) ?? [];
|
|
244
|
+
servers.push({
|
|
245
|
+
url,
|
|
246
|
+
...description === undefined ? {} : { description },
|
|
247
|
+
...Object.keys(variables).length === 0 ? {} : { variables }
|
|
248
|
+
});
|
|
249
|
+
map.set(target, servers);
|
|
250
|
+
setExtension(program, target, `${SERVERS_EXTENSION}${getTypeName(target)}`, servers);
|
|
251
|
+
}
|
|
252
|
+
function validateComponents(program) {
|
|
253
|
+
if (!(program.compilerOptions.emit ?? []).includes("@typespec/openapi3")) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const marked = [];
|
|
257
|
+
for (const model of program.stateMap($lib.stateKeys.sharedResponses).keys()) {
|
|
258
|
+
marked.push([model.name, "@sharedResponse"]);
|
|
259
|
+
}
|
|
260
|
+
for (const property of program.stateSet($lib.stateKeys.sharedHeaders)) {
|
|
261
|
+
marked.push([property.name, "@sharedHeader"]);
|
|
262
|
+
}
|
|
263
|
+
for (const operation of program.stateMap($lib.stateKeys.pathServers).keys()) {
|
|
264
|
+
marked.push([operation.name, "@pathServer"]);
|
|
265
|
+
}
|
|
266
|
+
for (const [target, decorator] of marked) {
|
|
267
|
+
$lib.reportDiagnostic(program, {
|
|
268
|
+
code: "shared-needs-nxgt-emitter",
|
|
269
|
+
format: { target, decorator },
|
|
270
|
+
target: program.getGlobalNamespaceType()
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/one-reply-per-status.ts
|
|
276
|
+
import { isVoidType } from "@typespec/compiler";
|
|
277
|
+
|
|
278
|
+
// src/http-operations.ts
|
|
279
|
+
import { getAllHttpServices } from "@typespec/http";
|
|
280
|
+
function eachHttpOperation(program, visit) {
|
|
281
|
+
const [services] = getAllHttpServices(program);
|
|
282
|
+
const visited = new Set;
|
|
283
|
+
for (const service of services) {
|
|
284
|
+
for (const operation of service.operations) {
|
|
285
|
+
if (visited.has(operation.operation))
|
|
286
|
+
continue;
|
|
287
|
+
visited.add(operation.operation);
|
|
288
|
+
visit(operation);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
93
293
|
// src/one-reply-per-status.ts
|
|
94
294
|
function statusOf(code) {
|
|
95
295
|
return typeof code === "object" ? `${code.start}-${code.end}` : `${code}`;
|
|
@@ -348,11 +548,11 @@ function validateOperationIds(program) {
|
|
|
348
548
|
}
|
|
349
549
|
|
|
350
550
|
// src/query-method.ts
|
|
351
|
-
import { setExtension } from "@typespec/openapi";
|
|
551
|
+
import { setExtension as setExtension2 } from "@typespec/openapi";
|
|
352
552
|
function queryMethod(context, target) {
|
|
353
553
|
const { program } = context;
|
|
354
554
|
program.stateSet($lib.stateKeys.queryMethod).add(target);
|
|
355
|
-
|
|
555
|
+
setExtension2(program, target, "x-nxgt-method", "query");
|
|
356
556
|
}
|
|
357
557
|
function isQueryMethod(program, operation) {
|
|
358
558
|
return program.stateSet($lib.stateKeys.queryMethod).has(operation);
|
|
@@ -418,8 +618,28 @@ import {
|
|
|
418
618
|
NoTarget
|
|
419
619
|
} from "@typespec/compiler";
|
|
420
620
|
|
|
421
|
-
// src/
|
|
621
|
+
// src/document-text.ts
|
|
422
622
|
import { parse, stringify } from "yaml";
|
|
623
|
+
function rewriteDocument(content, rewrite) {
|
|
624
|
+
const json = content.trimStart().startsWith("{");
|
|
625
|
+
const document = json ? JSON.parse(content) : parse(content);
|
|
626
|
+
if (typeof document !== "object" || document === null)
|
|
627
|
+
return content;
|
|
628
|
+
if (!rewrite(document))
|
|
629
|
+
return content;
|
|
630
|
+
const text = json ? `${JSON.stringify(document, null, 2)}
|
|
631
|
+
` : stringify(document, {
|
|
632
|
+
singleQuote: true,
|
|
633
|
+
aliasDuplicateObjects: false,
|
|
634
|
+
lineWidth: 0,
|
|
635
|
+
compat: "yaml-1.1"
|
|
636
|
+
});
|
|
637
|
+
return content.includes(`\r
|
|
638
|
+
`) ? text.replace(/\n/g, `\r
|
|
639
|
+
`) : text;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/query-operations.ts
|
|
423
643
|
function moveQueries(document) {
|
|
424
644
|
if (!String(document["openapi"]).startsWith("3.2"))
|
|
425
645
|
return false;
|
|
@@ -439,22 +659,227 @@ function moveQueries(document) {
|
|
|
439
659
|
return moved;
|
|
440
660
|
}
|
|
441
661
|
function withQueryOperations(content) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
662
|
+
return rewriteDocument(content, moveQueries);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/response-marks.ts
|
|
666
|
+
import { getTypeName as getTypeName2 } from "@typespec/compiler";
|
|
667
|
+
import { getAllHttpServices as getAllHttpServices2 } from "@typespec/http";
|
|
668
|
+
import { setExtension as setExtension3 } from "@typespec/openapi";
|
|
669
|
+
function statusKey(status) {
|
|
670
|
+
if (typeof status === "number")
|
|
671
|
+
return String(status);
|
|
672
|
+
if (status === "*")
|
|
673
|
+
return "default";
|
|
674
|
+
const hundred = Math.floor(status.start / 100);
|
|
675
|
+
return status.start === hundred * 100 && status.end === hundred * 100 + 99 ? `${hundred}XX` : undefined;
|
|
676
|
+
}
|
|
677
|
+
function mergedStatuses(program, services) {
|
|
678
|
+
const names = program.stateMap($lib.stateKeys.sharedResponses);
|
|
679
|
+
const groups = new Map;
|
|
680
|
+
const refused = new Set;
|
|
681
|
+
for (const service of services) {
|
|
682
|
+
for (const { operation, path, verb, responses } of service.operations) {
|
|
683
|
+
for (const reply of responses) {
|
|
684
|
+
const status = statusKey(reply.statusCodes);
|
|
685
|
+
if (status === undefined)
|
|
686
|
+
continue;
|
|
687
|
+
const key = `${verb} ${path} ${status}`;
|
|
688
|
+
const held = groups.get(key) ?? new Map;
|
|
689
|
+
held.set(reply.type, operation.name);
|
|
690
|
+
groups.set(key, held);
|
|
691
|
+
const shared = [...held.keys()].some((type) => names.has(type));
|
|
692
|
+
if (held.size > 1 && shared && !refused.has(key)) {
|
|
693
|
+
refused.add(key);
|
|
694
|
+
$lib.reportDiagnostic(program, {
|
|
695
|
+
code: "shared-response-merged",
|
|
696
|
+
format: { status, path, verb: verb.toUpperCase() },
|
|
697
|
+
target: operation
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return refused;
|
|
704
|
+
}
|
|
705
|
+
function markSharedResponses(program) {
|
|
706
|
+
const names = program.stateMap($lib.stateKeys.sharedResponses);
|
|
707
|
+
if (names.size === 0)
|
|
708
|
+
return;
|
|
709
|
+
const [services] = getAllHttpServices2(program);
|
|
710
|
+
const statuses = new Map;
|
|
711
|
+
for (const service of services) {
|
|
712
|
+
for (const { responses } of service.operations) {
|
|
713
|
+
for (const reply of responses) {
|
|
714
|
+
const status = statusKey(reply.statusCodes);
|
|
715
|
+
if (status === undefined || !names.has(reply.type))
|
|
716
|
+
continue;
|
|
717
|
+
statuses.set(reply.type, (statuses.get(reply.type) ?? new Set).add(status));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const merged = mergedStatuses(program, services);
|
|
722
|
+
for (const service of services) {
|
|
723
|
+
for (const { operation, path, verb, responses } of service.operations) {
|
|
724
|
+
const marked = {};
|
|
725
|
+
for (const reply of responses) {
|
|
726
|
+
const name = names.get(reply.type);
|
|
727
|
+
const status = statusKey(reply.statusCodes);
|
|
728
|
+
if (typeof name !== "string" || status === undefined)
|
|
729
|
+
continue;
|
|
730
|
+
if (merged.has(`${verb} ${path} ${status}`))
|
|
731
|
+
continue;
|
|
732
|
+
const several = (statuses.get(reply.type)?.size ?? 0) > 1;
|
|
733
|
+
marked[status] = several ? `${name}${status}` : name;
|
|
734
|
+
}
|
|
735
|
+
if (Object.keys(marked).length > 0) {
|
|
736
|
+
setExtension3(program, operation, `${RESPONSES_EXTENSION}${getTypeName2(operation)}`, marked);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/shared-components.ts
|
|
743
|
+
var isObj = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
744
|
+
function canonical(value) {
|
|
745
|
+
return JSON.stringify(value, (_, held) => typeof held === "object" && held !== null && !Array.isArray(held) ? Object.fromEntries(Object.entries(held).sort(([left], [right]) => left < right ? -1 : 1)) : held);
|
|
746
|
+
}
|
|
747
|
+
function hoist(components, kind, name, value) {
|
|
748
|
+
if (!isObj(components[kind]))
|
|
749
|
+
components[kind] = {};
|
|
750
|
+
const group = components[kind];
|
|
751
|
+
const held = group[name];
|
|
752
|
+
if (held === undefined) {
|
|
753
|
+
group[name] = value;
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
return canonical(held) === canonical(value);
|
|
757
|
+
}
|
|
758
|
+
function* operations(paths) {
|
|
759
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
760
|
+
if (!isObj(item))
|
|
761
|
+
continue;
|
|
762
|
+
for (const operation of Object.values(item)) {
|
|
763
|
+
if (isObj(operation))
|
|
764
|
+
yield [path, item, operation];
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function hoistHeaders(paths, components, problems) {
|
|
769
|
+
let changed = false;
|
|
770
|
+
for (const [, , operation] of operations(paths)) {
|
|
771
|
+
for (const reply of Object.values(operation["responses"] ?? {})) {
|
|
772
|
+
if (!isObj(reply) || !isObj(reply["headers"]))
|
|
773
|
+
continue;
|
|
774
|
+
for (const [key, header] of Object.entries(reply["headers"])) {
|
|
775
|
+
if (!isObj(header))
|
|
776
|
+
continue;
|
|
777
|
+
const name = header[COMPONENT_EXTENSION];
|
|
778
|
+
if (typeof name !== "string")
|
|
779
|
+
continue;
|
|
780
|
+
const { [COMPONENT_EXTENSION]: _, ...bare } = header;
|
|
781
|
+
if (hoist(components, "headers", name, bare)) {
|
|
782
|
+
reply["headers"][key] = { $ref: `#/components/headers/${name}` };
|
|
783
|
+
} else {
|
|
784
|
+
reply["headers"][key] = bare;
|
|
785
|
+
problems.push({ kind: "headers", name });
|
|
786
|
+
}
|
|
787
|
+
changed = true;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return changed;
|
|
792
|
+
}
|
|
793
|
+
function hoistResponses(paths, components, problems) {
|
|
794
|
+
let changed = false;
|
|
795
|
+
for (const [, , operation] of operations(paths)) {
|
|
796
|
+
const replies = operation["responses"];
|
|
797
|
+
if (!isObj(replies))
|
|
798
|
+
continue;
|
|
799
|
+
for (const key of marksOf(operation, RESPONSES_EXTENSION)) {
|
|
800
|
+
const marked = operation[key];
|
|
801
|
+
if (!isObj(marked))
|
|
802
|
+
continue;
|
|
803
|
+
for (const [status, name] of Object.entries(marked)) {
|
|
804
|
+
const reply = replies[status];
|
|
805
|
+
if (typeof name !== "string" || !isObj(reply))
|
|
806
|
+
continue;
|
|
807
|
+
const ref = `#/components/responses/${name}`;
|
|
808
|
+
if (reply["$ref"] === ref)
|
|
809
|
+
continue;
|
|
810
|
+
if (reply["$ref"] !== undefined) {
|
|
811
|
+
problems.push({ kind: "responses", name });
|
|
812
|
+
} else if (hoist(components, "responses", name, reply)) {
|
|
813
|
+
replies[status] = { $ref: ref };
|
|
814
|
+
changed = true;
|
|
815
|
+
} else {
|
|
816
|
+
problems.push({ kind: "responses", name });
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return changed;
|
|
822
|
+
}
|
|
823
|
+
function setServers(paths, problems) {
|
|
824
|
+
const found = new Map;
|
|
825
|
+
for (const [path, , operation] of operations(paths)) {
|
|
826
|
+
const held = found.get(path) ?? [];
|
|
827
|
+
for (const key of marksOf(operation, SERVERS_EXTENSION)) {
|
|
828
|
+
const marked = operation[key];
|
|
829
|
+
if (!Array.isArray(marked))
|
|
830
|
+
continue;
|
|
831
|
+
for (const server of marked) {
|
|
832
|
+
const same = held.find(({ url }) => url === server.url);
|
|
833
|
+
if (same === undefined)
|
|
834
|
+
held.push(server);
|
|
835
|
+
else if (canonical(same) !== canonical(server)) {
|
|
836
|
+
problems.push({ kind: "server", path, url: server.url });
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (held.length > 0)
|
|
841
|
+
found.set(path, held);
|
|
842
|
+
}
|
|
843
|
+
for (const [path, servers] of found) {
|
|
844
|
+
paths[path]["servers"] = servers;
|
|
845
|
+
}
|
|
846
|
+
return found.size > 0;
|
|
847
|
+
}
|
|
848
|
+
function takeMarks(value, left) {
|
|
849
|
+
let taken = false;
|
|
850
|
+
if (Array.isArray(value)) {
|
|
851
|
+
for (const item of value)
|
|
852
|
+
taken = takeMarks(item, left) || taken;
|
|
853
|
+
} else if (isObj(value)) {
|
|
854
|
+
for (const mark of Object.keys(value).filter(isMark)) {
|
|
855
|
+
if (mark === COMPONENT_EXTENSION)
|
|
856
|
+
left.push(String(value[mark]));
|
|
857
|
+
delete value[mark];
|
|
858
|
+
taken = true;
|
|
859
|
+
}
|
|
860
|
+
for (const item of Object.values(value)) {
|
|
861
|
+
taken = takeMarks(item, left) || taken;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return taken;
|
|
865
|
+
}
|
|
866
|
+
function withComponents(content, problems) {
|
|
867
|
+
return rewriteDocument(content, (document) => {
|
|
868
|
+
const paths = document["paths"];
|
|
869
|
+
if (!isObj(paths))
|
|
870
|
+
return false;
|
|
871
|
+
const components = isObj(document["components"]) ? document["components"] : {};
|
|
872
|
+
const headers = hoistHeaders(paths, components, problems);
|
|
873
|
+
const responses = hoistResponses(paths, components, problems);
|
|
874
|
+
setServers(paths, problems);
|
|
875
|
+
if (headers || responses)
|
|
876
|
+
document["components"] = components;
|
|
877
|
+
const misplaced = [];
|
|
878
|
+
const taken = takeMarks(document, misplaced);
|
|
879
|
+
for (const name of misplaced)
|
|
880
|
+
problems.push({ kind: "misplaced", name });
|
|
881
|
+
return taken;
|
|
454
882
|
});
|
|
455
|
-
return content.includes(`\r
|
|
456
|
-
`) ? text.replace(/\n/g, `\r
|
|
457
|
-
`) : text;
|
|
458
883
|
}
|
|
459
884
|
|
|
460
885
|
// src/emitter.ts
|
|
@@ -496,6 +921,34 @@ function queriesFit(context) {
|
|
|
496
921
|
}
|
|
497
922
|
return false;
|
|
498
923
|
}
|
|
924
|
+
function reportProblems(program, problems) {
|
|
925
|
+
const seen = new Set;
|
|
926
|
+
for (const problem of problems) {
|
|
927
|
+
const key = JSON.stringify(problem);
|
|
928
|
+
if (seen.has(key))
|
|
929
|
+
continue;
|
|
930
|
+
seen.add(key);
|
|
931
|
+
if (problem.kind === "server") {
|
|
932
|
+
$lib.reportDiagnostic(program, {
|
|
933
|
+
code: "path-server-conflict",
|
|
934
|
+
format: { path: problem.path, url: problem.url },
|
|
935
|
+
target: NoTarget
|
|
936
|
+
});
|
|
937
|
+
} else if (problem.kind === "misplaced") {
|
|
938
|
+
$lib.reportDiagnostic(program, {
|
|
939
|
+
code: "shared-header-misplaced",
|
|
940
|
+
format: { name: problem.name },
|
|
941
|
+
target: NoTarget
|
|
942
|
+
});
|
|
943
|
+
} else {
|
|
944
|
+
$lib.reportDiagnostic(program, {
|
|
945
|
+
code: "shared-component-conflict",
|
|
946
|
+
format: { kind: problem.kind, name: problem.name },
|
|
947
|
+
target: NoTarget
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
499
952
|
async function $onEmit(context) {
|
|
500
953
|
const openapi3 = await loadOpenAPI3(context);
|
|
501
954
|
if (openapi3 === undefined)
|
|
@@ -509,10 +962,12 @@ async function $onEmit(context) {
|
|
|
509
962
|
if (!queriesFit(context))
|
|
510
963
|
return;
|
|
511
964
|
const host = program.host;
|
|
965
|
+
markSharedResponses(program);
|
|
966
|
+
const problems = [];
|
|
512
967
|
const writing = new Proxy(host, {
|
|
513
968
|
get(target, key) {
|
|
514
969
|
if (key === "writeFile") {
|
|
515
|
-
return (path, content) => target.writeFile(path, withQueryOperations(content));
|
|
970
|
+
return (path, content) => target.writeFile(path, withQueryOperations(withComponents(content, problems)));
|
|
516
971
|
}
|
|
517
972
|
const value = Reflect.get(target, key, target);
|
|
518
973
|
return typeof value === "function" ? value.bind(target) : value;
|
|
@@ -524,6 +979,7 @@ async function $onEmit(context) {
|
|
|
524
979
|
} finally {
|
|
525
980
|
program.host = host;
|
|
526
981
|
}
|
|
982
|
+
reportProblems(program, problems);
|
|
527
983
|
}
|
|
528
984
|
// src/linter.ts
|
|
529
985
|
import { defineLinter } from "@typespec/compiler";
|
|
@@ -650,12 +1106,19 @@ var $linter = defineLinter({
|
|
|
650
1106
|
// src/index.ts
|
|
651
1107
|
function $onValidate(program) {
|
|
652
1108
|
validateQueryMethods(program);
|
|
1109
|
+
validateComponents(program);
|
|
653
1110
|
validateVerbMethods(program);
|
|
654
1111
|
validateOperationIds(program);
|
|
655
1112
|
validateOneReplyPerStatus(program);
|
|
656
1113
|
}
|
|
657
1114
|
var $decorators = {
|
|
658
|
-
Nxgt: {
|
|
1115
|
+
Nxgt: {
|
|
1116
|
+
operationIds,
|
|
1117
|
+
pathServer,
|
|
1118
|
+
queryMethod,
|
|
1119
|
+
sharedHeader,
|
|
1120
|
+
sharedResponse
|
|
1121
|
+
}
|
|
659
1122
|
};
|
|
660
1123
|
export {
|
|
661
1124
|
$decorators,
|
|
@@ -665,5 +1128,5 @@ export {
|
|
|
665
1128
|
$onValidate
|
|
666
1129
|
};
|
|
667
1130
|
|
|
668
|
-
//# debugId=
|
|
1131
|
+
//# debugId=478BF462A5B3465864756E2164756E21
|
|
669
1132
|
//# sourceMappingURL=index.js.map
|