microsoft/typespec
Publicmirrored from https://github.com/microsoft/typespecAvailable
packages/http/src/operations.ts
260lines · modecode
| 1 | import { |
| 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"; |
| 18 | import { createDiagnostic, reportDiagnostic } from "./lib.js"; |
| 19 | import { getResponsesForOperation } from "./responses.js"; |
| 20 | import { isSharedRoute, resolvePathAndParameters } from "./route.js"; |
| 21 | import { |
| 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 | */ |
| 34 | export 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 | */ |
| 49 | export 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 | */ |
| 66 | export 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 | |
| 84 | export 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 | */ |
| 112 | export 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 | |
| 120 | export 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 | |
| 138 | export 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 | |
| 186 | export function isOverloadSameEndpoint(overload: HttpOperation & { overloading: HttpOperation }) { |
| 187 | return overload.path === overload.overloading.path && overload.verb === overload.overloading.verb; |
| 188 | } |
| 189 | |
| 190 | function 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 | |
| 238 | function 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 | |