microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
3040a83d6de0cc6876163b48ec9be61eefa3ebdd

Branches

Tags

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

Clone

HTTPS

Download ZIP

packages/http/src/operations.ts

260lines · 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 navigateProgram(program, {
123 namespace: (namespace) => {
124 if (namespace.operations.size > 0) {
125 reportDiagnostic(program, {
126 code: "no-service-found",
127 format: {
128 namespace: namespace.name,
129 },
130 target: namespace,
131 });
132 }
133 },
134 });
135 }
136}
137
138export function validateRouteUnique(
139 program: Program,
140 diagnostics: DiagnosticCollector,
141 operations: HttpOperation[]
142) {
143 const grouped = new Map<string, Map<HttpVerb, HttpOperation[]>>();
144
145 for (const operation of operations) {
146 const { verb, path } = operation;
147
148 if (operation.overloading !== undefined && isOverloadSameEndpoint(operation as any)) {
149 continue;
150 }
151 if (isSharedRoute(program, operation.operation)) {
152 continue;
153 }
154 let map = grouped.get(path);
155 if (map === undefined) {
156 map = new Map<HttpVerb, HttpOperation[]>();
157 grouped.set(path, map);
158 }
159
160 let list = map.get(verb);
161 if (list === undefined) {
162 list = [];
163 map.set(verb, list);
164 }
165
166 list.push(operation);
167 }
168
169 for (const [path, map] of grouped) {
170 for (const [verb, routes] of map) {
171 if (routes.length >= 2) {
172 for (const route of routes) {
173 diagnostics.add(
174 createDiagnostic({
175 code: "duplicate-operation",
176 format: { path, verb, operationName: route.operation.name },
177 target: route.operation,
178 })
179 );
180 }
181 }
182 }
183 }
184}
185
186export function isOverloadSameEndpoint(overload: HttpOperation & { overloading: HttpOperation }) {
187 return overload.path === overload.overloading.path && overload.verb === overload.overloading.verb;
188}
189
190function getHttpOperationInternal(
191 program: Program,
192 operation: Operation,
193 options: RouteResolutionOptions | undefined,
194 cache: Map<Operation, HttpOperation>
195): [HttpOperation, readonly Diagnostic[]] {
196 const existing = cache.get(operation);
197 if (existing) {
198 return [existing, []];
199 }
200 const diagnostics = createDiagnosticCollector();
201 const httpOperationRef: HttpOperation = { operation } as any;
202 cache.set(operation, httpOperationRef);
203
204 const overloadBase = getOverloadedOperation(program, operation);
205 let overloading;
206 if (overloadBase) {
207 overloading = httpOperationRef.overloading = diagnostics.pipe(
208 getHttpOperationInternal(program, overloadBase, options, cache)
209 );
210 }
211
212 const route = diagnostics.pipe(
213 resolvePathAndParameters(program, operation, overloading, options ?? {})
214 );
215 const responses = diagnostics.pipe(getResponsesForOperation(program, operation));
216
217 const httpOperation: HttpOperation = {
218 path: route.path,
219 pathSegments: route.pathSegments,
220 verb: route.parameters.verb,
221 container: operation.interface ?? operation.namespace ?? program.getGlobalNamespaceType(),
222 parameters: route.parameters,
223 operation,
224 responses,
225 };
226 Object.assign(httpOperationRef, httpOperation);
227
228 const overloads = getOverloads(program, operation);
229 if (overloads) {
230 httpOperationRef.overloads = overloads.map((x) =>
231 diagnostics.pipe(getHttpOperationInternal(program, x, options, cache))
232 );
233 }
234
235 return diagnostics.wrap(httpOperationRef);
236}
237
238function validateProgram(program: Program, diagnostics: DiagnosticCollector) {
239 navigateProgram(program, {
240 modelProperty(property) {
241 checkForUnsupportedVisibility(property);
242 },
243 });
244
245 // NOTE: This is intentionally not checked in the visibility decorator
246 // itself as that would be a layering violation, putting a REST
247 // interpretation of visibility into the core.
248 function checkForUnsupportedVisibility(property: ModelProperty) {
249 if (getVisibility(program, property)?.includes("write")) {
250 // NOTE: Check for name equality instead of function equality
251 // to deal with multiple copies of core being used.
252 const decorator = property.decorators.find((d) => d.decorator.name === $visibility.name);
253 const arg = decorator?.args.find(
254 (a) => a.node?.kind === SyntaxKind.StringLiteral && a.node.value === "write"
255 );
256 const target = arg?.node ?? property;
257 diagnostics.add(createDiagnostic({ code: "write-visibility-not-supported", target }));
258 }
259 }
260}
261