Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[http] Add Parameter Decorator @cookie to Specify Cookie Parameters #4761

Merged
merged 28 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
214a1e8
define cookie decorator
su8ru Oct 16, 2024
3a01550
implement cookie decorator
su8ru Oct 16, 2024
5acaf1c
add tests for cookie
su8ru Oct 16, 2024
5aa92cc
update example
su8ru Oct 16, 2024
6bad2fb
regen-docs
su8ru Oct 16, 2024
3dc5f98
regen defs
su8ru Oct 16, 2024
1682f66
add cookie impl as parameters
su8ru Oct 16, 2024
b9656c3
detect cookie parameters as metadata
su8ru Oct 16, 2024
e203100
add inheritDoc
su8ru Oct 17, 2024
d79d56e
remove explode and style options
su8ru Oct 17, 2024
5864d85
omit getCookieParamName func
su8ru Oct 17, 2024
867a2b2
correct return type and add js doc to getCookieParamOptions
su8ru Oct 17, 2024
86b086d
[http-server-javascript] throw UnimplementedError when cookie paramet…
su8ru Oct 17, 2024
dc238fa
[openapi3] add cookie case to getParameterAttributes
su8ru Oct 17, 2024
d703e46
regen docs
su8ru Oct 17, 2024
9a238bc
[openapi3] add tests for cookie params
su8ru Oct 17, 2024
47e30a3
add doc
su8ru Oct 18, 2024
48dde7a
omit response cookie and diagnose that as warning
su8ru Oct 21, 2024
3ee7a3c
[openapi3] remove response cookies in test
su8ru Oct 21, 2024
fc02809
[openapi3] specify explode: false as default
su8ru Oct 21, 2024
a8628b2
remove response-cookie-not-supported from explicit cookie
su8ru Oct 21, 2024
c28f21f
add changelog
su8ru Oct 21, 2024
d047554
Merge branch 'main' into cookie-decorator
su8ru Oct 21, 2024
7346ffb
Merge branch 'main' into cookie-decorator
su8ru Oct 22, 2024
66c3c41
Merge branch 'main' into cookie-decorator
timotheeguerin Oct 31, 2024
632e618
Merge remote-tracking branch 'upstream/main' into cookie-decorator
su8ru Nov 1, 2024
e85c96b
regen docs
su8ru Nov 1, 2024
751de85
Merge branch 'main' into cookie-decorator
timotheeguerin Nov 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .chronus/changes/cookie-decorator-2024-9-22-1-33-4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
changeKind: feature
packages:
- "@typespec/http-server-javascript"
- "@typespec/http"
- "@typespec/openapi3"
---

Add `@cookie` decorator to specify cookie parameters
14 changes: 14 additions & 0 deletions docs/libraries/http/reference/data-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,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.
Expand Down
41 changes: 41 additions & 0 deletions docs/libraries/http/reference/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br />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`.
Expand Down
2 changes: 2 additions & 0 deletions docs/libraries/http/reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions packages/http-server-javascript/src/http/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
42 changes: 42 additions & 0 deletions packages/http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Available ruleSets:
- [`@body`](#@body)
- [`@bodyIgnore`](#@bodyignore)
- [`@bodyRoot`](#@bodyroot)
- [`@cookie`](#@cookie)
- [`@delete`](#@delete)
- [`@get`](#@get)
- [`@head`](#@head)
Expand Down Expand Up @@ -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.<br />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`.
Expand Down
28 changes: 28 additions & 0 deletions packages/http/generated-defs/TypeSpec.Http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -321,6 +348,7 @@ export type TypeSpecHttpDecorators = {
statusCode: StatusCodeDecorator;
body: BodyDecorator;
header: HeaderDecorator;
cookie: CookieDecorator;
query: QueryDecorator;
path: PathDecorator;
bodyRoot: BodyRootDecorator;
Expand Down
32 changes: 32 additions & 0 deletions packages/http/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
44 changes: 44 additions & 0 deletions packages/http/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
BodyDecorator,
BodyIgnoreDecorator,
BodyRootDecorator,
CookieDecorator,
CookieOptions,
DeleteDecorator,
GetDecorator,
HeadDecorator,
Expand All @@ -49,6 +51,7 @@ import { getStatusCodesFromType } from "./status-codes.js";
import {
Authentication,
AuthenticationOption,
CookieParameterOptions,
HeaderFieldOptions,
HttpAuth,
HttpStatusCodeRange,
Expand Down Expand Up @@ -122,6 +125,47 @@ export function isHeader(program: Program, entity: Type) {
return program.stateMap(HttpStateKeys.header).has(entity);
}

/** {@inheritDoc CookieDecorator } */
export const $cookie: CookieDecorator = (
su8ru marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand Down
30 changes: 29 additions & 1 deletion packages/http/src/http-property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type Program,
} from "@typespec/compiler";
import {
getCookieParamOptions,
getHeaderFieldOptions,
getPathParamOptions,
getQueryParamOptions,
Expand All @@ -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
Expand All @@ -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";
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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" ||
Expand Down
Loading
Loading