From 214a1e808f1c90be7d34da173d9d64e24c8a91b1 Mon Sep 17 00:00:00 2001 From: subaru Date: Wed, 16 Oct 2024 10:07:21 +0900 Subject: [PATCH 01/11] define cookie decorator --- packages/http/generated-defs/TypeSpec.Http.ts | 30 +++++++++++++ packages/http/lib/decorators.tsp | 44 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/packages/http/generated-defs/TypeSpec.Http.ts b/packages/http/generated-defs/TypeSpec.Http.ts index 9549772607..c00a954b5c 100644 --- a/packages/http/generated-defs/TypeSpec.Http.ts +++ b/packages/http/generated-defs/TypeSpec.Http.ts @@ -7,6 +7,11 @@ import type { Type, } from "@typespec/compiler"; +export interface CookieOptions { + readonly name?: string; + readonly explode?: boolean; +} + export interface QueryOptions { readonly name?: string; readonly explode?: boolean; @@ -73,6 +78,30 @@ 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 authToken: string): {@header("ETag") eTag: string}; + * op create(@header({name: "X-Color", format: "csv"}) colors: string[]): void; + * ``` + * simple + * @example Implicit header name + * + * ```typespec + * op read(): {@header contentType: string}; // headerName: content-type + * op update(@header ifMatch: string): void; // headerName: if-match + * ``` + */ +export type CookieDecorator = ( + context: DecoratorContext, + target: ModelProperty, + cookieNameOrOptions?: string | CookieOptions, +) => void; + /** * Specify this property is to be sent as a query parameter. * @@ -321,6 +350,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 22267a2086..3c9ea6a9a4 100644 --- a/packages/http/lib/decorators.tsp +++ b/packages/http/lib/decorators.tsp @@ -39,6 +39,50 @@ model HeaderOptions { */ extern dec header(target: ModelProperty, headerNameOrOptions?: string | HeaderOptions); +/** + * Cookie Options. + */ +model CookieOptions { + /** + * Name in the cookie. + */ + name?: string; + + /** + * If false the value will be joined with `,`. + * + * | Style | Explode | Uri Template | Primitive value id = 5 | Array id = [3, 4, 5] | Object id = {"role": "admin", "firstName": "Alex"} | + * | ----- | ------- | -------------| ---------------------- | -------------------- | -------------------------------------------------- | + * | form | true | ` ` | `Cookie: id=5` | | | + * | form | false | `id={id}` | `Cookie: id=5` | `Cookie: id=3,4,5 `| `Cookie: id=role,admin,firstName,Alex` | + * + */ + explode?: boolean; +} + +// TODO: example +/** + * 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 authToken: string): {@header("ETag") eTag: string}; + * op create(@header({name: "X-Color", format: "csv"}) colors: string[]): void; + * ``` + *simple + * @example Implicit header name + * + * ```typespec + * op read(): {@header contentType: string}; // headerName: content-type + * op update(@header ifMatch: string): void; // headerName: if-match + * ``` + */ +extern dec cookie(target: ModelProperty, cookieNameOrOptions?: valueof string | CookieOptions); + /** * Query parameter options. */ From 3a01550f1d75df19574c3292edc6f2e3a6d6d8fa Mon Sep 17 00:00:00 2001 From: subaru Date: Wed, 16 Oct 2024 10:08:07 +0900 Subject: [PATCH 02/11] implement cookie decorator --- packages/http/src/decorators.ts | 35 ++++++++++++++++++++++++++++++ packages/http/src/http-property.ts | 13 ++++++++++- packages/http/src/lib.ts | 1 + packages/http/src/tsp-index.ts | 2 ++ packages/http/src/types.ts | 12 +++++++++- 5 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/http/src/decorators.ts b/packages/http/src/decorators.ts index 5f9e089c72..76813941a6 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,38 @@ export function isHeader(program: Program, entity: Type) { return program.stateMap(HttpStateKeys.header).has(entity); } +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 userOptions = typeof cookieNameOrOptions === "object" ? cookieNameOrOptions : {}; + const options: CookieParameterOptions = { + type: "cookie", + name: paramName, + explode: userOptions.explode ?? true, + format: "form", + }; + context.program.stateMap(HttpStateKeys.cookie).set(entity, options); +}; + +export function getCookieParamOptions(program: Program, entity: Type): QueryParameterOptions { + return program.stateMap(HttpStateKeys.cookie).get(entity); +} + +export function getCookieParamName(program: Program, entity: Type): string { + return getCookieParamOptions(program, entity)?.name; +} + +export function isCookieParam(program: Program, entity: Type) { + 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..214f2439ba 100644 --- a/packages/http/src/http-property.ts +++ b/packages/http/src/http-property.ts @@ -20,10 +20,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 +50,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"; } diff --git a/packages/http/src/lib.ts b/packages/http/src/lib.ts index b4102a819e..fd91653dfb 100644 --- a/packages/http/src/lib.ts +++ b/packages/http/src/lib.ts @@ -170,6 +170,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/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..695e9e36f6 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,16 @@ export interface HeaderFieldOptions { format?: "csv" | "multi" | "ssv" | "tsv" | "pipes" | "simple" | "form"; } +export interface CookieParameterOptions extends Required> { + type: "cookie"; + name: string; + /** + * The string format of the array. "csv" and "simple" are used interchangeably, as are + * "multi" and "form". + */ + format?: "csv" | "multi" | "ssv" | "tsv" | "pipes" | "simple" | "form"; +} + export interface QueryParameterOptions extends Required> { type: "query"; /** From 5acaf1c4c029d9778c78dce1a44ada52953babc3 Mon Sep 17 00:00:00 2001 From: subaru Date: Wed, 16 Oct 2024 10:08:29 +0900 Subject: [PATCH 03/11] add tests for cookie --- packages/http/test/http-decorators.test.ts | 95 ++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index 8fe4350af6..b769739c56 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -8,6 +8,8 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, expect, it } from "vitest"; import { getAuthentication, + getCookieParamName, + getCookieParamOptions, getHeaderFieldName, getHeaderFieldOptions, getPathParamName, @@ -20,6 +22,7 @@ import { isBody, isBodyIgnore, isBodyRoot, + isCookieParam, isHeader, isPathParam, isQueryParam, @@ -132,6 +135,98 @@ 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(getCookieParamName(runner.program, myCookie), "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(getCookieParamName(runner.program, myCookie), "my-cookie"); + }); + + it("override cookie with CookieOptions", async () => { + const { myCookie } = await runner.compile(` + op test(@test @cookie(#{name: "my-cookie"}) myCookie: string): string; + `); + + strictEqual(getCookieParamName(runner.program, myCookie), "my-cookie"); + }); + + it("set default explode: true", async () => { + const { myCookie } = await runner.compile(` + op test(@test @cookie myCookie: string): string; + `); + expect(getCookieParamOptions(runner.program, myCookie)).toEqual({ + type: "cookie", + name: "my_cookie", + explode: true, + format: "form", + }); + }); + + it("specify explode: false", async () => { + const { myCookie } = await runner.compile(` + op test(@test @cookie(#{explode: false}) myCookie: string): string; + `); + expect(getCookieParamOptions(runner.program, myCookie)).toEqual({ + type: "cookie", + name: "my_cookie", + explode: false, + format: "form", + }); + }); + }); + describe("@query", () => { it("emit diagnostics when @query is not used on model property", async () => { const diagnostics = await runner.diagnose(` From 5aa92cc7c186ac949e0ae42961905d747e21c952 Mon Sep 17 00:00:00 2001 From: subaru Date: Wed, 16 Oct 2024 10:14:38 +0900 Subject: [PATCH 04/11] update example --- packages/http/lib/decorators.tsp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/http/lib/decorators.tsp b/packages/http/lib/decorators.tsp index 3c9ea6a9a4..2c959c03b5 100644 --- a/packages/http/lib/decorators.tsp +++ b/packages/http/lib/decorators.tsp @@ -60,7 +60,6 @@ model CookieOptions { explode?: boolean; } -// TODO: example /** * Specify this property is to be sent or received in the cookie. * @@ -70,15 +69,15 @@ model CookieOptions { * @example * * ```typespec - * op read(@cookie authToken: string): {@header("ETag") eTag: string}; - * op create(@header({name: "X-Color", format: "csv"}) colors: string[]): void; + * op read(@cookie token: string): {data: string[]}; + * op create(@cookie({name: "auth_token"}) data: string[]): void; * ``` - *simple + * * @example Implicit header name * * ```typespec - * op read(): {@header contentType: string}; // headerName: content-type - * op update(@header ifMatch: string): void; // headerName: if-match + * 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); From 6bad2fbbb73bd2f381527e11d774657a8fca0d9d Mon Sep 17 00:00:00 2001 From: subaru Date: Wed, 16 Oct 2024 10:37:43 +0900 Subject: [PATCH 05/11] regen-docs --- docs/libraries/http/reference/data-types.md | 15 ++++++++ docs/libraries/http/reference/decorators.md | 41 ++++++++++++++++++++ docs/libraries/http/reference/index.mdx | 2 + packages/http/README.md | 42 +++++++++++++++++++++ 4 files changed, 100 insertions(+) diff --git a/docs/libraries/http/reference/data-types.md b/docs/libraries/http/reference/data-types.md index e36be1cd43..e621b1bac5 100644 --- a/docs/libraries/http/reference/data-types.md +++ b/docs/libraries/http/reference/data-types.md @@ -189,6 +189,21 @@ 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. | +| explode? | `boolean` | If false the value will be joined with `,`.

