microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e26e7826068edd7ad2659a4a2ea74397dcdd1ea6

Branches

Tags

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

Clone

HTTPS

Download ZIP

packages/http/src/operations.ts

251lines · modecode

1import {
2 $visibility,
3 createDiagnosticCollector,
4 Diagnostic,
5 DiagnosticCollector,
6 getOverloadedOperation,
7 getOverloads,
8 getVisibility,
9 listOperationsIn,
10 listServices,
11 ModelProperty,
12 Namespace,
13 navigateProgram,
14 Operation,
15 Program,
16 SyntaxKind,
17} from "@typespec/compiler";
18import { createDiagnostic, reportDiagnostic } from "./lib.js";
19import { getResponsesForOperation } from "./responses.js";
20import { isSharedRoute, resolvePathAndParameters } from "./route.js";
21import {
22 HttpOperation,
23 HttpService,
24 HttpVerb,
25 OperationContainer,
26 RouteResolutionOptions,
27} from "./types.js";
28
29/**
30 * Return the Http Operation details for a given TypeSpec operation.
31 * @param operation Operation
32 * @param options Optional option on how to resolve the http details.
33 */
34export function getHttpOperation(
35 program: Program,
36 operation: Operation,
37 options?: RouteResolutionOptions
38): [HttpOperation, readonly Diagnostic[]] {
39 return getHttpOperationInternal(program, operation, options, new Map());
40}
41
42/**
43 * Get all the Http Operation in the given container.
44 * @param program Program
45 * @param container Namespace or interface containing operations
46 * @param options Resolution options
47 * @returns
48 */
49export function listHttpOperationsIn(
50 program: Program,
51 container: OperationContainer,
52 options?: RouteResolutionOptions
53): [HttpOperation[], readonly Diagnostic[]] {
54 const diagnostics = createDiagnosticCollector();
55 const operations = listOperationsIn(container, options?.listOptions);
56 const cache = new Map();
57 const httpOperations = operations.map((x) =>
58 diagnostics.pipe(getHttpOperationInternal(program, x, options, cache))
59 );
60 return diagnostics.wrap(httpOperations);
61}
62
63/**
64 * Returns all the services defined.
65 */
66export function getAllHttpServices(
67 program: Program,
68 options?: RouteResolutionOptions
69): [HttpService[], readonly Diagnostic[]] {
70 const diagnostics = createDiagnosticCollector();
71 const serviceNamespaces = listServices(program);
72
73 const services: HttpService[] = serviceNamespaces.map((x) =>
74 diagnostics.pipe(getHttpService(program, x.type, options))
75 );
76 if (serviceNamespaces.length === 0) {
77 services.push(
78 diagnostics.pipe(getHttpService(program, program.getGlobalNamespaceType(), options))
79 );
80 }
81 return diagnostics.wrap(services);
82}
83
84export function getHttpService(
85 program: Program,
86 serviceNamespace: Namespace,
87 options?: RouteResolutionOptions
88): [HttpService, readonly Diagnostic[]] {
89 const diagnostics = createDiagnosticCollector();
90 const httpOperations = diagnostics.pipe(
91 listHttpOperationsIn(program, serviceNamespace, {
92 ...options,
93 listOptions: {
94 recursive: serviceNamespace !== program.getGlobalNamespaceType(),
95 },
96 })
97 );
98
99 validateProgram(program, diagnostics);
100 validateRouteUnique(program, diagnostics, httpOperations);
101
102 const service: HttpService = {
103 namespace: serviceNamespace,
104 operations: httpOperations,
105 };
106 return diagnostics.wrap(service);
107}
108
109/**
110 * @deprecated use `getAllHttpServices` instead
111 */
112export function getAllRoutes(
113 program: Program,
114 options?: RouteResolutionOptions
115): [HttpOperation[], readonly Diagnostic[]] {
116 const [services, diagnostics] = getAllHttpServices(program, options);
117 return [services[0].operations, diagnostics];
118}
119
120export function reportIfNoRoutes(program: Program, routes: HttpOperation[]) {
121 if (routes.length === 0) {
122 reportDiagnostic(program, {
123 code: "no-routes",
124 target: program.getGlobalNamespaceType(),
125 });
126 }
127}
128
129export function validateRouteUnique(
130 program: Program,
131 diagnostics: DiagnosticCollector,
132 operations: HttpOperation[]
133) {
134 const grouped = new Map<string, Map<HttpVerb, HttpOperation[]>>();
135
136 for (const operation of operations) {
137 const { verb, path } = operation;
138
139 if (operation.overloading !== undefined && isOverloadSameEndpoint(operation as any)) {
140 continue;
141 }
142 if (isSharedRoute(program, operation.operation)) {
143 continue;
144 }
145 let map = grouped.get(path);
146 if (map === undefined) {
147 map = new Map<HttpVerb, HttpOperation[]>();
148 grouped.set(path, map);
149 }
150
151 let list = map.get(verb);
152 if (list === undefined) {
153 list = [];
154 map.set(verb, list);
155 }
156
157 list.push(operation);
158 }
159
160 for (const [path, map] of grouped) {
161 for (const [verb, routes] of map) {
162 if (routes.length >= 2) {
163 for (const route of routes) {
164 diagnostics.add(
165 createDiagnostic({
166 code: "duplicate-operation",
167 format: { path, verb, operationName: route.operation.name },
168 target: route.operation,
169 })
170 );
171 }
172 }
173 }
174 }
175}
176
177export function isOverloadSameEndpoint(overload: HttpOperation & { overloading: HttpOperation }) {
178 return overload.path === overload.overloading.path && overload.verb === overload.overloading.verb;
179}
180
181function getHttpOperationInternal(
182 program: Program,
183 operation: Operation,
184 options: RouteResolutionOptions | undefined,
185 cache: Map<Operation, HttpOperation>
186): [HttpOperation, readonly Diagnostic[]] {
187 const existing = cache.get(operation);
188 if (existing) {
189 return [existing, []];
190 }
191 const diagnostics = createDiagnosticCollector();
192 const httpOperationRef: HttpOperation = { operation } as any;
193 cache.set(operation, httpOperationRef);
194
195 const overloadBase = getOverloadedOperation(program, operation);
196 let overloading;
197 if (overloadBase) {
198 overloading = httpOperationRef.overloading = diagnostics.pipe(
199 getHttpOperationInternal(program, overloadBase, options, cache)
200 );
201 }
202
203 const route = diagnostics.pipe(
204 resolvePathAndParameters(program, operation, overloading, options ?? {})
205 );
206 const responses = diagnostics.pipe(getResponsesForOperation(program, operation));
207
208 const httpOperation: HttpOperation = {
209 path: route.path,
210 pathSegments: route.pathSegments,
211 verb: route.parameters.verb,
212 container: operation.interface ?? operation.namespace ?? program.getGlobalNamespaceType(),
213 parameters: route.parameters,
214 operation,
215 responses,
216 };
217 Object.assign(httpOperationRef, httpOperation);
218
219 const overloads = getOverloads(program, operation);
220 if (overloads) {
221 httpOperationRef.overloads = overloads.map((x) =>
222 diagnostics.pipe(getHttpOperationInternal(program, x, options, cache))
223 );
224 }
225
226 return diagnostics.wrap(httpOperationRef);
227}
228
229function validateProgram(program: Program, diagnostics: DiagnosticCollector) {
230 navigateProgram(program, {
231 modelProperty(property) {
232 checkForUnsupportedVisibility(property);
233 },
234 });
235
236 // NOTE: This is intentionally not checked in the visibility decorator
237 // itself as that would be a layering violation, putting a REST
238 // interpretation of visibility into the core.
239 function checkForUnsupportedVisibility(property: ModelProperty) {
240 if (getVisibility(program, property)?.includes("write")) {
241 // NOTE: Check for name equality instead of function equality
242 // to deal with multiple copies of core being used.
243 const decorator = property.decorators.find((d) => d.decorator.name === $visibility.name);
244 const arg = decorator?.args.find(
245 (a) => a.node?.kind === SyntaxKind.StringLiteral && a.node.value === "write"
246 );
247 const target = arg?.node ?? property;
248 diagnostics.add(createDiagnostic({ code: "write-visibility-not-supported", target }));
249 }
250 }
251}
252