microsoft/typespec

Public

mirrored from https://github.com/microsoft/typespecAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
42d00cef2bd13ec70407ea77d5a0249cdaa3f154

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/http/src/route.ts

273lines · modecode

1import {
2 createDiagnosticCollector,
3 DecoratorContext,
4 DiagnosticResult,
5 Interface,
6 Namespace,
7 Operation,
8 Program,
9 Type,
10 validateDecoratorTarget,
11} from "@typespec/compiler";
12import { createDiagnostic, createStateSymbol, reportDiagnostic } from "./lib.js";
13import { getOperationParameters } from "./parameters.js";
14import {
15 HttpOperation,
16 HttpOperationParameters,
17 RouteOptions,
18 RoutePath,
19 RouteProducer,
20 RouteProducerResult,
21 RouteResolutionOptions,
22} from "./types.js";
23import { extractParamsFromPath } from "./utils.js";
24
25// The set of allowed segment separator characters
26const AllowedSegmentSeparators = ["/", ":"];
27
28function normalizeFragment(fragment: string) {
29 if (fragment.length > 0 && AllowedSegmentSeparators.indexOf(fragment[0]) < 0) {
30 // Insert the default separator
31 fragment = `/${fragment}`;
32 }
33
34 // Trim any trailing slash
35 return fragment.replace(/\/$/g, "");
36}
37
38function buildPath(pathFragments: string[]) {
39 // Join all fragments with leading and trailing slashes trimmed
40 const path =
41 pathFragments.length === 0
42 ? "/"
43 : pathFragments
44 .map(normalizeFragment)
45 .filter((x) => x !== "")
46 .join("");
47
48 // The final path must start with a '/'
49 return path.length > 0 && path[0] === "/" ? path : `/${path}`;
50}
51
52export function resolvePathAndParameters(
53 program: Program,
54 operation: Operation,
55 overloadBase: HttpOperation | undefined,
56 options: RouteResolutionOptions
57): DiagnosticResult<{
58 path: string;
59 pathSegments: string[];
60 parameters: HttpOperationParameters;
61}> {
62 const diagnostics = createDiagnosticCollector();
63 const { segments, parameters } = diagnostics.pipe(
64 getRouteSegments(program, operation, overloadBase, options)
65 );
66
67 // Pull out path parameters to verify what's in the path string
68 const paramByName = new Set(
69 parameters.parameters.filter(({ type }) => type === "path").map(({ param }) => param.name)
70 );
71
72 // Ensure that all of the parameters defined in the route are accounted for in
73 // the operation parameters
74 const routeParams = segments.flatMap(extractParamsFromPath);
75 for (const routeParam of routeParams) {
76 if (!paramByName.has(routeParam)) {
77 diagnostics.add(
78 createDiagnostic({
79 code: "missing-path-param",
80 format: { param: routeParam },
81 target: operation,
82 })
83 );
84 }
85 }
86
87 return diagnostics.wrap({
88 path: buildPath(segments),
89 pathSegments: segments,
90 parameters,
91 });
92}
93
94function collectSegmentsAndOptions(
95 program: Program,
96 source: Interface | Namespace | undefined
97): [string[], RouteOptions] {
98 if (source === undefined) return [[], {}];
99
100 const [parentSegments, parentOptions] = collectSegmentsAndOptions(program, source.namespace);
101
102 const route = getRoutePath(program, source)?.path;
103 const options =
104 source.kind === "Namespace" ? getRouteOptionsForNamespace(program, source) ?? {} : {};
105
106 return [[...parentSegments, ...(route ? [route] : [])], { ...parentOptions, ...options }];
107}
108
109function getRouteSegments(
110 program: Program,
111 operation: Operation,
112 overloadBase: HttpOperation | undefined,
113 options: RouteResolutionOptions
114): DiagnosticResult<RouteProducerResult> {
115 const diagnostics = createDiagnosticCollector();
116 const [parentSegments, parentOptions] = collectSegmentsAndOptions(
117 program,
118 operation.interface ?? operation.namespace
119 );
120
121 const routeProducer = getRouteProducer(program, operation) ?? DefaultRouteProducer;
122 const result = diagnostics.pipe(
123 routeProducer(program, operation, parentSegments, overloadBase, {
124 ...parentOptions,
125 ...options,
126 })
127 );
128
129 return diagnostics.wrap(result);
130}
131
132const externalInterfaces = createStateSymbol("externalInterfaces");
133/**
134 * @deprecated DO NOT USE. For internal use only as a workaround.
135 * @param program Program
136 * @param target Target namespace
137 * @param sourceInterface Interface that should be included in namespace.
138 */
139export function includeInterfaceRoutesInNamespace(
140 program: Program,
141 target: Namespace,
142 sourceInterface: string
143) {
144 let array = program.stateMap(externalInterfaces).get(target);
145 if (array === undefined) {
146 array = [];
147 program.stateMap(externalInterfaces).set(target, array);
148 }
149
150 array.push(sourceInterface);
151}
152
153const routeProducerKey = createStateSymbol("routeProducer");
154
155export function DefaultRouteProducer(
156 program: Program,
157 operation: Operation,
158 parentSegments: string[],
159 overloadBase: HttpOperation | undefined,
160 options: RouteOptions
161): DiagnosticResult<RouteProducerResult> {
162 const diagnostics = createDiagnosticCollector();
163 const routePath = getRoutePath(program, operation)?.path;
164 const segments =
165 !routePath && overloadBase
166 ? overloadBase.pathSegments
167 : [...parentSegments, ...(routePath ? [routePath] : [])];
168 const routeParams = segments.flatMap(extractParamsFromPath);
169
170 const parameters: HttpOperationParameters = diagnostics.pipe(
171 getOperationParameters(program, operation, overloadBase, routeParams, options.paramOptions)
172 );
173
174 // Pull out path parameters to verify what's in the path string
175 const unreferencedPathParamNames = new Set(
176 parameters.parameters.filter(({ type }) => type === "path").map(({ param }) => param.name)
177 );
178
179 // Compile the list of all route params that aren't represented in the route
180 for (const routeParam of routeParams) {
181 unreferencedPathParamNames.delete(routeParam);
182 }
183
184 // Add any remaining declared path params
185 for (const paramName of unreferencedPathParamNames) {
186 segments.push(`{${paramName}}`);
187 }
188
189 return diagnostics.wrap({
190 segments,
191 parameters,
192 });
193}
194
195export function setRouteProducer(
196 program: Program,
197 operation: Operation,
198 routeProducer: RouteProducer
199): void {
200 program.stateMap(routeProducerKey).set(operation, routeProducer);
201}
202
203export function getRouteProducer(program: Program, operation: Operation): RouteProducer {
204 return program.stateMap(routeProducerKey).get(operation);
205}
206
207const routesKey = createStateSymbol("routes");
208
209export function setRoute(context: DecoratorContext, entity: Type, details: RoutePath) {
210 if (
211 !validateDecoratorTarget(context, entity, "@route", ["Namespace", "Interface", "Operation"])
212 ) {
213 return;
214 }
215
216 const state = context.program.stateMap(routesKey);
217
218 if (state.has(entity) && entity.kind === "Namespace") {
219 const existingPath: string | undefined = state.get(entity);
220 if (existingPath !== details.path) {
221 reportDiagnostic(context.program, {
222 code: "duplicate-route-decorator",
223 messageId: "namespace",
224 target: entity,
225 });
226 }
227 } else {
228 state.set(entity, details.path);
229 if (entity.kind === "Operation" && details.shared) {
230 setSharedRoute(context.program, entity as Operation);
231 }
232 }
233}
234
235const sharedRoutesKey = createStateSymbol("sharedRoutes");
236
237export function setSharedRoute(program: Program, operation: Operation) {
238 program.stateMap(sharedRoutesKey).set(operation, true);
239}
240
241export function isSharedRoute(program: Program, operation: Operation): boolean {
242 return program.stateMap(sharedRoutesKey).get(operation) === true;
243}
244
245export function getRoutePath(
246 program: Program,
247 entity: Namespace | Interface | Operation
248): RoutePath | undefined {
249 const path = program.stateMap(routesKey).get(entity);
250 return path
251 ? {
252 path,
253 shared: entity.kind === "Operation" && isSharedRoute(program, entity as Operation),
254 }
255 : undefined;
256}
257
258const routeOptionsKey = createStateSymbol("routeOptions");
259
260export function setRouteOptionsForNamespace(
261 program: Program,
262 namespace: Namespace,
263 options: RouteOptions
264) {
265 program.stateMap(routeOptionsKey).set(namespace, options);
266}
267
268export function getRouteOptionsForNamespace(
269 program: Program,
270 namespace: Namespace
271): RouteOptions | undefined {
272 return program.stateMap(routeOptionsKey).get(namespace);
273}
274