\| Style \| Explode \| Uri Template \| Primitive value id = 5 \| Array id = [3, 4, 5] \| Object id = {"role": "admin", "firstName": "Alex"} \|
\| ----- \| ------- \| -------------\| ---------------------- \| -------------------- \| -------------------------------------------------- \|
\| form \| true \| ` ` \| `Cookie: id=5` \| \| \|
\| form \| false \| `id={id}` \| `Cookie: id=5` \| `Cookie: id=3,4,5 `\| `Cookie: id=role,admin,firstName,Alex` \| | + ### `CreatedResponse` {#TypeSpec.Http.CreatedResponse} The request has succeeded and a new resource has been created as a result. diff --git a/docs/libraries/http/reference/decorators.md b/docs/libraries/http/reference/decorators.md index d4f2a49f0e..612194ade6 100644 --- a/docs/libraries/http/reference/decorators.md +++ b/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/docs/libraries/http/reference/index.mdx b/docs/libraries/http/reference/index.mdx index 865df0b23c..4eb94114d9 100644 --- a/docs/libraries/http/reference/index.mdx +++ b/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/packages/http/README.md b/packages/http/README.md index f96ba37e1a..2481e73c7a 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`. From 3dc5f984eb3b08ab96ff2b5adb87293c0126d640 Mon Sep 17 00:00:00 2001 From: subaru Date: Wed, 16 Oct 2024 11:13:10 +0900 Subject: [PATCH 06/11] regen defs --- packages/http/generated-defs/TypeSpec.Http.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/http/generated-defs/TypeSpec.Http.ts b/packages/http/generated-defs/TypeSpec.Http.ts index c00a954b5c..f674c0e3cc 100644 --- a/packages/http/generated-defs/TypeSpec.Http.ts +++ b/packages/http/generated-defs/TypeSpec.Http.ts @@ -85,15 +85,14 @@ export type HeaderDecorator = ( * 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 authToken: string): {@header("ETag") eTag: string}; - * op create(@header({name: "X-Color", format: "csv"}) colors: string[]): void; + * op read(@cookie token: string): {data: string[]}; + * op create(@cookie({name: "auth_token"}) data: string[]): void; * ``` - * simple * @example Implicit header name * * ```typespec - * op read(): {@header contentType: string}; // headerName: content-type - * op update(@header ifMatch: string): void; // headerName: if-match + * op read(): {@cookie authToken: string}; // headerName: auth_token + * op update(@cookie AuthToken: string): void; // headerName: auth_token * ``` */ export type CookieDecorator = ( From 1682f662a85277d53b6145c78130230abeb40c1d Mon Sep 17 00:00:00 2001 From: subaru Date: Wed, 16 Oct 2024 11:13:36 +0900 Subject: [PATCH 07/11] add cookie impl as parameters --- packages/http/src/http-property.ts | 4 ++++ packages/http/src/parameters.ts | 1 + packages/http/src/types.ts | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/packages/http/src/http-property.ts b/packages/http/src/http-property.ts index 214f2439ba..434640811c 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, @@ -107,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), @@ -185,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) { 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/types.ts b/packages/http/src/types.ts index 695e9e36f6..cada59fff1 100644 --- a/packages/http/src/types.ts +++ b/packages/http/src/types.ts @@ -323,12 +323,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; }; From b9656c3c65d8ab1880205f2e1a99f6b4946b8fb9 Mon Sep 17 00:00:00 2001 From: subaru Date: Wed, 16 Oct 2024 19:24:43 +0900 Subject: [PATCH 08/11] detect cookie parameters as metadata --- packages/http/src/metadata.ts | 4 +++- packages/http/src/payload.ts | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) 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/payload.ts b/packages/http/src/payload.ts index 441195fe81..43dabd4465 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,15 @@ 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; + : usedIn === "request" && 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( From e203100f651876b57f2b0f7fe8b47a3b5489690e Mon Sep 17 00:00:00 2001 From: subaru Date: Thu, 17 Oct 2024 10:56:36 +0900 Subject: [PATCH 09/11] add inheritDoc Co-authored-by: Timothee Guerin --- packages/http/src/decorators.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/http/src/decorators.ts b/packages/http/src/decorators.ts index 76813941a6..256bab2fea 100644 --- a/packages/http/src/decorators.ts +++ b/packages/http/src/decorators.ts @@ -125,6 +125,7 @@ export function isHeader(program: Program, entity: Type) { return program.stateMap(HttpStateKeys.header).has(entity); } +/** {@inheritDoc CookieDecorator } */ export const $cookie: CookieDecorator = ( context: DecoratorContext, entity: ModelProperty, From d79d56ee92d58b22ffb8c68baf615664de643174 Mon Sep 17 00:00:00 2001 From: subaru Date: Thu, 17 Oct 2024 11:06:42 +0900 Subject: [PATCH 10/11] remove explode and style options --- packages/http/generated-defs/TypeSpec.Http.ts | 1 - packages/http/lib/decorators.tsp | 11 --------- packages/http/src/decorators.ts | 3 --- packages/http/src/types.ts | 7 +----- packages/http/test/http-decorators.test.ts | 24 ------------------- 5 files changed, 1 insertion(+), 45 deletions(-) diff --git a/packages/http/generated-defs/TypeSpec.Http.ts b/packages/http/generated-defs/TypeSpec.Http.ts index f674c0e3cc..fa6380976c 100644 --- a/packages/http/generated-defs/TypeSpec.Http.ts +++ b/packages/http/generated-defs/TypeSpec.Http.ts @@ -9,7 +9,6 @@ import type { export interface CookieOptions { readonly name?: string; - readonly explode?: boolean; } export interface QueryOptions { diff --git a/packages/http/lib/decorators.tsp b/packages/http/lib/decorators.tsp index 2c959c03b5..b34aa1fd9e 100644 --- a/packages/http/lib/decorators.tsp +++ b/packages/http/lib/decorators.tsp @@ -47,17 +47,6 @@ model CookieOptions { * Name in the cookie. */ name?: string; - - /** - * If false the value will be joined with `,`. - * - * | Style | Explode | Uri Template | Primitive value id = 5 | Array id = [3, 4, 5] | Object id = {"role": "admin", "firstName": "Alex"} | - * | ----- | ------- | -------------| ---------------------- | -------------------- | -------------------------------------------------- | - * | form | true | ` ` | `Cookie: id=5` | | | - * | form | false | `id={id}` | `Cookie: id=5` | `Cookie: id=3,4,5 `| `Cookie: id=role,admin,firstName,Alex` | - * - */ - explode?: boolean; } /** diff --git a/packages/http/src/decorators.ts b/packages/http/src/decorators.ts index 256bab2fea..da0cac77a5 100644 --- a/packages/http/src/decorators.ts +++ b/packages/http/src/decorators.ts @@ -136,12 +136,9 @@ export const $cookie: CookieDecorator = ( ? cookieNameOrOptions : (cookieNameOrOptions?.name ?? entity.name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase()); - const userOptions = typeof cookieNameOrOptions === "object" ? cookieNameOrOptions : {}; const options: CookieParameterOptions = { type: "cookie", name: paramName, - explode: userOptions.explode ?? true, - format: "form", }; context.program.stateMap(HttpStateKeys.cookie).set(entity, options); }; diff --git a/packages/http/src/types.ts b/packages/http/src/types.ts index cada59fff1..f2076ab15a 100644 --- a/packages/http/src/types.ts +++ b/packages/http/src/types.ts @@ -299,14 +299,9 @@ export interface HeaderFieldOptions { format?: "csv" | "multi" | "ssv" | "tsv" | "pipes" | "simple" | "form"; } -export interface CookieParameterOptions extends Required> { +export interface CookieParameterOptions extends Required { type: "cookie"; name: string; - /** - * The string format of the array. "csv" and "simple" are used interchangeably, as are - * "multi" and "form". - */ - format?: "csv" | "multi" | "ssv" | "tsv" | "pipes" | "simple" | "form"; } export interface QueryParameterOptions extends Required> { diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index b769739c56..51e5c93330 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -201,30 +201,6 @@ describe("http: decorators", () => { strictEqual(getCookieParamName(runner.program, myCookie), "my-cookie"); }); - - it("set default explode: true", async () => { - const { myCookie } = await runner.compile(` - op test(@test @cookie myCookie: string): string; - `); - expect(getCookieParamOptions(runner.program, myCookie)).toEqual({ - type: "cookie", - name: "my_cookie", - explode: true, - format: "form", - }); - }); - - it("specify explode: false", async () => { - const { myCookie } = await runner.compile(` - op test(@test @cookie(#{explode: false}) myCookie: string): string; - `); - expect(getCookieParamOptions(runner.program, myCookie)).toEqual({ - type: "cookie", - name: "my_cookie", - explode: false, - format: "form", - }); - }); }); describe("@query", () => { From 5864d85a532a562cda81712c9665103c41f4925a Mon Sep 17 00:00:00 2001 From: subaru Date: Thu, 17 Oct 2024 11:09:01 +0900 Subject: [PATCH 11/11] omit getCookieParamName func --- packages/http/src/decorators.ts | 4 ---- packages/http/test/http-decorators.test.ts | 7 +++---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/http/src/decorators.ts b/packages/http/src/decorators.ts index da0cac77a5..aedee65864 100644 --- a/packages/http/src/decorators.ts +++ b/packages/http/src/decorators.ts @@ -147,10 +147,6 @@ export function getCookieParamOptions(program: Program, entity: Type): QueryPara return program.stateMap(HttpStateKeys.cookie).get(entity); } -export function getCookieParamName(program: Program, entity: Type): string { - return getCookieParamOptions(program, entity)?.name; -} - export function isCookieParam(program: Program, entity: Type) { return program.stateMap(HttpStateKeys.cookie).has(entity); } diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index 51e5c93330..69b6447383 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -8,7 +8,6 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, expect, it } from "vitest"; import { getAuthentication, - getCookieParamName, getCookieParamOptions, getHeaderFieldName, getHeaderFieldOptions, @@ -183,7 +182,7 @@ describe("http: decorators", () => { `); ok(isCookieParam(runner.program, myCookie)); - strictEqual(getCookieParamName(runner.program, myCookie), "my_cookie"); + strictEqual(getCookieParamOptions(runner.program, myCookie).name, "my_cookie"); }); it("override cookie name with 1st parameter", async () => { @@ -191,7 +190,7 @@ describe("http: decorators", () => { op test(@test @cookie("my-cookie") myCookie: string): string; `); - strictEqual(getCookieParamName(runner.program, myCookie), "my-cookie"); + strictEqual(getCookieParamOptions(runner.program, myCookie).name, "my-cookie"); }); it("override cookie with CookieOptions", async () => { @@ -199,7 +198,7 @@ describe("http: decorators", () => { op test(@test @cookie(#{name: "my-cookie"}) myCookie: string): string; `); - strictEqual(getCookieParamName(runner.program, myCookie), "my-cookie"); + strictEqual(getCookieParamOptions(runner.program, myCookie).name, "my-cookie"); }); });