diff --git a/.chronus/changes/cookie-decorator-2024-9-22-1-33-4.md b/.chronus/changes/cookie-decorator-2024-9-22-1-33-4.md
new file mode 100644
index 0000000000..58b77537a6
--- /dev/null
+++ b/.chronus/changes/cookie-decorator-2024-9-22-1-33-4.md
@@ -0,0 +1,9 @@
+---
+changeKind: feature
+packages:
+ - "@typespec/http-server-javascript"
+ - "@typespec/http"
+ - "@typespec/openapi3"
+---
+
+Add `@cookie` decorator to specify cookie parameters
\ No newline at end of file
diff --git a/packages/http-server-javascript/src/http/server/index.ts b/packages/http-server-javascript/src/http/server/index.ts
index 6db97cd245..9ab47195b6 100644
--- a/packages/http-server-javascript/src/http/server/index.ts
+++ b/packages/http-server-javascript/src/http/server/index.ts
@@ -115,6 +115,8 @@ function* emitRawServerOperation(
case "header":
yield* indent(emitHeaderParamBinding(ctx, parameter));
break;
+ case "cookie":
+ throw new UnimplementedError("cookie parameters");
case "query":
queryParams.push(parameter);
parsedParams.add(resolvedParameter);
diff --git a/packages/http/README.md b/packages/http/README.md
index 2283e4ff23..5b2d406b14 100644
--- a/packages/http/README.md
+++ b/packages/http/README.md
@@ -37,6 +37,7 @@ Available ruleSets:
- [`@body`](#@body)
- [`@bodyIgnore`](#@bodyignore)
- [`@bodyRoot`](#@bodyroot)
+- [`@cookie`](#@cookie)
- [`@delete`](#@delete)
- [`@get`](#@get)
- [`@head`](#@head)
@@ -145,6 +146,47 @@ op download(): {
};
```
+#### `@cookie`
+
+Specify this property is to be sent or received in the cookie.
+
+```typespec
+@TypeSpec.Http.cookie(cookieNameOrOptions?: valueof string | TypeSpec.Http.CookieOptions)
+```
+
+##### Target
+
+`ModelProperty`
+
+##### Parameters
+
+| Name | Type | Description |
+| ------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| cookieNameOrOptions | `valueof string \| TypeSpec.Http.CookieOptions` | Optional name of the cookie in the cookie or cookie options.
By default the cookie name will be the property name converted from camelCase to snake_case. (e.g. `authToken` -> `auth_token`) |
+
+##### Examples
+
+```typespec
+op read(@cookie token: string): {
+ data: string[];
+};
+op create(
+ @cookie({
+ name: "auth_token",
+ })
+ data: string[],
+): void;
+```
+
+###### Implicit header name
+
+```typespec
+op read(): {
+ @cookie authToken: string;
+}; // headerName: auth_token
+op update(@cookie AuthToken: string): void; // headerName: auth_token
+```
+
#### `@delete`
Specify the HTTP verb for the target operation to be `DELETE`.
diff --git a/packages/http/generated-defs/TypeSpec.Http.ts b/packages/http/generated-defs/TypeSpec.Http.ts
index 75eb5e9a59..d8a20e1ffe 100644
--- a/packages/http/generated-defs/TypeSpec.Http.ts
+++ b/packages/http/generated-defs/TypeSpec.Http.ts
@@ -7,6 +7,10 @@ import type {
Type,
} from "@typespec/compiler";
+export interface CookieOptions {
+ readonly name?: string;
+}
+
export interface QueryOptions {
readonly name?: string;
readonly explode?: boolean;
@@ -73,6 +77,29 @@ export type HeaderDecorator = (
headerNameOrOptions?: Type,
) => void;
+/**
+ * Specify this property is to be sent or received in the cookie.
+ *
+ * @param cookieNameOrOptions Optional name of the cookie in the cookie or cookie options.
+ * By default the cookie name will be the property name converted from camelCase to snake_case. (e.g. `authToken` -> `auth_token`)
+ * @example
+ * ```typespec
+ * op read(@cookie token: string): {data: string[]};
+ * op create(@cookie({name: "auth_token"}) data: string[]): void;
+ * ```
+ * @example Implicit header name
+ *
+ * ```typespec
+ * op read(): {@cookie authToken: string}; // headerName: auth_token
+ * op update(@cookie AuthToken: string): void; // headerName: auth_token
+ * ```
+ */
+export type CookieDecorator = (
+ context: DecoratorContext,
+ target: ModelProperty,
+ cookieNameOrOptions?: string | CookieOptions,
+) => void;
+
/**
* Specify this property is to be sent as a query parameter.
*
@@ -328,6 +355,7 @@ export type TypeSpecHttpDecorators = {
statusCode: StatusCodeDecorator;
body: BodyDecorator;
header: HeaderDecorator;
+ cookie: CookieDecorator;
query: QueryDecorator;
path: PathDecorator;
bodyRoot: BodyRootDecorator;
diff --git a/packages/http/lib/decorators.tsp b/packages/http/lib/decorators.tsp
index 2a969214a9..4f7b1b4ea9 100644
--- a/packages/http/lib/decorators.tsp
+++ b/packages/http/lib/decorators.tsp
@@ -39,6 +39,38 @@ model HeaderOptions {
*/
extern dec header(target: ModelProperty, headerNameOrOptions?: string | HeaderOptions);
+/**
+ * Cookie Options.
+ */
+model CookieOptions {
+ /**
+ * Name in the cookie.
+ */
+ name?: string;
+}
+
+/**
+ * Specify this property is to be sent or received in the cookie.
+ *
+ * @param cookieNameOrOptions Optional name of the cookie in the cookie or cookie options.
+ * By default the cookie name will be the property name converted from camelCase to snake_case. (e.g. `authToken` -> `auth_token`)
+ *
+ * @example
+ *
+ * ```typespec
+ * op read(@cookie token: string): {data: string[]};
+ * op create(@cookie({name: "auth_token"}) data: string[]): void;
+ * ```
+ *
+ * @example Implicit header name
+ *
+ * ```typespec
+ * op read(): {@cookie authToken: string}; // headerName: auth_token
+ * op update(@cookie AuthToken: string): void; // headerName: auth_token
+ * ```
+ */
+extern dec cookie(target: ModelProperty, cookieNameOrOptions?: valueof string | CookieOptions);
+
/**
* Query parameter options.
*/
diff --git a/packages/http/src/decorators.ts b/packages/http/src/decorators.ts
index d052a81c7f..165b4f8054 100644
--- a/packages/http/src/decorators.ts
+++ b/packages/http/src/decorators.ts
@@ -26,6 +26,8 @@ import {
BodyDecorator,
BodyIgnoreDecorator,
BodyRootDecorator,
+ CookieDecorator,
+ CookieOptions,
DeleteDecorator,
GetDecorator,
HeadDecorator,
@@ -49,6 +51,7 @@ import { getStatusCodesFromType } from "./status-codes.js";
import {
Authentication,
AuthenticationOption,
+ CookieParameterOptions,
HeaderFieldOptions,
HttpAuth,
HttpStatusCodeRange,
@@ -122,6 +125,47 @@ export function isHeader(program: Program, entity: Type) {
return program.stateMap(HttpStateKeys.header).has(entity);
}
+/** {@inheritDoc CookieDecorator } */
+export const $cookie: CookieDecorator = (
+ context: DecoratorContext,
+ entity: ModelProperty,
+ cookieNameOrOptions?: string | CookieOptions,
+) => {
+ const paramName =
+ typeof cookieNameOrOptions === "string"
+ ? cookieNameOrOptions
+ : (cookieNameOrOptions?.name ??
+ entity.name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase());
+ const options: CookieParameterOptions = {
+ type: "cookie",
+ name: paramName,
+ };
+ context.program.stateMap(HttpStateKeys.cookie).set(entity, options);
+};
+
+/**
+ * Get the cookie parameter options for the given entity.
+ * @param program
+ * @param entity
+ * @returns The cookie parameter options or undefined if the entity is not a cookie parameter.
+ */
+export function getCookieParamOptions(
+ program: Program,
+ entity: Type,
+): QueryParameterOptions | undefined {
+ return program.stateMap(HttpStateKeys.cookie).get(entity);
+}
+
+/**
+ * Check whether the given entity is a cookie parameter.
+ * @param program
+ * @param entity
+ * @returns True if the entity is a cookie parameter, false otherwise.
+ */
+export function isCookieParam(program: Program, entity: Type): boolean {
+ return program.stateMap(HttpStateKeys.cookie).has(entity);
+}
+
export const $query: QueryDecorator = (
context: DecoratorContext,
entity: ModelProperty,
diff --git a/packages/http/src/http-property.ts b/packages/http/src/http-property.ts
index e82b2ee8d7..e44da5575e 100644
--- a/packages/http/src/http-property.ts
+++ b/packages/http/src/http-property.ts
@@ -10,6 +10,7 @@ import {
type Program,
} from "@typespec/compiler";
import {
+ getCookieParamOptions,
getHeaderFieldOptions,
getPathParamOptions,
getQueryParamOptions,
@@ -20,10 +21,16 @@ import {
} from "./decorators.js";
import { createDiagnostic } from "./lib.js";
import { Visibility, isVisible } from "./metadata.js";
-import { HeaderFieldOptions, PathParameterOptions, QueryParameterOptions } from "./types.js";
+import {
+ CookieParameterOptions,
+ HeaderFieldOptions,
+ PathParameterOptions,
+ QueryParameterOptions,
+} from "./types.js";
export type HttpProperty =
| HeaderProperty
+ | CookieProperty
| ContentTypeProperty
| QueryProperty
| PathProperty
@@ -44,6 +51,11 @@ export interface HeaderProperty extends HttpPropertyBase {
readonly options: HeaderFieldOptions;
}
+export interface CookieProperty extends HttpPropertyBase {
+ readonly kind: "cookie";
+ readonly options: CookieParameterOptions;
+}
+
export interface ContentTypeProperty extends HttpPropertyBase {
readonly kind: "contentType";
}
@@ -96,6 +108,7 @@ function getHttpProperty(
const annotations = {
header: getHeaderFieldOptions(program, property),
+ cookie: getCookieParamOptions(program, property),
query: getQueryParamOptions(program, property),
path: getPathParamOptions(program, property),
body: isBody(program, property),
@@ -174,6 +187,8 @@ function getHttpProperty(
} else {
return createResult({ kind: "header", options: annotations.header });
}
+ } else if (annotations.cookie) {
+ return createResult({ kind: "cookie", options: annotations.cookie });
} else if (annotations.query) {
return createResult({ kind: "query", options: annotations.query });
} else if (annotations.path) {
@@ -225,6 +240,19 @@ export function resolvePayloadProperties(
httpProperty = { kind: "bodyProperty", property, path: propPath };
}
+ // Ignore cookies in response to avoid future breaking changes to @cookie.
+ // https://github.com/microsoft/typespec/pull/4761#discussion_r1805082132
+ if (httpProperty.kind === "cookie" && visibility & Visibility.Read) {
+ diagnostics.add(
+ createDiagnostic({
+ code: "response-cookie-not-supported",
+ target: property,
+ format: { propName: property.name },
+ }),
+ );
+ continue;
+ }
+
if (
httpProperty.kind === "body" ||
httpProperty.kind === "bodyRoot" ||
diff --git a/packages/http/src/lib.ts b/packages/http/src/lib.ts
index b4102a819e..235c2412a4 100644
--- a/packages/http/src/lib.ts
+++ b/packages/http/src/lib.ts
@@ -100,6 +100,12 @@ export const $lib = createTypeSpecLibrary({
default: paramMessage`${"kind"} property will be ignored as it is inside of a @body property. Use @bodyRoot instead if wanting to mix.`,
},
},
+ "response-cookie-not-supported": {
+ severity: "warning",
+ messages: {
+ default: paramMessage`@cookie on response is not supported. Property '${"propName"}' will be ignored in the body. If you need 'Set-Cookie', use @header instead.`,
+ },
+ },
"no-service-found": {
severity: "warning",
messages: {
@@ -170,6 +176,7 @@ export const $lib = createTypeSpecLibrary({
state: {
authentication: { description: "State for the @auth decorator" },
header: { description: "State for the @header decorator" },
+ cookie: { description: "State for the @cookie decorator" },
query: { description: "State for the @query decorator" },
path: { description: "State for the @path decorator" },
body: { description: "State for the @body decorator" },
diff --git a/packages/http/src/metadata.ts b/packages/http/src/metadata.ts
index 1c0de783cb..093f22407b 100644
--- a/packages/http/src/metadata.ts
+++ b/packages/http/src/metadata.ts
@@ -16,6 +16,7 @@ import {
isBody,
isBodyIgnore,
isBodyRoot,
+ isCookieParam,
isHeader,
isMultipartBodyProperty,
isPathParam,
@@ -219,11 +220,12 @@ export function resolveRequestVisibility(
/**
* Determines if a property is metadata. A property is defined to be
- * metadata if it is marked `@header`, `@query`, `@path`, or `@statusCode`.
+ * metadata if it is marked `@header`, `@cookie`, `@query`, `@path`, or `@statusCode`.
*/
export function isMetadata(program: Program, property: ModelProperty) {
return (
isHeader(program, property) ||
+ isCookieParam(program, property) ||
isQueryParam(program, property) ||
isPathParam(program, property) ||
isStatusCode(program, property)
diff --git a/packages/http/src/parameters.ts b/packages/http/src/parameters.ts
index 61603f7b02..b41de87acb 100644
--- a/packages/http/src/parameters.ts
+++ b/packages/http/src/parameters.ts
@@ -126,6 +126,7 @@ function getOperationParametersForVerb(
}
// eslint-disable-next-line no-fallthrough
case "query":
+ case "cookie":
case "header":
parameters.push({
...item.options,
diff --git a/packages/http/src/payload.ts b/packages/http/src/payload.ts
index 441195fe81..c647bff792 100644
--- a/packages/http/src/payload.ts
+++ b/packages/http/src/payload.ts
@@ -15,7 +15,7 @@ import {
} from "@typespec/compiler";
import { DuplicateTracker } from "@typespec/compiler/utils";
import { getContentTypes } from "./content-types.js";
-import { isHeader, isPathParam, isQueryParam, isStatusCode } from "./decorators.js";
+import { isCookieParam, isHeader, isPathParam, isQueryParam, isStatusCode } from "./decorators.js";
import {
GetHttpPropertyOptions,
HeaderProperty,
@@ -259,13 +259,16 @@ function validateBodyProperty(
modelProperty: (prop) => {
const kind = isHeader(program, prop)
? "header"
- : (usedIn === "request" || usedIn === "multipart") && isQueryParam(program, prop)
- ? "query"
- : usedIn === "request" && isPathParam(program, prop)
- ? "path"
- : usedIn === "response" && isStatusCode(program, prop)
- ? "statusCode"
- : undefined;
+ : // also emit metadata-ignored for response cookie
+ (usedIn === "request" || usedIn === "response") && isCookieParam(program, prop)
+ ? "cookie"
+ : (usedIn === "request" || usedIn === "multipart") && isQueryParam(program, prop)
+ ? "query"
+ : usedIn === "request" && isPathParam(program, prop)
+ ? "path"
+ : usedIn === "response" && isStatusCode(program, prop)
+ ? "statusCode"
+ : undefined;
if (kind) {
diagnostics.add(
diff --git a/packages/http/src/tsp-index.ts b/packages/http/src/tsp-index.ts
index 11c34db444..c7314615e6 100644
--- a/packages/http/src/tsp-index.ts
+++ b/packages/http/src/tsp-index.ts
@@ -3,6 +3,7 @@ import {
$body,
$bodyIgnore,
$bodyRoot,
+ $cookie,
$delete,
$get,
$head,
@@ -30,6 +31,7 @@ export const $decorators = {
body: $body,
bodyIgnore: $bodyIgnore,
bodyRoot: $bodyRoot,
+ cookie: $cookie,
delete: $delete,
get: $get,
header: $header,
diff --git a/packages/http/src/types.ts b/packages/http/src/types.ts
index 6bb7989f96..f2076ab15a 100644
--- a/packages/http/src/types.ts
+++ b/packages/http/src/types.ts
@@ -10,7 +10,7 @@ import {
Tuple,
Type,
} from "@typespec/compiler";
-import { PathOptions, QueryOptions } from "../generated-defs/TypeSpec.Http.js";
+import { CookieOptions, PathOptions, QueryOptions } from "../generated-defs/TypeSpec.Http.js";
import { HeaderProperty, HttpProperty } from "./http-property.js";
/**
@@ -299,6 +299,11 @@ export interface HeaderFieldOptions {
format?: "csv" | "multi" | "ssv" | "tsv" | "pipes" | "simple" | "form";
}
+export interface CookieParameterOptions extends Required {
+ type: "cookie";
+ name: string;
+}
+
export interface QueryParameterOptions extends Required> {
type: "query";
/**
@@ -313,12 +318,16 @@ export interface PathParameterOptions extends Required {
export type HttpOperationParameter =
| HttpOperationHeaderParameter
+ | HttpOperationCookieParameter
| HttpOperationQueryParameter
| HttpOperationPathParameter;
export type HttpOperationHeaderParameter = HeaderFieldOptions & {
param: ModelProperty;
};
+export type HttpOperationCookieParameter = CookieParameterOptions & {
+ param: ModelProperty;
+};
export type HttpOperationQueryParameter = QueryParameterOptions & {
param: ModelProperty;
};
diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts
index 6b8bb03621..a54cedfd9d 100644
--- a/packages/http/test/http-decorators.test.ts
+++ b/packages/http/test/http-decorators.test.ts
@@ -8,6 +8,7 @@ import { deepStrictEqual, ok, strictEqual } from "assert";
import { beforeEach, describe, expect, it } from "vitest";
import {
getAuthentication,
+ getCookieParamOptions,
getHeaderFieldName,
getHeaderFieldOptions,
getPathParamName,
@@ -20,6 +21,7 @@ import {
isBody,
isBodyIgnore,
isBodyRoot,
+ isCookieParam,
isHeader,
isPathParam,
isQueryParam,
@@ -132,6 +134,74 @@ describe("http: decorators", () => {
});
});
+ describe("@cookie", () => {
+ it("emit diagnostics when @cookie is not used on model property", async () => {
+ const diagnostics = await runner.diagnose(`
+ @cookie op test(): string;
+
+ @cookie model Foo {}
+ `);
+
+ expectDiagnostics(diagnostics, [
+ {
+ code: "decorator-wrong-target",
+ message:
+ "Cannot apply @cookie decorator to test since it is not assignable to ModelProperty",
+ },
+ {
+ code: "decorator-wrong-target",
+ message:
+ "Cannot apply @cookie decorator to Foo since it is not assignable to ModelProperty",
+ },
+ ]);
+ });
+
+ it("emit diagnostics when cookie name is not a string or of type CookieOptions", async () => {
+ const diagnostics = await runner.diagnose(`
+ op test(@cookie(123) MyCookie: string): string;
+ op test2(@cookie(#{ name: 123 }) MyCookie: string): string;
+ op test3(@cookie(#{ format: "invalid" }) MyCookie: string): string;
+ `);
+
+ expectDiagnostics(diagnostics, [
+ {
+ code: "invalid-argument",
+ },
+ {
+ code: "invalid-argument",
+ },
+ {
+ code: "invalid-argument",
+ },
+ ]);
+ });
+
+ it("generate cookie name from property name", async () => {
+ const { myCookie } = await runner.compile(`
+ op test(@test @cookie myCookie: string): string;
+ `);
+
+ ok(isCookieParam(runner.program, myCookie));
+ strictEqual(getCookieParamOptions(runner.program, myCookie)?.name, "my_cookie");
+ });
+
+ it("override cookie name with 1st parameter", async () => {
+ const { myCookie } = await runner.compile(`
+ op test(@test @cookie("my-cookie") myCookie: string): string;
+ `);
+
+ strictEqual(getCookieParamOptions(runner.program, myCookie)?.name, "my-cookie");
+ });
+
+ it("override cookie with CookieOptions", async () => {
+ const { myCookie } = await runner.compile(`
+ op test(@test @cookie(#{name: "my-cookie"}) myCookie: string): string;
+ `);
+
+ strictEqual(getCookieParamOptions(runner.program, myCookie)?.name, "my-cookie");
+ });
+ });
+
describe("@query", () => {
it("emit diagnostics when @query is not used on model property", async () => {
const diagnostics = await runner.diagnose(`
diff --git a/packages/http/test/parameters.test.ts b/packages/http/test/parameters.test.ts
index ef3ab3a8c6..79b460d629 100644
--- a/packages/http/test/parameters.test.ts
+++ b/packages/http/test/parameters.test.ts
@@ -210,6 +210,7 @@ it("resolves unannotated path parameters that are included in the route path", a
describe("emit diagnostics when using metadata decorator in @body", () => {
it.each([
["@header", "id: string"],
+ ["@cookie", "id: string"],
["@query", "id: string"],
["@path", "id: string"],
])("%s", async (dec, prop) => {
diff --git a/packages/http/test/responses.test.ts b/packages/http/test/responses.test.ts
index 68d6ad3d6b..99a23376ea 100644
--- a/packages/http/test/responses.test.ts
+++ b/packages/http/test/responses.test.ts
@@ -64,6 +64,24 @@ describe("body resolution", () => {
});
});
+describe("response cookie", () => {
+ it("emit diagnostics for implicit @cookie in the response", async () => {
+ const [_, diagnostics] = await compileOperations(`
+ op get(): { @cookie token: string };
+ `);
+
+ expectDiagnostics(diagnostics, { code: "@typespec/http/response-cookie-not-supported" });
+ });
+
+ it("doesn't emit response-cookie-not-supported diagnostics for explicit @cookie in the response", async () => {
+ const [_, diagnostics] = await compileOperations(`
+ op get(): { @body explicit: { @cookie token: string } };
+ `);
+
+ expectDiagnostics(diagnostics, { code: "@typespec/http/metadata-ignored" });
+ });
+});
+
it("doesn't emit diagnostic if the metadata is not applicable in the response", async () => {
const [_, diagnostics] = await compileOperations(
`op read(): { @body explicit: {@path id: string} };`,
diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts
index 84f595fc3d..ff48b979fa 100644
--- a/packages/openapi3/src/openapi.ts
+++ b/packages/openapi3/src/openapi.ts
@@ -1413,6 +1413,10 @@ function createOAPIEmitter(
switch (parameter.type) {
case "header":
return mapHeaderParameterFormat(parameter);
+ case "cookie":
+ // style and explode options are omitted from cookies
+ // https://github.com/microsoft/typespec/pull/4761#discussion_r1803365689
+ return { explode: false };
case "query":
return getQueryParameterAttributes(parameter);
case "path":
diff --git a/packages/openapi3/test/metadata.test.ts b/packages/openapi3/test/metadata.test.ts
index aa108d9085..de877882cb 100644
--- a/packages/openapi3/test/metadata.test.ts
+++ b/packages/openapi3/test/metadata.test.ts
@@ -610,6 +610,7 @@ describe("openapi3: metadata", () => {
@query q: string;
@path p: string;
@header h: string;
+ @cookie c: string;
}
@route("/single") @get op single(...Parameters): string;
@route("/batch") @get op batch(@bodyRoot _: Parameters[]): string;
@@ -623,6 +624,7 @@ describe("openapi3: metadata", () => {
{ $ref: "#/components/parameters/Parameters.q" },
{ $ref: "#/components/parameters/Parameters.p" },
{ $ref: "#/components/parameters/Parameters.h" },
+ { $ref: "#/components/parameters/Parameters.c" },
],
responses: {
"200": {
@@ -677,10 +679,20 @@ describe("openapi3: metadata", () => {
required: true,
schema: { type: "string" },
},
+ "Parameters.c": {
+ name: "c",
+ in: "cookie",
+ explode: false,
+ required: true,
+ schema: { type: "string" },
+ },
},
schemas: {
Parameters: {
properties: {
+ c: {
+ type: "string",
+ },
h: {
type: "string",
},
@@ -691,7 +703,7 @@ describe("openapi3: metadata", () => {
type: "string",
},
},
- required: ["q", "p", "h"],
+ required: ["q", "p", "h", "c"],
type: "object",
},
},
@@ -704,6 +716,7 @@ describe("openapi3: metadata", () => {
@route("/test") @post op test(
@query q: string;
@header h: string;
+ @cookie c: string;
foo: string;
bar: int32;
): string;
@@ -727,6 +740,13 @@ describe("openapi3: metadata", () => {
required: true,
schema: { type: "string" },
},
+ {
+ name: "c",
+ in: "cookie",
+ required: true,
+ explode: false,
+ schema: { type: "string" },
+ },
],
responses: {
"200": {
@@ -766,6 +786,7 @@ describe("openapi3: metadata", () => {
@query q: string;
@path p: string;
@header h: string;
+ @cookie c: string;
}
@route("/batch") @post op batch(@bodyRoot body?: Parameters[]): string;
`,
@@ -799,6 +820,9 @@ describe("openapi3: metadata", () => {
schemas: {
Parameters: {
properties: {
+ c: {
+ type: "string",
+ },
h: {
type: "string",
},
@@ -809,7 +833,7 @@ describe("openapi3: metadata", () => {
type: "string",
},
},
- required: ["q", "p", "h"],
+ required: ["q", "p", "h", "c"],
type: "object",
},
},
diff --git a/packages/openapi3/test/parameters.test.ts b/packages/openapi3/test/parameters.test.ts
index d813a4bfa8..4f7454f78a 100644
--- a/packages/openapi3/test/parameters.test.ts
+++ b/packages/openapi3/test/parameters.test.ts
@@ -240,6 +240,27 @@ describe("query parameters", () => {
});
});
+ it("create a cookie param", async () => {
+ const res = await openApiFor(
+ `
+ op test(@cookie arg1: string): void;
+ `,
+ );
+ strictEqual(res.paths["/"].get.parameters[0].in, "cookie");
+ strictEqual(res.paths["/"].get.parameters[0].name, "arg1");
+ deepStrictEqual(res.paths["/"].get.parameters[0].schema, { type: "string" });
+ });
+
+ it("create a cookie param with a different name", async () => {
+ const res = await openApiFor(
+ `
+ op test(@cookie("foo_bar") foo: string): void;
+ `,
+ );
+ strictEqual(res.paths["/"].get.parameters[0].in, "cookie");
+ strictEqual(res.paths["/"].get.parameters[0].name, "foo_bar");
+ });
+
// Regression test for https://github.com/microsoft/typespec/issues/414
it("@doc set the description on the parameter not its schema", async () => {
const res = await openApiFor(
@@ -347,6 +368,8 @@ describe("query parameters", () => {
#suppress "@typespec/http/metadata-ignored"
@header header: string,
#suppress "@typespec/http/metadata-ignored"
+ @cookie cookie: string,
+ #suppress "@typespec/http/metadata-ignored"
@query query: string,
#suppress "@typespec/http/metadata-ignored"
@statusCode code: 201,
@@ -356,10 +379,11 @@ describe("query parameters", () => {
type: "object",
properties: {
header: { type: "string" },
+ cookie: { type: "string" },
query: { type: "string" },
code: { type: "number", enum: [201] },
},
- required: ["header", "query", "code"],
+ required: ["header", "cookie", "query", "code"],
});
});
@@ -379,15 +403,20 @@ describe("query parameters", () => {
@header header1: string;
@header header2: string;
};
+ cookies: {
+ @cookie cookie1: string;
+ @cookie cookie2: string;
+ };
name: string;
): void;`);
expect(res.paths["/"].post.requestBody.content["application/json"].schema).toEqual({
type: "object",
properties: {
headers: { type: "object" },
+ cookies: { type: "object" },
name: { type: "string" },
},
- required: ["headers", "name"],
+ required: ["headers", "cookies", "name"],
});
});
@@ -397,6 +426,10 @@ describe("query parameters", () => {
@header header1: string;
@header header2: string;
};
+ @bodyIgnore cookies: {
+ @cookie cookie1: string;
+ @cookie cookie2: string;
+ };
name: string;
): void;`);
expect(res.paths["/"].post.requestBody.content["application/json"].schema).toEqual({
diff --git a/packages/openapi3/test/return-types.test.ts b/packages/openapi3/test/return-types.test.ts
index 9cfe47b3b8..d647cde7b2 100644
--- a/packages/openapi3/test/return-types.test.ts
+++ b/packages/openapi3/test/return-types.test.ts
@@ -435,6 +435,8 @@ describe("openapi3: return types", () => {
#suppress "@typespec/http/metadata-ignored"
@header header: string,
#suppress "@typespec/http/metadata-ignored"
+ @cookie cookie: string,
+ #suppress "@typespec/http/metadata-ignored"
@query query: string,
#suppress "@typespec/http/metadata-ignored"
@statusCode code: 201,
@@ -444,10 +446,11 @@ describe("openapi3: return types", () => {
type: "object",
properties: {
header: { type: "string" },
+ cookie: { type: "string" },
query: { type: "string" },
code: { type: "number", enum: [201] },
},
- required: ["header", "query", "code"],
+ required: ["header", "cookie", "query", "code"],
});
});
@@ -496,6 +499,24 @@ describe("openapi3: return types", () => {
});
});
+ it("invalid metadata properties in body should still be included but response cookies should not be included", async () => {
+ const res = await openApiFor(`op read(): {
+ @header header: string;
+ #suppress "@typespec/http/response-cookie-not-supported"
+ @cookie cookie: string;
+ @query query: string;
+ name: string;
+ };`);
+ expect(res.paths["/"].get.responses["200"].content["application/json"].schema).toEqual({
+ type: "object",
+ properties: {
+ query: { type: "string" },
+ name: { type: "string" },
+ },
+ required: ["query", "name"],
+ });
+ });
+
describe("multiple content types", () => {
it("handles multiple content types for the same status code", async () => {
const res = await openApiFor(
diff --git a/website/src/content/docs/docs/libraries/http/reference/data-types.md b/website/src/content/docs/docs/libraries/http/reference/data-types.md
index 8cfee7d5a5..3e2a068c57 100644
--- a/website/src/content/docs/docs/libraries/http/reference/data-types.md
+++ b/website/src/content/docs/docs/libraries/http/reference/data-types.md
@@ -1,5 +1,7 @@
---
title: "Data types"
+toc_min_heading_level: 2
+toc_max_heading_level: 3
---
## TypeSpec.Http
@@ -187,6 +189,20 @@ model TypeSpec.Http.ConflictResponse
| ---------- | ----- | ---------------- |
| statusCode | `409` | The status code. |
+### `CookieOptions` {#TypeSpec.Http.CookieOptions}
+
+Cookie Options.
+
+```typespec
+model TypeSpec.Http.CookieOptions
+```
+
+#### Properties
+
+| Name | Type | Description |
+| ----- | -------- | ------------------- |
+| name? | `string` | Name in the cookie. |
+
### `CreatedResponse` {#TypeSpec.Http.CreatedResponse}
The request has succeeded and a new resource has been created as a result.
diff --git a/website/src/content/docs/docs/libraries/http/reference/decorators.md b/website/src/content/docs/docs/libraries/http/reference/decorators.md
index bc5785cfac..ee18e8266b 100644
--- a/website/src/content/docs/docs/libraries/http/reference/decorators.md
+++ b/website/src/content/docs/docs/libraries/http/reference/decorators.md
@@ -97,6 +97,47 @@ op download(): {
};
```
+### `@cookie` {#@TypeSpec.Http.cookie}
+
+Specify this property is to be sent or received in the cookie.
+
+```typespec
+@TypeSpec.Http.cookie(cookieNameOrOptions?: valueof string | TypeSpec.Http.CookieOptions)
+```
+
+#### Target
+
+`ModelProperty`
+
+#### Parameters
+
+| Name | Type | Description |
+| ------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| cookieNameOrOptions | `valueof string \| TypeSpec.Http.CookieOptions` | Optional name of the cookie in the cookie or cookie options.
By default the cookie name will be the property name converted from camelCase to snake_case. (e.g. `authToken` -> `auth_token`) |
+
+#### Examples
+
+```typespec
+op read(@cookie token: string): {
+ data: string[];
+};
+op create(
+ @cookie({
+ name: "auth_token",
+ })
+ data: string[],
+): void;
+```
+
+##### Implicit header name
+
+```typespec
+op read(): {
+ @cookie authToken: string;
+}; // headerName: auth_token
+op update(@cookie AuthToken: string): void; // headerName: auth_token
+```
+
### `@delete` {#@TypeSpec.Http.delete}
Specify the HTTP verb for the target operation to be `DELETE`.
diff --git a/website/src/content/docs/docs/libraries/http/reference/index.mdx b/website/src/content/docs/docs/libraries/http/reference/index.mdx
index 865df0b23c..4eb94114d9 100644
--- a/website/src/content/docs/docs/libraries/http/reference/index.mdx
+++ b/website/src/content/docs/docs/libraries/http/reference/index.mdx
@@ -36,6 +36,7 @@ npm install --save-peer @typespec/http
- [`@body`](./decorators.md#@TypeSpec.Http.body)
- [`@bodyIgnore`](./decorators.md#@TypeSpec.Http.bodyIgnore)
- [`@bodyRoot`](./decorators.md#@TypeSpec.Http.bodyRoot)
+- [`@cookie`](./decorators.md#@TypeSpec.Http.cookie)
- [`@delete`](./decorators.md#@TypeSpec.Http.delete)
- [`@get`](./decorators.md#@TypeSpec.Http.get)
- [`@head`](./decorators.md#@TypeSpec.Http.head)
@@ -64,6 +65,7 @@ npm install --save-peer @typespec/http
- [`Body`](./data-types.md#TypeSpec.Http.Body)
- [`ClientCredentialsFlow`](./data-types.md#TypeSpec.Http.ClientCredentialsFlow)
- [`ConflictResponse`](./data-types.md#TypeSpec.Http.ConflictResponse)
+- [`CookieOptions`](./data-types.md#TypeSpec.Http.CookieOptions)
- [`CreatedResponse`](./data-types.md#TypeSpec.Http.CreatedResponse)
- [`File`](./data-types.md#TypeSpec.Http.File)
- [`ForbiddenResponse`](./data-types.md#TypeSpec.Http.ForbiddenResponse)
diff --git a/website/src/content/docs/docs/libraries/http/reference/linter.md b/website/src/content/docs/docs/libraries/http/reference/linter.md
index 3b4e99e7f4..5440e045d1 100644
--- a/website/src/content/docs/docs/libraries/http/reference/linter.md
+++ b/website/src/content/docs/docs/libraries/http/reference/linter.md
@@ -1,5 +1,7 @@
---
title: "Linter usage"
+toc_min_heading_level: 2
+toc_max_heading_level: 3
---
## Usage