@typespec/http
Advanced tools
Comparing version
@@ -93,2 +93,18 @@ import type { DecoratorContext, Interface, ModelProperty, Namespace, Operation, Type } from "@typespec/compiler"; | ||
/** | ||
* | ||
* | ||
* | ||
* @example | ||
* ```tsp | ||
* op upload( | ||
* @header `content-type`: "multipart/form-data", | ||
* @multipartBody body: { | ||
* fullName: HttpPart<string>, | ||
* headShots: HttpPart<Image>[] | ||
* } | ||
* ): void; | ||
* ``` | ||
*/ | ||
export type MultipartBodyDecorator = (context: DecoratorContext, target: ModelProperty) => void; | ||
/** | ||
* Specify the HTTP verb for the target operation to be `GET`. | ||
@@ -95,0 +111,0 @@ * |
@@ -1,3 +0,5 @@ | ||
import type { DecoratorContext, Model } from "@typespec/compiler"; | ||
import type { DecoratorContext, Model, Type } from "@typespec/compiler"; | ||
export type PlainDataDecorator = (context: DecoratorContext, target: Model) => void; | ||
export type HttpFileDecorator = (context: DecoratorContext, target: Model) => void; | ||
export type HttpPartDecorator = (context: DecoratorContext, target: Model, type: Type, options: unknown) => void; | ||
//# sourceMappingURL=TypeSpec.Http.Private.d.ts.map |
/** An error here would mean that the decorator is not exported or doesn't have the right name. */ | ||
import { $body, $bodyIgnore, $bodyRoot, $delete, $get, $head, $header, $includeInapplicableMetadataInPayload, $patch, $path, $post, $put, $query, $route, $server, $sharedRoute, $statusCode, $useAuth, } from "@typespec/http"; | ||
import { $body, $bodyIgnore, $bodyRoot, $delete, $get, $head, $header, $includeInapplicableMetadataInPayload, $multipartBody, $patch, $path, $post, $put, $query, $route, $server, $sharedRoute, $statusCode, $useAuth, } from "@typespec/http"; | ||
/** An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ | ||
@@ -12,2 +12,3 @@ const _ = { | ||
$bodyIgnore, | ||
$multipartBody, | ||
$get, | ||
@@ -14,0 +15,0 @@ $put, |
import { Diagnostic, ModelProperty, Program } from "@typespec/compiler"; | ||
/** | ||
* @deprecated Use `OperationProperty.kind === 'contentType'` instead. | ||
* Check if the given model property is the content type header. | ||
@@ -4,0 +5,0 @@ * @param program Program |
@@ -5,2 +5,3 @@ import { createDiagnosticCollector } from "@typespec/compiler"; | ||
/** | ||
* @deprecated Use `OperationProperty.kind === 'contentType'` instead. | ||
* Check if the given model property is the content type header. | ||
@@ -41,4 +42,7 @@ * @param program Program | ||
} | ||
else if (property.type.kind === "Scalar" && property.type.name === "string") { | ||
return [["*/*"], []]; | ||
} | ||
return [[], [createDiagnostic({ code: "content-type-string", target: property })]]; | ||
} | ||
//# sourceMappingURL=content-types.js.map |
import { DecoratorContext, Diagnostic, Interface, Model, ModelProperty, Namespace, Operation, Program, Type } from "@typespec/compiler"; | ||
import { PlainDataDecorator } from "../generated-defs/TypeSpec.Http.Private.js"; | ||
import { BodyDecorator, BodyIgnoreDecorator, BodyRootDecorator, DeleteDecorator, GetDecorator, HeadDecorator, HeaderDecorator, PatchDecorator, PathDecorator, PostDecorator, PutDecorator, QueryDecorator, RouteDecorator, ServerDecorator, SharedRouteDecorator, StatusCodeDecorator } from "../generated-defs/TypeSpec.Http.js"; | ||
import { BodyDecorator, BodyIgnoreDecorator, BodyRootDecorator, DeleteDecorator, GetDecorator, HeadDecorator, HeaderDecorator, MultipartBodyDecorator, PatchDecorator, PathDecorator, PostDecorator, PutDecorator, QueryDecorator, RouteDecorator, ServerDecorator, SharedRouteDecorator, StatusCodeDecorator } from "../generated-defs/TypeSpec.Http.js"; | ||
import { Authentication, HeaderFieldOptions, HttpStatusCodeRange, HttpStatusCodes, HttpVerb, PathParameterOptions, QueryParameterOptions } from "./types.js"; | ||
@@ -24,2 +23,4 @@ export declare const namespace = "TypeSpec.Http"; | ||
export declare function isBodyIgnore(program: Program, entity: ModelProperty): boolean; | ||
export declare const $multipartBody: MultipartBodyDecorator; | ||
export declare function isMultipartBodyProperty(program: Program, entity: Type): boolean; | ||
export declare const $statusCode: StatusCodeDecorator; | ||
@@ -55,3 +56,2 @@ /** | ||
export declare function getServers(program: Program, type: Namespace): HttpServer[] | undefined; | ||
export declare const $plainData: PlainDataDecorator; | ||
export declare function $useAuth(context: DecoratorContext, entity: Namespace | Interface | Operation, authConfig: Type): void; | ||
@@ -58,0 +58,0 @@ export declare function setAuthentication(program: Program, entity: Namespace | Interface | Operation, auth: Authentication): void; |
@@ -1,2 +0,2 @@ | ||
import { createDiagnosticCollector, getDoc, ignoreDiagnostics, isArrayModelType, reportDeprecated, setTypeSpecNamespace, typespecTypeToJson, validateDecoratorTarget, validateDecoratorUniqueOnNode, } from "@typespec/compiler"; | ||
import { createDiagnosticCollector, getDoc, ignoreDiagnostics, isArrayModelType, reportDeprecated, typespecTypeToJson, validateDecoratorTarget, validateDecoratorUniqueOnNode, } from "@typespec/compiler"; | ||
import { HttpStateKeys, createDiagnostic, reportDiagnostic } from "./lib.js"; | ||
@@ -134,2 +134,8 @@ import { setRoute, setSharedRoute } from "./route.js"; | ||
} | ||
export const $multipartBody = (context, entity) => { | ||
context.program.stateSet(HttpStateKeys.multipartBody).add(entity); | ||
}; | ||
export function isMultipartBodyProperty(program, entity) { | ||
return program.stateSet(HttpStateKeys.multipartBody).has(entity); | ||
} | ||
export const $statusCode = (context, entity) => { | ||
@@ -333,25 +339,2 @@ context.program.stateSet(HttpStateKeys.statusCode).add(entity); | ||
} | ||
export const $plainData = (context, entity) => { | ||
const { program } = context; | ||
const decoratorsToRemove = ["$header", "$body", "$query", "$path", "$statusCode"]; | ||
const [headers, bodies, queries, paths, statusCodes] = [ | ||
program.stateMap(HttpStateKeys.header), | ||
program.stateSet(HttpStateKeys.body), | ||
program.stateMap(HttpStateKeys.query), | ||
program.stateMap(HttpStateKeys.path), | ||
program.stateMap(HttpStateKeys.statusCode), | ||
]; | ||
for (const property of entity.properties.values()) { | ||
// Remove the decorators so that they do not run in the future, for example, | ||
// if this model is later spread into another. | ||
property.decorators = property.decorators.filter((d) => !decoratorsToRemove.includes(d.decorator.name)); | ||
// Remove the impact the decorators already had on this model. | ||
headers.delete(property); | ||
bodies.delete(property); | ||
queries.delete(property); | ||
paths.delete(property); | ||
statusCodes.delete(property); | ||
} | ||
}; | ||
setTypeSpecNamespace("Private", $plainData); | ||
export function $useAuth(context, entity, authConfig) { | ||
@@ -358,0 +341,0 @@ validateDecoratorUniqueOnNode(context, entity, $useAuth); |
@@ -9,2 +9,3 @@ export { $lib } from "./lib.js"; | ||
export * from "./parameters.js"; | ||
export { getHttpFileModel, isHttpFile, isOrExtendsHttpFile } from "./private.decorators.js"; | ||
export * from "./responses.js"; | ||
@@ -11,0 +12,0 @@ export * from "./route.js"; |
@@ -9,2 +9,3 @@ export { $lib } from "./lib.js"; | ||
export * from "./parameters.js"; | ||
export { getHttpFileModel, isHttpFile, isOrExtendsHttpFile } from "./private.decorators.js"; | ||
export * from "./responses.js"; | ||
@@ -11,0 +12,0 @@ export * from "./route.js"; |
@@ -59,5 +59,17 @@ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{ | ||
}; | ||
"multipart-invalid-content-type": { | ||
readonly default: import("@typespec/compiler").CallableMessage<[string, string]>; | ||
}; | ||
"multipart-model": { | ||
readonly default: "Multipart request body must be a model."; | ||
}; | ||
"multipart-part": { | ||
readonly default: "Expect item to be an HttpPart model."; | ||
}; | ||
"multipart-nested": { | ||
readonly default: "Cannot use @multipartBody inside of an HttpPart"; | ||
}; | ||
"formdata-no-part-name": { | ||
readonly default: "Part used in multipart/form-data must have a name."; | ||
}; | ||
"header-format-required": { | ||
@@ -69,4 +81,4 @@ readonly default: "A format must be specified for @header when type is an array. e.g. @header({format: \"csv\"})"; | ||
}; | ||
}, Record<string, any>, "path" | "query" | "authentication" | "header" | "body" | "bodyRoot" | "bodyIgnore" | "statusCode" | "verbs" | "servers" | "includeInapplicableMetadataInPayload" | "externalInterfaces" | "routeProducer" | "routes" | "sharedRoutes" | "routeOptions">; | ||
export declare const reportDiagnostic: <C extends "http-verb-duplicate" | "http-verb-wrong-type" | "missing-path-param" | "optional-path-param" | "missing-server-param" | "duplicate-body" | "duplicate-route-decorator" | "operation-param-duplicate-type" | "duplicate-operation" | "multiple-status-codes" | "status-code-invalid" | "content-type-string" | "content-type-ignored" | "metadata-ignored" | "no-service-found" | "invalid-type-for-auth" | "shared-inconsistency" | "write-visibility-not-supported" | "multipart-model" | "header-format-required" | "query-format-required", M extends keyof { | ||
}, Record<string, any>, "path" | "file" | "query" | "authentication" | "header" | "body" | "bodyRoot" | "bodyIgnore" | "multipartBody" | "statusCode" | "verbs" | "servers" | "includeInapplicableMetadataInPayload" | "externalInterfaces" | "routeProducer" | "routes" | "sharedRoutes" | "routeOptions" | "httpPart">; | ||
export declare const reportDiagnostic: <C extends "http-verb-duplicate" | "http-verb-wrong-type" | "missing-path-param" | "optional-path-param" | "missing-server-param" | "duplicate-body" | "duplicate-route-decorator" | "operation-param-duplicate-type" | "duplicate-operation" | "multiple-status-codes" | "status-code-invalid" | "content-type-string" | "content-type-ignored" | "metadata-ignored" | "no-service-found" | "invalid-type-for-auth" | "shared-inconsistency" | "write-visibility-not-supported" | "multipart-invalid-content-type" | "multipart-model" | "multipart-part" | "multipart-nested" | "formdata-no-part-name" | "header-format-required" | "query-format-required", M extends keyof { | ||
"http-verb-duplicate": { | ||
@@ -129,5 +141,17 @@ readonly default: import("@typespec/compiler").CallableMessage<[string]>; | ||
}; | ||
"multipart-invalid-content-type": { | ||
readonly default: import("@typespec/compiler").CallableMessage<[string, string]>; | ||
}; | ||
"multipart-model": { | ||
readonly default: "Multipart request body must be a model."; | ||
}; | ||
"multipart-part": { | ||
readonly default: "Expect item to be an HttpPart model."; | ||
}; | ||
"multipart-nested": { | ||
readonly default: "Cannot use @multipartBody inside of an HttpPart"; | ||
}; | ||
"formdata-no-part-name": { | ||
readonly default: "Part used in multipart/form-data must have a name."; | ||
}; | ||
"header-format-required": { | ||
@@ -197,5 +221,17 @@ readonly default: "A format must be specified for @header when type is an array. e.g. @header({format: \"csv\"})"; | ||
}; | ||
"multipart-invalid-content-type": { | ||
readonly default: import("@typespec/compiler").CallableMessage<[string, string]>; | ||
}; | ||
"multipart-model": { | ||
readonly default: "Multipart request body must be a model."; | ||
}; | ||
"multipart-part": { | ||
readonly default: "Expect item to be an HttpPart model."; | ||
}; | ||
"multipart-nested": { | ||
readonly default: "Cannot use @multipartBody inside of an HttpPart"; | ||
}; | ||
"formdata-no-part-name": { | ||
readonly default: "Part used in multipart/form-data must have a name."; | ||
}; | ||
"header-format-required": { | ||
@@ -207,3 +243,3 @@ readonly default: "A format must be specified for @header when type is an array. e.g. @header({format: \"csv\"})"; | ||
}; | ||
}, C, M>) => void, createDiagnostic: <C extends "http-verb-duplicate" | "http-verb-wrong-type" | "missing-path-param" | "optional-path-param" | "missing-server-param" | "duplicate-body" | "duplicate-route-decorator" | "operation-param-duplicate-type" | "duplicate-operation" | "multiple-status-codes" | "status-code-invalid" | "content-type-string" | "content-type-ignored" | "metadata-ignored" | "no-service-found" | "invalid-type-for-auth" | "shared-inconsistency" | "write-visibility-not-supported" | "multipart-model" | "header-format-required" | "query-format-required", M extends keyof { | ||
}, C, M>) => void, createDiagnostic: <C extends "http-verb-duplicate" | "http-verb-wrong-type" | "missing-path-param" | "optional-path-param" | "missing-server-param" | "duplicate-body" | "duplicate-route-decorator" | "operation-param-duplicate-type" | "duplicate-operation" | "multiple-status-codes" | "status-code-invalid" | "content-type-string" | "content-type-ignored" | "metadata-ignored" | "no-service-found" | "invalid-type-for-auth" | "shared-inconsistency" | "write-visibility-not-supported" | "multipart-invalid-content-type" | "multipart-model" | "multipart-part" | "multipart-nested" | "formdata-no-part-name" | "header-format-required" | "query-format-required", M extends keyof { | ||
"http-verb-duplicate": { | ||
@@ -266,5 +302,17 @@ readonly default: import("@typespec/compiler").CallableMessage<[string]>; | ||
}; | ||
"multipart-invalid-content-type": { | ||
readonly default: import("@typespec/compiler").CallableMessage<[string, string]>; | ||
}; | ||
"multipart-model": { | ||
readonly default: "Multipart request body must be a model."; | ||
}; | ||
"multipart-part": { | ||
readonly default: "Expect item to be an HttpPart model."; | ||
}; | ||
"multipart-nested": { | ||
readonly default: "Cannot use @multipartBody inside of an HttpPart"; | ||
}; | ||
"formdata-no-part-name": { | ||
readonly default: "Part used in multipart/form-data must have a name."; | ||
}; | ||
"header-format-required": { | ||
@@ -334,5 +382,17 @@ readonly default: "A format must be specified for @header when type is an array. e.g. @header({format: \"csv\"})"; | ||
}; | ||
"multipart-invalid-content-type": { | ||
readonly default: import("@typespec/compiler").CallableMessage<[string, string]>; | ||
}; | ||
"multipart-model": { | ||
readonly default: "Multipart request body must be a model."; | ||
}; | ||
"multipart-part": { | ||
readonly default: "Expect item to be an HttpPart model."; | ||
}; | ||
"multipart-nested": { | ||
readonly default: "Cannot use @multipartBody inside of an HttpPart"; | ||
}; | ||
"formdata-no-part-name": { | ||
readonly default: "Part used in multipart/form-data must have a name."; | ||
}; | ||
"header-format-required": { | ||
@@ -344,3 +404,3 @@ readonly default: "A format must be specified for @header when type is an array. e.g. @header({format: \"csv\"})"; | ||
}; | ||
}, C, M>) => import("@typespec/compiler").Diagnostic, HttpStateKeys: Record<"path" | "query" | "authentication" | "header" | "body" | "bodyRoot" | "bodyIgnore" | "statusCode" | "verbs" | "servers" | "includeInapplicableMetadataInPayload" | "externalInterfaces" | "routeProducer" | "routes" | "sharedRoutes" | "routeOptions", symbol>; | ||
}, C, M>) => import("@typespec/compiler").Diagnostic, HttpStateKeys: Record<"path" | "file" | "query" | "authentication" | "header" | "body" | "bodyRoot" | "bodyIgnore" | "multipartBody" | "statusCode" | "verbs" | "servers" | "includeInapplicableMetadataInPayload" | "externalInterfaces" | "routeProducer" | "routes" | "sharedRoutes" | "routeOptions" | "httpPart", symbol>; | ||
//# sourceMappingURL=lib.d.ts.map |
@@ -116,2 +116,8 @@ import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler"; | ||
}, | ||
"multipart-invalid-content-type": { | ||
severity: "error", | ||
messages: { | ||
default: paramMessage `Content type '${"contentType"}' is not a multipart content type. Supported content types are: ${"supportedContentTypes"}.`, | ||
}, | ||
}, | ||
"multipart-model": { | ||
@@ -123,2 +129,20 @@ severity: "error", | ||
}, | ||
"multipart-part": { | ||
severity: "error", | ||
messages: { | ||
default: "Expect item to be an HttpPart model.", | ||
}, | ||
}, | ||
"multipart-nested": { | ||
severity: "error", | ||
messages: { | ||
default: "Cannot use @multipartBody inside of an HttpPart", | ||
}, | ||
}, | ||
"formdata-no-part-name": { | ||
severity: "error", | ||
messages: { | ||
default: "Part used in multipart/form-data must have a name.", | ||
}, | ||
}, | ||
"header-format-required": { | ||
@@ -145,2 +169,3 @@ severity: "error", | ||
bodyIgnore: { description: "State for the @bodyIgnore decorator" }, | ||
multipartBody: { description: "State for the @bodyIgnore decorator" }, | ||
statusCode: { description: "State for the @statusCode decorator" }, | ||
@@ -158,2 +183,5 @@ verbs: { description: "State for the verb decorators (@get, @post, @put, etc.)" }, | ||
routeOptions: {}, | ||
// private | ||
file: { description: "State for the @Private.file decorator" }, | ||
httpPart: { description: "State for the @Private.httpPart decorator" }, | ||
}, | ||
@@ -160,0 +188,0 @@ }); |
@@ -1,2 +0,2 @@ | ||
import { DiagnosticCollector, ModelProperty, Operation, Program, Type } from "@typespec/compiler"; | ||
import { ModelProperty, Operation, Program, Type } from "@typespec/compiler"; | ||
import { HttpVerb } from "./types.js"; | ||
@@ -64,11 +64,2 @@ /** | ||
/** | ||
* Walks the given type and collects all applicable metadata and `@body` | ||
* properties recursively. | ||
* | ||
* @param rootMapOut If provided, the map will be populated to link | ||
* nested metadata properties to their root properties. | ||
*/ | ||
export declare function gatherMetadata(program: Program, diagnostics: DiagnosticCollector, // currently unused, but reserved for future diagnostics | ||
type: Type, visibility: Visibility, isMetadataCallback?: typeof isMetadata, rootMapOut?: Map<ModelProperty, ModelProperty>): Set<ModelProperty>; | ||
/** | ||
* Determines if a property is metadata. A property is defined to be | ||
@@ -75,0 +66,0 @@ * metadata if it is marked `@header`, `@query`, `@path`, or `@statusCode`. |
@@ -1,3 +0,4 @@ | ||
import { compilerAssert, getEffectiveModelType, getParameterVisibility, isVisible as isVisibleCore, Queue, TwoLevelMap, walkPropertiesInherited, } from "@typespec/compiler"; | ||
import { includeInapplicableMetadataInPayload, isBody, isBodyIgnore, isBodyRoot, isHeader, isPathParam, isQueryParam, isStatusCode, } from "./decorators.js"; | ||
import { compilerAssert, getEffectiveModelType, getParameterVisibility, isVisible as isVisibleCore, } from "@typespec/compiler"; | ||
import { TwoLevelMap } from "@typespec/compiler/utils"; | ||
import { includeInapplicableMetadataInPayload, isBody, isBodyIgnore, isBodyRoot, isHeader, isMultipartBodyProperty, isPathParam, isQueryParam, isStatusCode, } from "./decorators.js"; | ||
/** | ||
@@ -175,51 +176,2 @@ * Flags enum representation of well-known visibilities that are used in | ||
/** | ||
* Walks the given type and collects all applicable metadata and `@body` | ||
* properties recursively. | ||
* | ||
* @param rootMapOut If provided, the map will be populated to link | ||
* nested metadata properties to their root properties. | ||
*/ | ||
export function gatherMetadata(program, diagnostics, // currently unused, but reserved for future diagnostics | ||
type, visibility, isMetadataCallback = isMetadata, rootMapOut) { | ||
const metadata = new Map(); | ||
if (type.kind !== "Model" || type.properties.size === 0) { | ||
return new Set(); | ||
} | ||
const visited = new Set(); | ||
const queue = new Queue([[type, undefined]]); | ||
while (!queue.isEmpty()) { | ||
const [model, rootOpt] = queue.dequeue(); | ||
visited.add(model); | ||
for (const property of walkPropertiesInherited(model)) { | ||
const root = rootOpt ?? property; | ||
if (!isVisible(program, property, visibility)) { | ||
continue; | ||
} | ||
// ISSUE: This should probably be an error, but that's a breaking | ||
// change that currently breaks some samples and tests. | ||
// | ||
// The traversal here is level-order so that the preferred metadata in | ||
// the case of duplicates, which is the most compatible with prior | ||
// behavior where nested metadata was always dropped. | ||
if (metadata.has(property.name)) { | ||
continue; | ||
} | ||
if (isApplicableMetadataOrBody(program, property, visibility, isMetadataCallback)) { | ||
metadata.set(property.name, property); | ||
rootMapOut?.set(property, root); | ||
if (isBody(program, property)) { | ||
continue; // We ignore any properties under `@body` | ||
} | ||
} | ||
if (property.type.kind === "Model" && | ||
!type.indexer && | ||
type.properties.size > 0 && | ||
!visited.has(property.type)) { | ||
queue.enqueue([property.type, root]); | ||
} | ||
} | ||
} | ||
return new Set(metadata.values()); | ||
} | ||
/** | ||
* Determines if a property is metadata. A property is defined to be | ||
@@ -264,3 +216,6 @@ * metadata if it is marked `@header`, `@query`, `@path`, or `@statusCode`. | ||
} | ||
if (treatBodyAsMetadata && (isBody(program, property) || isBodyRoot(program, property))) { | ||
if (treatBodyAsMetadata && | ||
(isBody(program, property) || | ||
isBodyRoot(program, property) || | ||
isMultipartBodyProperty(program, property))) { | ||
return true; | ||
@@ -402,3 +357,3 @@ } | ||
* If the type is an anonymous model, tries to find a named model that has the same | ||
* set of properties when non-payload properties are excluded. | ||
* set of properties when non-payload properties are excluded.we | ||
*/ | ||
@@ -405,0 +360,0 @@ function getEffectivePayloadType(type, visibility) { |
import { createDiagnosticCollector, } from "@typespec/compiler"; | ||
import { resolveBody } from "./body.js"; | ||
import { getContentTypes, isContentTypeHeader } from "./content-types.js"; | ||
import { getHeaderFieldOptions, getOperationVerb, getPathParamOptions, getQueryParamOptions, isBody, isBodyRoot, } from "./decorators.js"; | ||
import { getOperationVerb } from "./decorators.js"; | ||
import { createDiagnostic } from "./lib.js"; | ||
import { gatherMetadata, isMetadata, resolveRequestVisibility } from "./metadata.js"; | ||
import { resolveRequestVisibility } from "./metadata.js"; | ||
import { resolveHttpPayload } from "./payload.js"; | ||
export function getOperationParameters(program, operation, overloadBase, knownPathParamNames = [], options = {}) { | ||
@@ -26,4 +25,2 @@ const verb = (options?.verbSelector && options.verbSelector(program, operation)) ?? | ||
const visibility = resolveRequestVisibility(program, operation, verb); | ||
const rootPropertyMap = new Map(); | ||
const metadata = gatherMetadata(program, diagnostics, operation.parameters, visibility, (_, param) => isMetadata(program, param) || isImplicitPathParam(param), rootPropertyMap); | ||
function isImplicitPathParam(param) { | ||
@@ -34,54 +31,33 @@ const isTopLevel = param.model === operation.parameters; | ||
const parameters = []; | ||
const resolvedBody = diagnostics.pipe(resolveBody(program, operation.parameters, metadata, rootPropertyMap, visibility, "request")); | ||
let contentTypes; | ||
for (const param of metadata) { | ||
const queryOptions = getQueryParamOptions(program, param); | ||
const pathOptions = getPathParamOptions(program, param) ?? | ||
(isImplicitPathParam(param) && { type: "path", name: param.name }); | ||
const headerOptions = getHeaderFieldOptions(program, param); | ||
const isBodyVal = isBody(program, param); | ||
const isBodyRootVal = isBodyRoot(program, param); | ||
const defined = [ | ||
["query", queryOptions], | ||
["path", pathOptions], | ||
["header", headerOptions], | ||
["body", isBodyVal || isBodyRootVal], | ||
].filter((x) => !!x[1]); | ||
if (defined.length >= 2) { | ||
diagnostics.add(createDiagnostic({ | ||
code: "operation-param-duplicate-type", | ||
format: { paramName: param.name, types: defined.map((x) => x[0]).join(", ") }, | ||
target: param, | ||
})); | ||
const { body: resolvedBody, metadata } = diagnostics.pipe(resolveHttpPayload(program, operation.parameters, visibility, "request", { | ||
isImplicitPathParam, | ||
})); | ||
for (const item of metadata) { | ||
switch (item.kind) { | ||
case "contentType": | ||
parameters.push({ | ||
name: "Content-Type", | ||
type: "header", | ||
param: item.property, | ||
}); | ||
break; | ||
case "path": | ||
if (item.property.optional) { | ||
diagnostics.add(createDiagnostic({ | ||
code: "optional-path-param", | ||
format: { paramName: item.property.name }, | ||
target: item.property, | ||
})); | ||
} | ||
// eslint-disable-next-line no-fallthrough | ||
case "query": | ||
case "header": | ||
parameters.push({ | ||
...item.options, | ||
param: item.property, | ||
}); | ||
break; | ||
} | ||
if (queryOptions) { | ||
parameters.push({ | ||
...queryOptions, | ||
param, | ||
}); | ||
} | ||
else if (pathOptions) { | ||
if (param.optional) { | ||
diagnostics.add(createDiagnostic({ | ||
code: "optional-path-param", | ||
format: { paramName: param.name }, | ||
target: operation, | ||
})); | ||
} | ||
parameters.push({ | ||
...pathOptions, | ||
param, | ||
}); | ||
} | ||
else if (headerOptions) { | ||
if (isContentTypeHeader(program, param)) { | ||
contentTypes = diagnostics.pipe(getContentTypes(param)); | ||
} | ||
parameters.push({ | ||
...headerOptions, | ||
param, | ||
}); | ||
} | ||
} | ||
const body = diagnostics.pipe(computeHttpOperationBody(operation, resolvedBody, contentTypes)); | ||
const body = resolvedBody; | ||
return diagnostics.wrap({ | ||
@@ -95,36 +71,6 @@ parameters, | ||
get bodyParameter() { | ||
return body?.parameter; | ||
return body?.property; | ||
}, | ||
}); | ||
} | ||
function computeHttpOperationBody(operation, resolvedBody, contentTypes) { | ||
contentTypes ??= []; | ||
const diagnostics = []; | ||
if (resolvedBody === undefined) { | ||
if (contentTypes.length > 0) { | ||
diagnostics.push(createDiagnostic({ | ||
code: "content-type-ignored", | ||
target: operation.parameters, | ||
})); | ||
} | ||
return [undefined, diagnostics]; | ||
} | ||
if (contentTypes.includes("multipart/form-data") && resolvedBody.type.kind !== "Model") { | ||
diagnostics.push(createDiagnostic({ | ||
code: "multipart-model", | ||
target: resolvedBody.property ?? operation.parameters, | ||
})); | ||
return [undefined, diagnostics]; | ||
} | ||
const body = { | ||
type: resolvedBody.type, | ||
isExplicit: resolvedBody.isExplicit, | ||
containsMetadataAnnotations: resolvedBody.containsMetadataAnnotations, | ||
contentTypes, | ||
}; | ||
if (resolvedBody.property) { | ||
body.parameter = resolvedBody.property; | ||
} | ||
return [body, diagnostics]; | ||
} | ||
//# sourceMappingURL=parameters.js.map |
import { createDiagnosticCollector, getDoc, getErrorsDoc, getReturnsDoc, isErrorModel, isNullType, isVoidType, } from "@typespec/compiler"; | ||
import { resolveBody } from "./body.js"; | ||
import { getContentTypes, isContentTypeHeader } from "./content-types.js"; | ||
import { getHeaderFieldName, getStatusCodeDescription, getStatusCodesWithDiagnostics, isHeader, isStatusCode, } from "./decorators.js"; | ||
import { createDiagnostic, HttpStateKeys, reportDiagnostic } from "./lib.js"; | ||
import { gatherMetadata, Visibility } from "./metadata.js"; | ||
import { getStatusCodeDescription, getStatusCodesWithDiagnostics } from "./decorators.js"; | ||
import { HttpStateKeys, reportDiagnostic } from "./lib.js"; | ||
import { Visibility } from "./metadata.js"; | ||
import { resolveHttpPayload } from "./payload.js"; | ||
/** | ||
@@ -52,12 +51,8 @@ * Get the responses for a given operation. | ||
function processResponseType(program, diagnostics, operation, responses, responseType) { | ||
const rootPropertyMap = new Map(); | ||
const metadata = gatherMetadata(program, diagnostics, responseType, Visibility.Read, undefined, rootPropertyMap); | ||
// Get body | ||
let { body: resolvedBody, metadata } = diagnostics.pipe(resolveHttpPayload(program, responseType, Visibility.Read, "response")); | ||
// Get explicity defined status codes | ||
const statusCodes = diagnostics.pipe(getResponseStatusCodes(program, responseType, metadata)); | ||
// Get explicitly defined content types | ||
const contentTypes = getResponseContentTypes(program, diagnostics, metadata); | ||
// Get response headers | ||
const headers = getResponseHeaders(program, metadata); | ||
// Get body | ||
let resolvedBody = diagnostics.pipe(resolveBody(program, responseType, metadata, rootPropertyMap, Visibility.Read, "response")); | ||
// If there is no explicit status code, check if it should be 204 | ||
@@ -80,6 +75,2 @@ if (statusCodes.length === 0) { | ||
} | ||
// If there is a body but no explicit content types, use application/json | ||
if (resolvedBody && contentTypes.length === 0) { | ||
contentTypes.push("application/json"); | ||
} | ||
// Put them into currentEndpoint.responses | ||
@@ -98,15 +89,6 @@ for (const statusCode of statusCodes) { | ||
response.responses.push({ | ||
body: { | ||
contentTypes: contentTypes, | ||
...resolvedBody, | ||
}, | ||
body: resolvedBody, | ||
headers, | ||
}); | ||
} | ||
else if (contentTypes.length > 0) { | ||
diagnostics.add(createDiagnostic({ | ||
code: "content-type-ignored", | ||
target: responseType, | ||
})); | ||
} | ||
else { | ||
@@ -128,3 +110,3 @@ response.responses.push({ headers }); | ||
for (const prop of metadata) { | ||
if (isStatusCode(program, prop)) { | ||
if (prop.kind === "statusCode") { | ||
if (statusFound) { | ||
@@ -137,3 +119,3 @@ reportDiagnostic(program, { | ||
statusFound = true; | ||
codes.push(...diagnostics.pipe(getStatusCodesWithDiagnostics(program, prop))); | ||
codes.push(...diagnostics.pipe(getStatusCodesWithDiagnostics(program, prop.property))); | ||
} | ||
@@ -154,16 +136,2 @@ } | ||
/** | ||
* Get explicity defined content-types from response metadata | ||
* Return is an array of strings, possibly empty, which indicates no explicitly defined content-type. | ||
* We do not check for duplicates here -- that will be done by the caller. | ||
*/ | ||
function getResponseContentTypes(program, diagnostics, metadata) { | ||
const contentTypes = []; | ||
for (const prop of metadata) { | ||
if (isHeader(program, prop) && isContentTypeHeader(program, prop)) { | ||
contentTypes.push(...diagnostics.pipe(getContentTypes(prop))); | ||
} | ||
} | ||
return contentTypes; | ||
} | ||
/** | ||
* Get response headers from response metadata | ||
@@ -174,5 +142,4 @@ */ | ||
for (const prop of metadata) { | ||
const headerName = getHeaderFieldName(program, prop); | ||
if (isHeader(program, prop) && headerName !== "content-type") { | ||
responseHeaders[headerName] = prop; | ||
if (prop.kind === "header") { | ||
responseHeaders[prop.options.name] = prop.property; | ||
} | ||
@@ -179,0 +146,0 @@ } |
@@ -1,2 +0,3 @@ | ||
import { DiagnosticResult, Interface, ListOperationOptions, ModelProperty, Namespace, Operation, Program, Type } from "@typespec/compiler"; | ||
import { DiagnosticResult, Interface, ListOperationOptions, Model, ModelProperty, Namespace, Operation, Program, Tuple, Type } from "@typespec/compiler"; | ||
import { HeaderProperty } from "./http-property.js"; | ||
/** | ||
@@ -240,21 +241,12 @@ * @deprecated use `HttpOperation`. To remove in November 2022 release. | ||
/** | ||
* Represent the body information for an http request. | ||
* | ||
* @note the `type` must be a `Model` if the content type is multipart. | ||
* @deprecated use {@link HttpOperationBody} | ||
*/ | ||
export interface HttpOperationRequestBody extends HttpOperationBody { | ||
/** | ||
* If the body was explicitly set as a property. Correspond to the property with `@body` or `@bodyRoot` | ||
*/ | ||
parameter?: ModelProperty; | ||
} | ||
export interface HttpOperationResponseBody extends HttpOperationBody { | ||
/** | ||
* If the body was explicitly set as a property. Correspond to the property with `@body` or `@bodyRoot` | ||
*/ | ||
readonly property?: ModelProperty; | ||
} | ||
export type HttpOperationRequestBody = HttpOperationBody; | ||
/** | ||
* @deprecated use {@link HttpOperationBody} | ||
*/ | ||
export type HttpOperationResponseBody = HttpOperationBody; | ||
export interface HttpOperationParameters { | ||
parameters: HttpOperationParameter[]; | ||
body?: HttpOperationRequestBody; | ||
body?: HttpOperationBody | HttpOperationMultipartBody; | ||
/** @deprecated use {@link body.type} */ | ||
@@ -338,18 +330,47 @@ bodyType?: Type; | ||
headers?: Record<string, ModelProperty>; | ||
body?: HttpOperationResponseBody; | ||
body?: HttpOperationBody | HttpOperationMultipartBody; | ||
} | ||
export interface HttpOperationBody { | ||
export interface HttpOperationBodyBase { | ||
/** Content types. */ | ||
readonly contentTypes: string[]; | ||
} | ||
export interface HttpBody { | ||
readonly type: Type; | ||
/** If the body was explicitly set with `@body`. */ | ||
readonly isExplicit: boolean; | ||
/** If the body contains metadata annotations to ignore. For example `@header`. */ | ||
readonly containsMetadataAnnotations: boolean; | ||
/** | ||
* Content types. | ||
* @deprecated use {@link property} | ||
*/ | ||
contentTypes: string[]; | ||
parameter?: ModelProperty; | ||
/** | ||
* Type of the operation body. | ||
* If the body was explicitly set as a property. Correspond to the property with `@body` or `@bodyRoot` | ||
*/ | ||
type: Type; | ||
/** If the body was explicitly set with `@body`. */ | ||
readonly isExplicit: boolean; | ||
/** If the body contains metadata annotations to ignore. For example `@header`. */ | ||
readonly containsMetadataAnnotations: boolean; | ||
readonly property?: ModelProperty; | ||
} | ||
export interface HttpOperationBody extends HttpOperationBodyBase, HttpBody { | ||
readonly bodyKind: "single"; | ||
} | ||
/** Body marked with `@multipartBody` */ | ||
export interface HttpOperationMultipartBody extends HttpOperationBodyBase { | ||
readonly bodyKind: "multipart"; | ||
readonly type: Model | Tuple; | ||
/** Property annotated with `@multipartBody` */ | ||
readonly property: ModelProperty; | ||
readonly parts: HttpOperationPart[]; | ||
} | ||
/** Represent an part in a multipart body. */ | ||
export interface HttpOperationPart { | ||
/** Part name */ | ||
readonly name?: string; | ||
/** If the part is optional */ | ||
readonly optional: boolean; | ||
/** Part body */ | ||
readonly body: HttpOperationBody; | ||
/** Part headers */ | ||
readonly headers: HeaderProperty[]; | ||
/** If there can be multiple of that part */ | ||
readonly multi: boolean; | ||
} | ||
export interface HttpStatusCodeRange { | ||
@@ -356,0 +377,0 @@ start: number; |
{ | ||
"name": "@typespec/http", | ||
"version": "0.57.0-dev.5", | ||
"version": "0.57.0-dev.6", | ||
"author": "Microsoft Corporation", | ||
@@ -5,0 +5,0 @@ "description": "TypeSpec HTTP protocol binding", |
@@ -47,2 +47,3 @@ # @typespec/http | ||
- [`@includeInapplicableMetadataInPayload`](#@includeinapplicablemetadatainpayload) | ||
- [`@multipartBody`](#@multipartbody) | ||
- [`@patch`](#@patch) | ||
@@ -276,2 +277,28 @@ - [`@path`](#@path) | ||
#### `@multipartBody` | ||
```typespec | ||
@TypeSpec.Http.multipartBody | ||
``` | ||
##### Target | ||
`ModelProperty` | ||
##### Parameters | ||
None | ||
##### Examples | ||
```tsp | ||
op upload( | ||
@header `content-type`: "multipart/form-data", | ||
@multipartBody body: { | ||
fullName: HttpPart<string>; | ||
headShots: HttpPart<Image>[]; | ||
}, | ||
): void; | ||
``` | ||
#### `@patch` | ||
@@ -278,0 +305,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
316905
11.38%99
10%4070
11.91%573
4.95%