microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
archive/docusaurus-website

Branches

Tags

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

Clone

HTTPS

Download ZIP

packages/http/src/http-property.ts

272lines · modecode

1import {
2 DiagnosticResult,
3 Model,
4 Type,
5 compilerAssert,
6 createDiagnosticCollector,
7 walkPropertiesInherited,
8 type Diagnostic,
9 type ModelProperty,
10 type Program,
11} from "@typespec/compiler";
12import {
13 getHeaderFieldOptions,
14 getPathParamOptions,
15 getQueryParamOptions,
16 isBody,
17 isBodyRoot,
18 isMultipartBodyProperty,
19 isStatusCode,
20} from "./decorators.js";
21import { createDiagnostic } from "./lib.js";
22import { Visibility, isVisible } from "./metadata.js";
23import { HeaderFieldOptions, PathParameterOptions, QueryParameterOptions } from "./types.js";
24
25export type HttpProperty =
26 | HeaderProperty
27 | ContentTypeProperty
28 | QueryProperty
29 | PathProperty
30 | StatusCodeProperty
31 | BodyProperty
32 | BodyRootProperty
33 | MultipartBodyProperty
34 | BodyPropertyProperty;
35
36export interface HttpPropertyBase {
37 readonly property: ModelProperty;
38 /** Path from the root of the operation parameters/returnType to the property. */
39 readonly path: (string | number)[];
40}
41
42export interface HeaderProperty extends HttpPropertyBase {
43 readonly kind: "header";
44 readonly options: HeaderFieldOptions;
45}
46
47export interface ContentTypeProperty extends HttpPropertyBase {
48 readonly kind: "contentType";
49}
50
51export interface QueryProperty extends HttpPropertyBase {
52 readonly kind: "query";
53 readonly options: QueryParameterOptions;
54}
55export interface PathProperty extends HttpPropertyBase {
56 readonly kind: "path";
57 readonly options: PathParameterOptions;
58}
59export interface StatusCodeProperty extends HttpPropertyBase {
60 readonly kind: "statusCode";
61}
62export interface BodyProperty extends HttpPropertyBase {
63 readonly kind: "body";
64}
65export interface BodyRootProperty extends HttpPropertyBase {
66 readonly kind: "bodyRoot";
67}
68export interface MultipartBodyProperty extends HttpPropertyBase {
69 readonly kind: "multipartBody";
70}
71/** Property to include inside the body */
72export interface BodyPropertyProperty extends HttpPropertyBase {
73 readonly kind: "bodyProperty";
74}
75
76export interface GetHttpPropertyOptions {
77 implicitParameter?: (
78 param: ModelProperty,
79 ) => PathParameterOptions | QueryParameterOptions | undefined;
80}
81/**
82 * Find the type of a property in a model
83 */
84function getHttpProperty(
85 program: Program,
86 property: ModelProperty,
87 path: (string | number)[],
88 options: GetHttpPropertyOptions = {},
89): [HttpProperty, readonly Diagnostic[]] {
90 const diagnostics: Diagnostic[] = [];
91 function createResult<T extends Omit<HttpProperty, "path" | "property">>(
92 opts: T,
93 ): [HttpProperty & T, readonly Diagnostic[]] {
94 return [{ ...opts, property, path } as any, diagnostics];
95 }
96
97 const annotations = {
98 header: getHeaderFieldOptions(program, property),
99 query: getQueryParamOptions(program, property),
100 path: getPathParamOptions(program, property),
101 body: isBody(program, property),
102 bodyRoot: isBodyRoot(program, property),
103 multipartBody: isMultipartBodyProperty(program, property),
104 statusCode: isStatusCode(program, property),
105 };
106 const defined = Object.entries(annotations).filter((x) => !!x[1]);
107 const implicit = options.implicitParameter?.(property);
108
109 if (implicit && defined.length > 0) {
110 if (implicit.type === "path" && annotations.path) {
111 if (
112 annotations.path.explode ||
113 annotations.path.style !== "simple" ||
114 annotations.path.allowReserved
115 ) {
116 diagnostics.push(
117 createDiagnostic({
118 code: "use-uri-template",
119 format: {
120 param: property.name,
121 },
122 target: property,
123 }),
124 );
125 }
126 } else if (implicit.type === "query" && annotations.query) {
127 if (annotations.query.explode) {
128 diagnostics.push(
129 createDiagnostic({
130 code: "use-uri-template",
131 format: {
132 param: property.name,
133 },
134 target: property,
135 }),
136 );
137 }
138 } else {
139 diagnostics.push(
140 createDiagnostic({
141 code: "incompatible-uri-param",
142 format: {
143 param: property.name,
144 uriKind: implicit.type,
145 annotationKind: defined[0][0],
146 },
147 target: property,
148 }),
149 );
150 }
151 }
152 if (defined.length === 0) {
153 if (implicit) {
154 return createResult({
155 kind: implicit.type,
156 options: implicit as any,
157 property,
158 });
159 }
160 return createResult({ kind: "bodyProperty" });
161 } else if (defined.length > 1) {
162 diagnostics.push(
163 createDiagnostic({
164 code: "operation-param-duplicate-type",
165 format: { paramName: property.name, types: defined.map((x) => x[0]).join(", ") },
166 target: property,
167 }),
168 );
169 }
170
171 if (annotations.header) {
172 if (annotations.header.name.toLowerCase() === "content-type") {
173 return createResult({ kind: "contentType" });
174 } else {
175 return createResult({ kind: "header", options: annotations.header });
176 }
177 } else if (annotations.query) {
178 return createResult({ kind: "query", options: annotations.query });
179 } else if (annotations.path) {
180 return createResult({ kind: "path", options: annotations.path });
181 } else if (annotations.statusCode) {
182 return createResult({ kind: "statusCode" });
183 } else if (annotations.body) {
184 return createResult({ kind: "body" });
185 } else if (annotations.bodyRoot) {
186 return createResult({ kind: "bodyRoot" });
187 } else if (annotations.multipartBody) {
188 return createResult({ kind: "multipartBody" });
189 }
190 compilerAssert(false, `Unexpected http property type`);
191}
192
193/**
194 * Walks the given input(request parameters or response) and return all the properties and where they should be included(header, query, path, body, as a body property, etc.)
195 *
196 * @param rootMapOut If provided, the map will be populated to link nested metadata properties to their root properties.
197 */
198export function resolvePayloadProperties(
199 program: Program,
200 type: Type,
201 visibility: Visibility,
202 options: GetHttpPropertyOptions = {},
203): DiagnosticResult<HttpProperty[]> {
204 const diagnostics = createDiagnosticCollector();
205 const httpProperties = new Map<ModelProperty, HttpProperty>();
206
207 if (type.kind !== "Model" || type.properties.size === 0) {
208 return diagnostics.wrap([]);
209 }
210
211 const visited = new Set();
212 function checkModel(model: Model, path: string[]) {
213 visited.add(model);
214 let foundBody = false;
215 let foundBodyProperty = false;
216 for (const property of walkPropertiesInherited(model)) {
217 const propPath = [...path, property.name];
218
219 if (!isVisible(program, property, visibility)) {
220 continue;
221 }
222
223 let httpProperty = diagnostics.pipe(getHttpProperty(program, property, propPath, options));
224 if (shouldTreatAsBodyProperty(httpProperty, visibility)) {
225 httpProperty = { kind: "bodyProperty", property, path: propPath };
226 }
227
228 if (
229 httpProperty.kind === "body" ||
230 httpProperty.kind === "bodyRoot" ||
231 httpProperty.kind === "multipartBody"
232 ) {
233 foundBody = true;
234 }
235
236 if (
237 !(httpProperty.kind === "body" || httpProperty.kind === "multipartBody") &&
238 isModelWithProperties(property.type) &&
239 !visited.has(property.type)
240 ) {
241 if (checkModel(property.type, propPath)) {
242 foundBody = true;
243 continue;
244 }
245 }
246 if (httpProperty.kind === "bodyProperty") {
247 foundBodyProperty = true;
248 }
249 httpProperties.set(property, httpProperty);
250 }
251 return foundBody && !foundBodyProperty;
252 }
253
254 checkModel(type, []);
255
256 return diagnostics.wrap([...httpProperties.values()]);
257}
258
259function isModelWithProperties(type: Type): type is Model {
260 return type.kind === "Model" && !type.indexer && type.properties.size > 0;
261}
262
263function shouldTreatAsBodyProperty(property: HttpProperty, visibility: Visibility): boolean {
264 if (visibility & Visibility.Read) {
265 return property.kind === "query" || property.kind === "path";
266 }
267
268 if (!(visibility & Visibility.Read)) {
269 return property.kind === "statusCode";
270 }
271 return false;
272}
273