Add support for URI templates (#3932)
fix https://github.com/microsoft/typespec/issues/3736 --------- Co-authored-by: Christopher Radek <14189820+chrisradek@users.noreply.github.com>
This commit is contained in:
Родитель
8a3e94123c
Коммит
e492ff7d7b
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
|
||||
changeKind: feature
|
||||
packages:
|
||||
- "@typespec/http"
|
||||
---
|
||||
|
||||
`@route` can now take a uri template as defined by [RFC-6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)
|
||||
|
||||
```tsp
|
||||
@route("files{+path}") download(path: string): void;
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
|
||||
changeKind: feature
|
||||
packages:
|
||||
- "@typespec/openapi3"
|
||||
- "@typespec/rest"
|
||||
---
|
||||
|
||||
Add support for URI templates in routes
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
changeKind: deprecation
|
||||
packages:
|
||||
- "@typespec/http"
|
||||
---
|
||||
|
||||
API deprecation: `HttpOperation#pathSegments` is deprecated. Use `HttpOperation#uriTemplate` instead.
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
changeKind: deprecation
|
||||
packages:
|
||||
- "@typespec/http"
|
||||
---
|
||||
|
||||
Deprecated `@query({format: })` option. Use `@query(#{explode: true})` instead of `form` or `multi` format. Previously `csv`/`simple` is the default now.
|
||||
Decorator is also expecting an object value now instead of a model. A deprecation warning with a codefix will help migrating.
|
||||
|
||||
```diff
|
||||
- @query({format: "form"}) select: string[];
|
||||
+ @query(#{explode: true}) select: string[];
|
||||
```
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
|
||||
changeKind: feature
|
||||
packages:
|
||||
- "@typespec/compiler"
|
||||
---
|
||||
|
||||
Add `ArrayEncoding` enum to define simple serialization of arrays
|
|
@ -466,6 +466,21 @@ model TypeSpec.Http.PasswordFlow
|
|||
| refreshUrl? | `string` | the refresh URL |
|
||||
| scopes? | `string[]` | list of scopes for the credential |
|
||||
|
||||
### `PathOptions` {#TypeSpec.Http.PathOptions}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.PathOptions
|
||||
```
|
||||
|
||||
#### Properties
|
||||
|
||||
| Name | Type | Description |
|
||||
| -------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| name? | `string` | Name of the parameter in the uri template. |
|
||||
| explode? | `boolean` | When interpolating this parameter in the case of array or object expand each value using the given style.<br />Equivalent of adding `*` in the path parameter as per [RFC-6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3) |
|
||||
| style? | `"simple" \| "label" \| "matrix" \| "fragment" \| "path"` | Different interpolating styles for the path parameter.<br />- `simple`: No special encoding.<br />- `label`: Using `.` separator.<br />- `matrix`: `;` as separator.<br />- `fragment`: `#` as separator.<br />- `path`: `/` as separator. |
|
||||
| allowReserved? | `boolean` | When interpolating this parameter do not encode reserved characters.<br />Equivalent of adding `+` in the path parameter as per [RFC-6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3) |
|
||||
|
||||
### `PlainData` {#TypeSpec.Http.PlainData}
|
||||
|
||||
Produces a new model with the same properties as T, but with `@query`,
|
||||
|
@ -495,10 +510,11 @@ model TypeSpec.Http.QueryOptions
|
|||
|
||||
#### Properties
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | --------------------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| name? | `string` | Name of the query when included in the url. |
|
||||
| format? | `"multi" \| "csv" \| "ssv" \| "tsv" \| "simple" \| "form" \| "pipes"` | Determines the format of the array if type array is used. |
|
||||
| Name | Type | Description |
|
||||
| -------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| name? | `string` | Name of the query when included in the url. |
|
||||
| explode? | `boolean` | If true send each value in the array/object as a separate query parameter.<br />Equivalent of adding `*` in the path parameter as per [RFC-6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)<br /><br />\| Style \| Explode \| Uri Template \| Primitive value id = 5 \| Array id = [3, 4, 5] \| Object id = {"role": "admin", "firstName": "Alex"} \|<br />\| ------ \| ------- \| -------------- \| ---------------------- \| ----------------------- \| -------------------------------------------------- \|<br />\| simple \| false \| `/users{?id}` \| `/users?id=5` \| `/users?id=3,4,5` \| `/users?id=role,admin,firstName,Alex` \|<br />\| simple \| true \| `/users{?id*}` \| `/users?id=5` \| `/users?id=3&id=4&id=5` \| `/users?role=admin&firstName=Alex` \| |
|
||||
| format? | `"multi" \| "csv" \| "ssv" \| "tsv" \| "simple" \| "form" \| "pipes"` | Determines the format of the array if type array is used.<br />**DEPRECATED**: use explode: true instead of `multi` or `@encode` |
|
||||
|
||||
### `Response` {#TypeSpec.Http.Response}
|
||||
|
||||
|
|
|
@ -278,7 +278,7 @@ None
|
|||
Explicitly specify that this property is to be interpolated as a path parameter.
|
||||
|
||||
```typespec
|
||||
@TypeSpec.Http.path(paramName?: valueof string)
|
||||
@TypeSpec.Http.path(paramNameOrOptions?: valueof string | TypeSpec.Http.PathOptions)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
@ -287,9 +287,9 @@ Explicitly specify that this property is to be interpolated as a path parameter.
|
|||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| --------- | ---------------- | --------------------------------------------------- |
|
||||
| paramName | `valueof string` | Optional name of the parameter in the url template. |
|
||||
| Name | Type | Description |
|
||||
| ------------------ | --------------------------------------------- | -------------------------------------------------------------- |
|
||||
| paramNameOrOptions | `valueof string \| TypeSpec.Http.PathOptions` | Optional name of the parameter in the uri template or options. |
|
||||
|
||||
#### Examples
|
||||
|
||||
|
@ -347,7 +347,7 @@ None
|
|||
Specify this property is to be sent as a query parameter.
|
||||
|
||||
```typespec
|
||||
@TypeSpec.Http.query(queryNameOrOptions?: string | TypeSpec.Http.QueryOptions)
|
||||
@TypeSpec.Http.query(queryNameOrOptions?: valueof string | TypeSpec.Http.QueryOptions)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
@ -356,30 +356,20 @@ Specify this property is to be sent as a query parameter.
|
|||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------------ | -------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| queryNameOrOptions | `string \| TypeSpec.Http.QueryOptions` | Optional name of the query when included in the url or query parameter options. |
|
||||
| Name | Type | Description |
|
||||
| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| queryNameOrOptions | `valueof string \| TypeSpec.Http.QueryOptions` | Optional name of the query when included in the url or query parameter options. |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
op read(@query select: string, @query("order-by") orderBy: string): void;
|
||||
op list(
|
||||
@query({
|
||||
name: "id",
|
||||
format: "multi",
|
||||
})
|
||||
ids: string[],
|
||||
): void;
|
||||
op list(@query(#{ name: "id", explode: true }) ids: string[]): void;
|
||||
```
|
||||
|
||||
### `@route` {#@TypeSpec.Http.route}
|
||||
|
||||
Defines the relative route URI for the target operation
|
||||
|
||||
The first argument should be a URI fragment that may contain one or more path parameter fields.
|
||||
If the namespace or interface that contains the operation is also marked with a `@route` decorator,
|
||||
it will be used as a prefix to the route URI of the operation.
|
||||
Defines the relative route URI template for the target operation as defined by [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)
|
||||
|
||||
`@route` can only be applied to operations, namespaces, and interfaces.
|
||||
|
||||
|
@ -393,16 +383,30 @@ it will be used as a prefix to the route URI of the operation.
|
|||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| path | `valueof string` | Relative route path. Cannot include query parameters. |
|
||||
| options | `{...}` | Set of parameters used to configure the route. Supports `{shared: true}` which indicates that the route may be shared by several operations. |
|
||||
| Name | Type | Description |
|
||||
| ------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| path | `valueof string` | |
|
||||
| options | `{...}` | _DEPRECATED_ Set of parameters used to configure the route. Supports `{shared: true}` which indicates that the route may be shared by several operations. |
|
||||
|
||||
#### Examples
|
||||
|
||||
##### Simple path parameter
|
||||
|
||||
```typespec
|
||||
@route("/widgets")
|
||||
op getWidget(@path id: string): Widget;
|
||||
@route("/widgets/{id}") op getWidget(@path id: string): Widget;
|
||||
```
|
||||
|
||||
##### Reserved characters
|
||||
|
||||
```typespec
|
||||
@route("/files{+path}") op getFile(@path path: string): bytes;
|
||||
```
|
||||
|
||||
##### Query parameter
|
||||
|
||||
```typespec
|
||||
@route("/files") op list(select?: string, filter?: string): Files[];
|
||||
@route("/files{?select,filter}") op listFullUriTemplate(select?: string, filter?: string): Files[];
|
||||
```
|
||||
|
||||
### `@server` {#@TypeSpec.Http.server}
|
||||
|
|
|
@ -83,6 +83,7 @@ npm install --save-peer @typespec/http
|
|||
- [`OkResponse`](./data-types.md#TypeSpec.Http.OkResponse)
|
||||
- [`OpenIdConnectAuth`](./data-types.md#TypeSpec.Http.OpenIdConnectAuth)
|
||||
- [`PasswordFlow`](./data-types.md#TypeSpec.Http.PasswordFlow)
|
||||
- [`PathOptions`](./data-types.md#TypeSpec.Http.PathOptions)
|
||||
- [`PlainData`](./data-types.md#TypeSpec.Http.PlainData)
|
||||
- [`QueryOptions`](./data-types.md#TypeSpec.Http.QueryOptions)
|
||||
- [`Response`](./data-types.md#TypeSpec.Http.Response)
|
||||
|
|
|
@ -194,6 +194,19 @@ model UpdateableProperties<Source>
|
|||
#### Properties
|
||||
None
|
||||
|
||||
### `ArrayEncoding` {#ArrayEncoding}
|
||||
|
||||
Encoding for serializing arrays
|
||||
```typespec
|
||||
enum ArrayEncoding
|
||||
```
|
||||
|
||||
| Name | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| pipeDelimited | | Each values of the array is separated by a \| |
|
||||
| spaceDelimited | | Each values of the array is separated by a <space> |
|
||||
|
||||
|
||||
### `BytesKnownEncoding` {#BytesKnownEncoding}
|
||||
|
||||
Known encoding to use on bytes
|
||||
|
|
|
@ -475,6 +475,17 @@ enum BytesKnownEncoding {
|
|||
base64url: "base64url",
|
||||
}
|
||||
|
||||
/**
|
||||
* Encoding for serializing arrays
|
||||
*/
|
||||
enum ArrayEncoding {
|
||||
/** Each values of the array is separated by a | */
|
||||
pipeDelimited,
|
||||
|
||||
/** Each values of the array is separated by a <space> */
|
||||
spaceDelimited,
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify how to encode the target type.
|
||||
* @param encodingOrEncodeAs Known name of an encoding or a scalar type to encode as(Only for numeric types to encode as string).
|
||||
|
|
|
@ -326,7 +326,7 @@ None
|
|||
Explicitly specify that this property is to be interpolated as a path parameter.
|
||||
|
||||
```typespec
|
||||
@TypeSpec.Http.path(paramName?: valueof string)
|
||||
@TypeSpec.Http.path(paramNameOrOptions?: valueof string | TypeSpec.Http.PathOptions)
|
||||
```
|
||||
|
||||
##### Target
|
||||
|
@ -335,9 +335,9 @@ Explicitly specify that this property is to be interpolated as a path parameter.
|
|||
|
||||
##### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| --------- | ---------------- | --------------------------------------------------- |
|
||||
| paramName | `valueof string` | Optional name of the parameter in the url template. |
|
||||
| Name | Type | Description |
|
||||
| ------------------ | --------------------------------------------- | -------------------------------------------------------------- |
|
||||
| paramNameOrOptions | `valueof string \| TypeSpec.Http.PathOptions` | Optional name of the parameter in the uri template or options. |
|
||||
|
||||
##### Examples
|
||||
|
||||
|
@ -395,7 +395,7 @@ None
|
|||
Specify this property is to be sent as a query parameter.
|
||||
|
||||
```typespec
|
||||
@TypeSpec.Http.query(queryNameOrOptions?: string | TypeSpec.Http.QueryOptions)
|
||||
@TypeSpec.Http.query(queryNameOrOptions?: valueof string | TypeSpec.Http.QueryOptions)
|
||||
```
|
||||
|
||||
##### Target
|
||||
|
@ -404,30 +404,20 @@ Specify this property is to be sent as a query parameter.
|
|||
|
||||
##### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------------ | -------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| queryNameOrOptions | `string \| TypeSpec.Http.QueryOptions` | Optional name of the query when included in the url or query parameter options. |
|
||||
| Name | Type | Description |
|
||||
| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| queryNameOrOptions | `valueof string \| TypeSpec.Http.QueryOptions` | Optional name of the query when included in the url or query parameter options. |
|
||||
|
||||
##### Examples
|
||||
|
||||
```typespec
|
||||
op read(@query select: string, @query("order-by") orderBy: string): void;
|
||||
op list(
|
||||
@query({
|
||||
name: "id",
|
||||
format: "multi",
|
||||
})
|
||||
ids: string[],
|
||||
): void;
|
||||
op list(@query(#{ name: "id", explode: true }) ids: string[]): void;
|
||||
```
|
||||
|
||||
#### `@route`
|
||||
|
||||
Defines the relative route URI for the target operation
|
||||
|
||||
The first argument should be a URI fragment that may contain one or more path parameter fields.
|
||||
If the namespace or interface that contains the operation is also marked with a `@route` decorator,
|
||||
it will be used as a prefix to the route URI of the operation.
|
||||
Defines the relative route URI template for the target operation as defined by [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)
|
||||
|
||||
`@route` can only be applied to operations, namespaces, and interfaces.
|
||||
|
||||
|
@ -441,16 +431,30 @@ it will be used as a prefix to the route URI of the operation.
|
|||
|
||||
##### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| path | `valueof string` | Relative route path. Cannot include query parameters. |
|
||||
| options | `{...}` | Set of parameters used to configure the route. Supports `{shared: true}` which indicates that the route may be shared by several operations. |
|
||||
| Name | Type | Description |
|
||||
| ------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| path | `valueof string` | |
|
||||
| options | `{...}` | _DEPRECATED_ Set of parameters used to configure the route. Supports `{shared: true}` which indicates that the route may be shared by several operations. |
|
||||
|
||||
##### Examples
|
||||
|
||||
###### Simple path parameter
|
||||
|
||||
```typespec
|
||||
@route("/widgets")
|
||||
op getWidget(@path id: string): Widget;
|
||||
@route("/widgets/{id}") op getWidget(@path id: string): Widget;
|
||||
```
|
||||
|
||||
###### Reserved characters
|
||||
|
||||
```typespec
|
||||
@route("/files{+path}") op getFile(@path path: string): bytes;
|
||||
```
|
||||
|
||||
###### Query parameter
|
||||
|
||||
```typespec
|
||||
@route("/files") op list(select?: string, filter?: string): Files[];
|
||||
@route("/files{?select,filter}") op listFullUriTemplate(select?: string, filter?: string): Files[];
|
||||
```
|
||||
|
||||
#### `@server`
|
||||
|
|
|
@ -7,6 +7,19 @@ import type {
|
|||
Type,
|
||||
} from "@typespec/compiler";
|
||||
|
||||
export interface QueryOptions {
|
||||
readonly name?: string;
|
||||
readonly explode?: boolean;
|
||||
readonly format?: "multi" | "csv" | "ssv" | "tsv" | "simple" | "form" | "pipes";
|
||||
}
|
||||
|
||||
export interface PathOptions {
|
||||
readonly name?: string;
|
||||
readonly explode?: boolean;
|
||||
readonly style?: "simple" | "label" | "matrix" | "fragment" | "path";
|
||||
readonly allowReserved?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the status code for this response. Property type must be a status code integer or a union of status code integer.
|
||||
*
|
||||
|
@ -67,19 +80,19 @@ export type HeaderDecorator = (
|
|||
* @example
|
||||
* ```typespec
|
||||
* op read(@query select: string, @query("order-by") orderBy: string): void;
|
||||
* op list(@query({name: "id", format: "multi"}) ids: string[]): void;
|
||||
* op list(@query(#{name: "id", explode: true}) ids: string[]): void;
|
||||
* ```
|
||||
*/
|
||||
export type QueryDecorator = (
|
||||
context: DecoratorContext,
|
||||
target: ModelProperty,
|
||||
queryNameOrOptions?: Type
|
||||
queryNameOrOptions?: string | QueryOptions
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Explicitly specify that this property is to be interpolated as a path parameter.
|
||||
*
|
||||
* @param paramName Optional name of the parameter in the url template.
|
||||
* @param paramNameOrOptions Optional name of the parameter in the uri template or options.
|
||||
* @example
|
||||
* ```typespec
|
||||
* @route("/read/{explicit}/things/{implicit}")
|
||||
|
@ -89,7 +102,7 @@ export type QueryDecorator = (
|
|||
export type PathDecorator = (
|
||||
context: DecoratorContext,
|
||||
target: ModelProperty,
|
||||
paramName?: string
|
||||
paramNameOrOptions?: string | PathOptions
|
||||
) => void;
|
||||
|
||||
/**
|
||||
|
@ -260,20 +273,25 @@ export type IncludeInapplicableMetadataInPayloadDecorator = (
|
|||
) => void;
|
||||
|
||||
/**
|
||||
* Defines the relative route URI for the target operation
|
||||
*
|
||||
* The first argument should be a URI fragment that may contain one or more path parameter fields.
|
||||
* If the namespace or interface that contains the operation is also marked with a `@route` decorator,
|
||||
* it will be used as a prefix to the route URI of the operation.
|
||||
* Defines the relative route URI template for the target operation as defined by [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)
|
||||
*
|
||||
* `@route` can only be applied to operations, namespaces, and interfaces.
|
||||
*
|
||||
* @param path Relative route path. Cannot include query parameters.
|
||||
* @param options Set of parameters used to configure the route. Supports `{shared: true}` which indicates that the route may be shared by several operations.
|
||||
* @example
|
||||
* @param uriTemplate Uri template for this operation.
|
||||
* @param options _DEPRECATED_ Set of parameters used to configure the route. Supports `{shared: true}` which indicates that the route may be shared by several operations.
|
||||
* @example Simple path parameter
|
||||
*
|
||||
* ```typespec
|
||||
* @route("/widgets")
|
||||
* op getWidget(@path id: string): Widget;
|
||||
* @route("/widgets/{id}") op getWidget(@path id: string): Widget;
|
||||
* ```
|
||||
* @example Reserved characters
|
||||
* ```typespec
|
||||
* @route("/files{+path}") op getFile(@path path: string): bytes;
|
||||
* ```
|
||||
* @example Query parameter
|
||||
* ```typespec
|
||||
* @route("/files") op list(select?: string, filter?: string): Files[];
|
||||
* @route("/files{?select,filter}") op listFullUriTemplate(select?: string, filter?: string): Files[];
|
||||
* ```
|
||||
*/
|
||||
export type RouteDecorator = (
|
||||
|
|
|
@ -48,8 +48,21 @@ model QueryOptions {
|
|||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* If true send each value in the array/object as a separate query parameter.
|
||||
* Equivalent of adding `*` in the path parameter as per [RFC-6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)
|
||||
*
|
||||
* | Style | Explode | Uri Template | Primitive value id = 5 | Array id = [3, 4, 5] | Object id = {"role": "admin", "firstName": "Alex"} |
|
||||
* | ------ | ------- | -------------- | ---------------------- | ----------------------- | -------------------------------------------------- |
|
||||
* | simple | false | `/users{?id}` | `/users?id=5` | `/users?id=3,4,5` | `/users?id=role,admin,firstName,Alex` |
|
||||
* | simple | true | `/users{?id*}` | `/users?id=5` | `/users?id=3&id=4&id=5` | `/users?role=admin&firstName=Alex` |
|
||||
*
|
||||
*/
|
||||
explode?: boolean;
|
||||
|
||||
/**
|
||||
* Determines the format of the array if type array is used.
|
||||
* **DEPRECATED**: use explode: true instead of `multi` or `@encode`
|
||||
*/
|
||||
format?: "multi" | "csv" | "ssv" | "tsv" | "simple" | "form" | "pipes";
|
||||
}
|
||||
|
@ -63,15 +76,42 @@ model QueryOptions {
|
|||
*
|
||||
* ```typespec
|
||||
* op read(@query select: string, @query("order-by") orderBy: string): void;
|
||||
* op list(@query({name: "id", format: "multi"}) ids: string[]): void;
|
||||
* op list(@query(#{name: "id", explode: true}) ids: string[]): void;
|
||||
* ```
|
||||
*/
|
||||
extern dec query(target: ModelProperty, queryNameOrOptions?: string | QueryOptions);
|
||||
extern dec query(target: ModelProperty, queryNameOrOptions?: valueof string | QueryOptions);
|
||||
|
||||
model PathOptions {
|
||||
/** Name of the parameter in the uri template. */
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* When interpolating this parameter in the case of array or object expand each value using the given style.
|
||||
* Equivalent of adding `*` in the path parameter as per [RFC-6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)
|
||||
*/
|
||||
explode?: boolean;
|
||||
|
||||
/**
|
||||
* Different interpolating styles for the path parameter.
|
||||
* - `simple`: No special encoding.
|
||||
* - `label`: Using `.` separator.
|
||||
* - `matrix`: `;` as separator.
|
||||
* - `fragment`: `#` as separator.
|
||||
* - `path`: `/` as separator.
|
||||
*/
|
||||
style?: "simple" | "label" | "matrix" | "fragment" | "path";
|
||||
|
||||
/**
|
||||
* When interpolating this parameter do not encode reserved characters.
|
||||
* Equivalent of adding `+` in the path parameter as per [RFC-6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)
|
||||
*/
|
||||
allowReserved?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly specify that this property is to be interpolated as a path parameter.
|
||||
*
|
||||
* @param paramName Optional name of the parameter in the url template.
|
||||
* @param paramNameOrOptions Optional name of the parameter in the uri template or options.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
|
@ -80,7 +120,7 @@ extern dec query(target: ModelProperty, queryNameOrOptions?: string | QueryOptio
|
|||
* op read(@path explicit: string, implicit: string): void;
|
||||
* ```
|
||||
*/
|
||||
extern dec path(target: ModelProperty, paramName?: valueof string);
|
||||
extern dec path(target: ModelProperty, paramNameOrOptions?: valueof string | PathOptions);
|
||||
|
||||
/**
|
||||
* Explicitly specify that this property type will be exactly the HTTP body.
|
||||
|
@ -282,22 +322,28 @@ extern dec useAuth(target: Namespace | Interface | Operation, auth: {} | Union |
|
|||
extern dec includeInapplicableMetadataInPayload(target: unknown, value: valueof boolean);
|
||||
|
||||
/**
|
||||
* Defines the relative route URI for the target operation
|
||||
*
|
||||
* The first argument should be a URI fragment that may contain one or more path parameter fields.
|
||||
* If the namespace or interface that contains the operation is also marked with a `@route` decorator,
|
||||
* it will be used as a prefix to the route URI of the operation.
|
||||
* Defines the relative route URI template for the target operation as defined by [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)
|
||||
*
|
||||
* `@route` can only be applied to operations, namespaces, and interfaces.
|
||||
*
|
||||
* @param path Relative route path. Cannot include query parameters.
|
||||
* @param options Set of parameters used to configure the route. Supports `{shared: true}` which indicates that the route may be shared by several operations.
|
||||
* @param uriTemplate Uri template for this operation.
|
||||
* @param options _DEPRECATED_ Set of parameters used to configure the route. Supports `{shared: true}` which indicates that the route may be shared by several operations.
|
||||
*
|
||||
* @example
|
||||
* @example Simple path parameter
|
||||
*
|
||||
* ```typespec
|
||||
* @route("/widgets")
|
||||
* op getWidget(@path id: string): Widget;
|
||||
* @route("/widgets/{id}") op getWidget(@path id: string): Widget;
|
||||
* ```
|
||||
*
|
||||
* @example Reserved characters
|
||||
* ```typespec
|
||||
* @route("/files{+path}") op getFile(@path path: string): bytes;
|
||||
* ```
|
||||
*
|
||||
* @example Query parameter
|
||||
* ```typespec
|
||||
* @route("/files") op list(select?: string, filter?: string): Files[];
|
||||
* @route("/files{?select,filter}") op listFullUriTemplate(select?: string, filter?: string): Files[];
|
||||
* ```
|
||||
*/
|
||||
extern dec route(
|
||||
|
|
|
@ -33,9 +33,11 @@ import {
|
|||
MultipartBodyDecorator,
|
||||
PatchDecorator,
|
||||
PathDecorator,
|
||||
PathOptions,
|
||||
PostDecorator,
|
||||
PutDecorator,
|
||||
QueryDecorator,
|
||||
QueryOptions,
|
||||
RouteDecorator,
|
||||
ServerDecorator,
|
||||
SharedRouteDecorator,
|
||||
|
@ -123,38 +125,28 @@ export function isHeader(program: Program, entity: Type) {
|
|||
export const $query: QueryDecorator = (
|
||||
context: DecoratorContext,
|
||||
entity: ModelProperty,
|
||||
queryNameOrOptions?: StringLiteral | Type
|
||||
queryNameOrOptions?: string | QueryOptions
|
||||
) => {
|
||||
const paramName =
|
||||
typeof queryNameOrOptions === "string"
|
||||
? queryNameOrOptions
|
||||
: (queryNameOrOptions?.name ?? entity.name);
|
||||
const userOptions: QueryOptions =
|
||||
typeof queryNameOrOptions === "object" ? queryNameOrOptions : {};
|
||||
if (userOptions.format) {
|
||||
reportDeprecated(
|
||||
context.program,
|
||||
"The `format` option of `@query` decorator is deprecated. Use `explode: true` instead of `form` and `multi`. `csv` or `simple` is the default now.",
|
||||
entity
|
||||
);
|
||||
}
|
||||
const options: QueryParameterOptions = {
|
||||
type: "query",
|
||||
name: entity.name,
|
||||
explode:
|
||||
userOptions.explode ?? (userOptions.format === "multi" || userOptions.format === "form"),
|
||||
format: userOptions.format ?? (userOptions.explode ? "multi" : "csv"),
|
||||
name: paramName,
|
||||
};
|
||||
if (queryNameOrOptions) {
|
||||
if (queryNameOrOptions.kind === "String") {
|
||||
options.name = queryNameOrOptions.value;
|
||||
} else if (queryNameOrOptions.kind === "Model") {
|
||||
const name = queryNameOrOptions.properties.get("name")?.type;
|
||||
if (name?.kind === "String") {
|
||||
options.name = name.value;
|
||||
}
|
||||
const format = queryNameOrOptions.properties.get("format")?.type;
|
||||
if (format?.kind === "String") {
|
||||
options.format = format.value as any; // That value should have already been validated by the TypeSpec dec
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (
|
||||
entity.type.kind === "Model" &&
|
||||
isArrayModelType(context.program, entity.type) &&
|
||||
options.format === undefined
|
||||
) {
|
||||
reportDiagnostic(context.program, {
|
||||
code: "query-format-required",
|
||||
target: context.decoratorTarget,
|
||||
});
|
||||
}
|
||||
context.program.stateMap(HttpStateKeys.query).set(entity, options);
|
||||
};
|
||||
|
||||
|
@ -173,11 +165,20 @@ export function isQueryParam(program: Program, entity: Type) {
|
|||
export const $path: PathDecorator = (
|
||||
context: DecoratorContext,
|
||||
entity: ModelProperty,
|
||||
paramName?: string
|
||||
paramNameOrOptions?: string | PathOptions
|
||||
) => {
|
||||
const paramName =
|
||||
typeof paramNameOrOptions === "string"
|
||||
? paramNameOrOptions
|
||||
: (paramNameOrOptions?.name ?? entity.name);
|
||||
|
||||
const userOptions: PathOptions = typeof paramNameOrOptions === "object" ? paramNameOrOptions : {};
|
||||
const options: PathParameterOptions = {
|
||||
type: "path",
|
||||
name: paramName ?? entity.name,
|
||||
explode: userOptions.explode ?? false,
|
||||
allowReserved: userOptions.allowReserved ?? false,
|
||||
style: userOptions.style ?? "simple",
|
||||
name: paramName,
|
||||
};
|
||||
context.program.stateMap(HttpStateKeys.path).set(entity, options);
|
||||
};
|
||||
|
|
|
@ -74,7 +74,9 @@ export interface BodyPropertyProperty extends HttpPropertyBase {
|
|||
}
|
||||
|
||||
export interface GetHttpPropertyOptions {
|
||||
isImplicitPathParam?: (param: ModelProperty) => boolean;
|
||||
implicitParameter?: (
|
||||
param: ModelProperty
|
||||
) => PathParameterOptions | QueryParameterOptions | undefined;
|
||||
}
|
||||
/**
|
||||
* Find the type of a property in a model
|
||||
|
@ -102,14 +104,57 @@ function getHttpProperty(
|
|||
statusCode: isStatusCode(program, property),
|
||||
};
|
||||
const defined = Object.entries(annotations).filter((x) => !!x[1]);
|
||||
const implicit = options.implicitParameter?.(property);
|
||||
|
||||
if (implicit && defined.length > 0) {
|
||||
if (implicit.type === "path" && annotations.path) {
|
||||
if (
|
||||
annotations.path.explode ||
|
||||
annotations.path.style !== "simple" ||
|
||||
annotations.path.allowReserved
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic({
|
||||
code: "use-uri-template",
|
||||
format: {
|
||||
param: property.name,
|
||||
},
|
||||
target: property,
|
||||
})
|
||||
);
|
||||
}
|
||||
} else if (implicit.type === "query" && annotations.query) {
|
||||
if (annotations.query.explode) {
|
||||
diagnostics.push(
|
||||
createDiagnostic({
|
||||
code: "use-uri-template",
|
||||
format: {
|
||||
param: property.name,
|
||||
},
|
||||
target: property,
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
diagnostics.push(
|
||||
createDiagnostic({
|
||||
code: "incompatible-uri-param",
|
||||
format: {
|
||||
param: property.name,
|
||||
uriKind: implicit.type,
|
||||
annotationKind: defined[0][0],
|
||||
},
|
||||
target: property,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
if (defined.length === 0) {
|
||||
if (options.isImplicitPathParam && options.isImplicitPathParam(property)) {
|
||||
if (implicit) {
|
||||
return createResult({
|
||||
kind: "path",
|
||||
options: {
|
||||
name: property.name,
|
||||
type: "path",
|
||||
},
|
||||
kind: implicit.type,
|
||||
options: implicit as any,
|
||||
property,
|
||||
});
|
||||
}
|
||||
return createResult({ kind: "bodyProperty" });
|
||||
|
|
|
@ -8,7 +8,7 @@ export * from "./decorators.js";
|
|||
export type { HttpProperty } from "./http-property.js";
|
||||
export * from "./metadata.js";
|
||||
export * from "./operations.js";
|
||||
export * from "./parameters.js";
|
||||
export { getOperationParameters } from "./parameters.js";
|
||||
export {
|
||||
HttpPart,
|
||||
getHttpFileModel,
|
||||
|
|
|
@ -9,12 +9,25 @@ export const $lib = createTypeSpecLibrary({
|
|||
default: paramMessage`HTTP verb already applied to ${"entityName"}`,
|
||||
},
|
||||
},
|
||||
"missing-path-param": {
|
||||
"missing-uri-param": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Route reference parameter '${"param"}' but wasn't found in operation parameters`,
|
||||
},
|
||||
},
|
||||
"incompatible-uri-param": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Parameter '${"param"}' is defined in the uri as a ${"uriKind"} but is annotated as a ${"annotationKind"}.`,
|
||||
},
|
||||
},
|
||||
"use-uri-template": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Parameter '${"param"}' is already defined in the uri template. Explode, style and allowReserved property must be defined in the uri template as described by RFC 6570.`,
|
||||
},
|
||||
},
|
||||
|
||||
"optional-path-param": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
|
@ -153,12 +166,6 @@ export const $lib = createTypeSpecLibrary({
|
|||
default: `A format must be specified for @header when type is an array. e.g. @header({format: "csv"})`,
|
||||
},
|
||||
},
|
||||
"query-format-required": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: `A format must be specified for @query when type is an array. e.g. @query({format: "multi"})`,
|
||||
},
|
||||
},
|
||||
},
|
||||
state: {
|
||||
authentication: { description: "State for the @auth decorator" },
|
||||
|
@ -187,6 +194,6 @@ export const $lib = createTypeSpecLibrary({
|
|||
file: { description: "State for the @Private.file decorator" },
|
||||
httpPart: { description: "State for the @Private.httpPart decorator" },
|
||||
},
|
||||
} as const);
|
||||
});
|
||||
|
||||
export const { reportDiagnostic, createDiagnostic, stateKeys: HttpStateKeys } = $lib;
|
||||
|
|
|
@ -221,7 +221,8 @@ function getHttpOperationInternal(
|
|||
|
||||
const httpOperation: HttpOperation = {
|
||||
path: route.path,
|
||||
pathSegments: route.pathSegments,
|
||||
uriTemplate: route.uriTemplate,
|
||||
pathSegments: [],
|
||||
verb: route.parameters.verb,
|
||||
container: operation.interface ?? operation.namespace ?? program.getGlobalNamespaceType(),
|
||||
parameters: route.parameters,
|
||||
|
|
|
@ -15,13 +15,16 @@ import {
|
|||
HttpOperationParameters,
|
||||
HttpVerb,
|
||||
OperationParameterOptions,
|
||||
PathParameterOptions,
|
||||
QueryParameterOptions,
|
||||
} from "./types.js";
|
||||
import { parseUriTemplate } from "./uri-template.js";
|
||||
|
||||
export function getOperationParameters(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
partialUriTemplate: string,
|
||||
overloadBase?: HttpOperation,
|
||||
knownPathParamNames: string[] = [],
|
||||
options: OperationParameterOptions = {}
|
||||
): [HttpOperationParameters, readonly Diagnostic[]] {
|
||||
const verb =
|
||||
|
@ -30,36 +33,75 @@ export function getOperationParameters(
|
|||
overloadBase?.verb;
|
||||
|
||||
if (verb) {
|
||||
return getOperationParametersForVerb(program, operation, verb, knownPathParamNames);
|
||||
return getOperationParametersForVerb(program, operation, verb, partialUriTemplate);
|
||||
}
|
||||
|
||||
// If no verb is explicitly specified, it is POST if there is a body and
|
||||
// GET otherwise. Theoretically, it is possible to use @visibility
|
||||
// strangely such that there is no body if the verb is POST and there is a
|
||||
// body if the verb is GET. In that rare case, GET is chosen arbitrarily.
|
||||
const post = getOperationParametersForVerb(program, operation, "post", knownPathParamNames);
|
||||
const post = getOperationParametersForVerb(program, operation, "post", partialUriTemplate);
|
||||
return post[0].body
|
||||
? post
|
||||
: getOperationParametersForVerb(program, operation, "get", knownPathParamNames);
|
||||
: getOperationParametersForVerb(program, operation, "get", partialUriTemplate);
|
||||
}
|
||||
|
||||
const operatorToStyle = {
|
||||
";": "matrix",
|
||||
"#": "fragment",
|
||||
".": "label",
|
||||
"/": "path",
|
||||
} as const;
|
||||
|
||||
function getOperationParametersForVerb(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
verb: HttpVerb,
|
||||
knownPathParamNames: string[]
|
||||
partialUriTemplate: string
|
||||
): [HttpOperationParameters, readonly Diagnostic[]] {
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
const visibility = resolveRequestVisibility(program, operation, verb);
|
||||
function isImplicitPathParam(param: ModelProperty) {
|
||||
const isTopLevel = param.model === operation.parameters;
|
||||
return isTopLevel && knownPathParamNames.includes(param.name);
|
||||
}
|
||||
const parsedUriTemplate = parseUriTemplate(partialUriTemplate);
|
||||
|
||||
const parameters: HttpOperationParameter[] = [];
|
||||
const { body: resolvedBody, metadata } = diagnostics.pipe(
|
||||
resolveHttpPayload(program, operation.parameters, visibility, "request", {
|
||||
isImplicitPathParam,
|
||||
implicitParameter: (
|
||||
param: ModelProperty
|
||||
): QueryParameterOptions | PathParameterOptions | undefined => {
|
||||
const isTopLevel = param.model === operation.parameters;
|
||||
const uriParam =
|
||||
isTopLevel && parsedUriTemplate.parameters.find((x) => x.name === param.name);
|
||||
|
||||
if (!uriParam) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const explode = uriParam.modifier?.type === "explode";
|
||||
if (uriParam.operator === "?" || uriParam.operator === "&") {
|
||||
return {
|
||||
type: "query",
|
||||
name: uriParam.name,
|
||||
explode,
|
||||
};
|
||||
} else if (uriParam.operator === "+") {
|
||||
return {
|
||||
type: "path",
|
||||
name: uriParam.name,
|
||||
explode,
|
||||
allowReserved: true,
|
||||
style: "simple",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "path",
|
||||
name: uriParam.name,
|
||||
explode,
|
||||
allowReserved: false,
|
||||
style: (uriParam.operator && operatorToStyle[uriParam.operator]) ?? "simple",
|
||||
};
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
createDiagnosticCollector,
|
||||
DecoratorContext,
|
||||
DiagnosticResult,
|
||||
Interface,
|
||||
|
@ -6,21 +7,22 @@ import {
|
|||
Operation,
|
||||
Program,
|
||||
Type,
|
||||
createDiagnosticCollector,
|
||||
validateDecoratorTarget,
|
||||
} from "@typespec/compiler";
|
||||
import { HttpStateKeys, createDiagnostic, reportDiagnostic } from "./lib.js";
|
||||
import { createDiagnostic, HttpStateKeys, reportDiagnostic } from "./lib.js";
|
||||
import { getOperationParameters } from "./parameters.js";
|
||||
import {
|
||||
HttpOperation,
|
||||
HttpOperationParameter,
|
||||
HttpOperationParameters,
|
||||
PathParameterOptions,
|
||||
RouteOptions,
|
||||
RoutePath,
|
||||
RouteProducer,
|
||||
RouteProducerResult,
|
||||
RouteResolutionOptions,
|
||||
} from "./types.js";
|
||||
import { extractParamsFromPath } from "./utils.js";
|
||||
import { parseUriTemplate, UriTemplate } from "./uri-template.js";
|
||||
|
||||
// The set of allowed segment separator characters
|
||||
const AllowedSegmentSeparators = ["/", ":"];
|
||||
|
@ -37,7 +39,7 @@ function normalizeFragment(fragment: string, trimLast = false) {
|
|||
return fragment;
|
||||
}
|
||||
|
||||
function joinPathSegments(rest: string[]) {
|
||||
export function joinPathSegments(rest: string[]) {
|
||||
let current = "";
|
||||
for (const [index, segment] of rest.entries()) {
|
||||
current += normalizeFragment(segment, index < rest.length - 1);
|
||||
|
@ -59,42 +61,60 @@ export function resolvePathAndParameters(
|
|||
overloadBase: HttpOperation | undefined,
|
||||
options: RouteResolutionOptions
|
||||
): DiagnosticResult<{
|
||||
readonly uriTemplate: string;
|
||||
path: string;
|
||||
pathSegments: string[];
|
||||
parameters: HttpOperationParameters;
|
||||
}> {
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
const { segments, parameters } = diagnostics.pipe(
|
||||
getRouteSegments(program, operation, overloadBase, options)
|
||||
const { uriTemplate, parameters } = diagnostics.pipe(
|
||||
getUriTemplateAndParameters(program, operation, overloadBase, options)
|
||||
);
|
||||
|
||||
const parsedUriTemplate = parseUriTemplate(uriTemplate);
|
||||
|
||||
// Pull out path parameters to verify what's in the path string
|
||||
const paramByName = new Set(
|
||||
parameters.parameters.filter(({ type }) => type === "path").map((x) => x.name)
|
||||
parameters.parameters
|
||||
.filter(({ type }) => type === "path" || type === "query")
|
||||
.map((x) => x.name)
|
||||
);
|
||||
|
||||
// Ensure that all of the parameters defined in the route are accounted for in
|
||||
// the operation parameters
|
||||
const routeParams = segments.flatMap(extractParamsFromPath);
|
||||
for (const routeParam of routeParams) {
|
||||
if (!paramByName.has(routeParam)) {
|
||||
for (const routeParam of parsedUriTemplate.parameters) {
|
||||
if (!paramByName.has(routeParam.name)) {
|
||||
diagnostics.add(
|
||||
createDiagnostic({
|
||||
code: "missing-path-param",
|
||||
format: { param: routeParam },
|
||||
code: "missing-uri-param",
|
||||
format: { param: routeParam.name },
|
||||
target: operation,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const path = produceLegacyPathFromUriTemplate(parsedUriTemplate);
|
||||
return diagnostics.wrap({
|
||||
path: buildPath(segments),
|
||||
pathSegments: segments,
|
||||
uriTemplate,
|
||||
path,
|
||||
parameters,
|
||||
});
|
||||
}
|
||||
|
||||
function produceLegacyPathFromUriTemplate(uriTemplate: UriTemplate) {
|
||||
let result = "";
|
||||
|
||||
for (const segment of uriTemplate.segments ?? []) {
|
||||
if (typeof segment === "string") {
|
||||
result += segment;
|
||||
} else if (segment.operator !== "?" && segment.operator !== "&") {
|
||||
result += `{${segment.name}}`;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function collectSegmentsAndOptions(
|
||||
program: Program,
|
||||
source: Interface | Namespace | undefined
|
||||
|
@ -110,27 +130,27 @@ function collectSegmentsAndOptions(
|
|||
return [[...parentSegments, ...(route ? [route] : [])], { ...parentOptions, ...options }];
|
||||
}
|
||||
|
||||
function getRouteSegments(
|
||||
function getUriTemplateAndParameters(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
overloadBase: HttpOperation | undefined,
|
||||
options: RouteResolutionOptions
|
||||
): DiagnosticResult<RouteProducerResult> {
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
const [parentSegments, parentOptions] = collectSegmentsAndOptions(
|
||||
program,
|
||||
operation.interface ?? operation.namespace
|
||||
);
|
||||
|
||||
const routeProducer = getRouteProducer(program, operation) ?? DefaultRouteProducer;
|
||||
const result = diagnostics.pipe(
|
||||
routeProducer(program, operation, parentSegments, overloadBase, {
|
||||
...parentOptions,
|
||||
...options,
|
||||
})
|
||||
);
|
||||
const [result, diagnostics] = routeProducer(program, operation, parentSegments, overloadBase, {
|
||||
...parentOptions,
|
||||
...options,
|
||||
});
|
||||
|
||||
return diagnostics.wrap(result);
|
||||
return [
|
||||
{ uriTemplate: buildPath([result.uriTemplate]), parameters: result.parameters },
|
||||
diagnostics,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -162,37 +182,61 @@ export function DefaultRouteProducer(
|
|||
): DiagnosticResult<RouteProducerResult> {
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
const routePath = getRoutePath(program, operation)?.path;
|
||||
const segments =
|
||||
const uriTemplate =
|
||||
!routePath && overloadBase
|
||||
? overloadBase.pathSegments
|
||||
: [...parentSegments, ...(routePath ? [routePath] : [])];
|
||||
const routeParams = segments.flatMap(extractParamsFromPath);
|
||||
? overloadBase.uriTemplate
|
||||
: joinPathSegments([...parentSegments, ...(routePath ? [routePath] : [])]);
|
||||
|
||||
const parsedUriTemplate = parseUriTemplate(uriTemplate);
|
||||
|
||||
const parameters: HttpOperationParameters = diagnostics.pipe(
|
||||
getOperationParameters(program, operation, overloadBase, routeParams, options.paramOptions)
|
||||
getOperationParameters(program, operation, uriTemplate, overloadBase, options.paramOptions)
|
||||
);
|
||||
|
||||
// Pull out path parameters to verify what's in the path string
|
||||
const unreferencedPathParamNames = new Set(
|
||||
parameters.parameters.filter(({ type }) => type === "path").map((x) => x.name)
|
||||
const unreferencedPathParamNames = new Map(
|
||||
parameters.parameters
|
||||
.filter(({ type }) => type === "path" || type === "query")
|
||||
.map((x) => [x.name, x])
|
||||
);
|
||||
|
||||
// Compile the list of all route params that aren't represented in the route
|
||||
for (const routeParam of routeParams) {
|
||||
unreferencedPathParamNames.delete(routeParam);
|
||||
}
|
||||
|
||||
// Add any remaining declared path params
|
||||
for (const paramName of unreferencedPathParamNames) {
|
||||
segments.push(`{${paramName}}`);
|
||||
for (const uriParam of parsedUriTemplate.parameters) {
|
||||
unreferencedPathParamNames.delete(uriParam.name);
|
||||
}
|
||||
|
||||
const resolvedUriTemplate = addOperationTemplateToUriTemplate(uriTemplate, [
|
||||
...unreferencedPathParamNames.values(),
|
||||
]);
|
||||
return diagnostics.wrap({
|
||||
segments,
|
||||
uriTemplate: resolvedUriTemplate,
|
||||
parameters,
|
||||
});
|
||||
}
|
||||
|
||||
const styleToOperator: Record<PathParameterOptions["style"], string> = {
|
||||
matrix: ";",
|
||||
label: ".",
|
||||
simple: "",
|
||||
path: "/",
|
||||
fragment: "#",
|
||||
};
|
||||
|
||||
function addOperationTemplateToUriTemplate(uriTemplate: string, params: HttpOperationParameter[]) {
|
||||
const pathParams = params
|
||||
.filter((x) => x.type === "path")
|
||||
.map((param) => {
|
||||
const operator = param.allowReserved ? "+" : styleToOperator[param.style];
|
||||
return `{${operator}${param.name}${param.explode ? "*" : ""}}`;
|
||||
});
|
||||
const queryParams = params.filter((x) => x.type === "query");
|
||||
|
||||
const pathPart = joinPathSegments([uriTemplate, ...pathParams]);
|
||||
return (
|
||||
pathPart + (queryParams.length > 0 ? `{?${queryParams.map((x) => x.name).join(",")}}` : "")
|
||||
);
|
||||
}
|
||||
|
||||
export function setRouteProducer(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
Tuple,
|
||||
Type,
|
||||
} from "@typespec/compiler";
|
||||
import { PathOptions, QueryOptions } from "../generated-defs/TypeSpec.Http.js";
|
||||
import { HeaderProperty, HttpProperty } from "./http-property.js";
|
||||
|
||||
/**
|
||||
|
@ -277,7 +278,7 @@ export interface RouteResolutionOptions extends RouteOptions {
|
|||
}
|
||||
|
||||
export interface RouteProducerResult {
|
||||
segments: string[];
|
||||
uriTemplate: string;
|
||||
parameters: HttpOperationParameters;
|
||||
}
|
||||
|
||||
|
@ -299,26 +300,30 @@ export interface HeaderFieldOptions {
|
|||
format?: "csv" | "multi" | "ssv" | "tsv" | "pipes" | "simple" | "form";
|
||||
}
|
||||
|
||||
export interface QueryParameterOptions {
|
||||
export interface QueryParameterOptions extends Required<Omit<QueryOptions, "format">> {
|
||||
type: "query";
|
||||
name: string;
|
||||
/**
|
||||
* The string format of the array. "csv" and "simple" are used interchangeably, as are
|
||||
* "multi" and "form".
|
||||
* @deprecated use explode and `@encode` decorator instead.
|
||||
*/
|
||||
format?: "multi" | "csv" | "ssv" | "tsv" | "pipes" | "simple" | "form";
|
||||
format?: "csv" | "multi" | "ssv" | "tsv" | "pipes" | "simple" | "form";
|
||||
}
|
||||
|
||||
export interface PathParameterOptions {
|
||||
export interface PathParameterOptions extends Required<PathOptions> {
|
||||
type: "path";
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type HttpOperationParameter = (
|
||||
| HeaderFieldOptions
|
||||
| QueryParameterOptions
|
||||
| PathParameterOptions
|
||||
) & {
|
||||
export type HttpOperationParameter =
|
||||
| HttpOperationHeaderParameter
|
||||
| HttpOperationQueryParameter
|
||||
| HttpOperationPathParameter;
|
||||
|
||||
export type HttpOperationHeaderParameter = HeaderFieldOptions & {
|
||||
param: ModelProperty;
|
||||
};
|
||||
export type HttpOperationQueryParameter = QueryParameterOptions & {
|
||||
param: ModelProperty;
|
||||
};
|
||||
export type HttpOperationPathParameter = PathParameterOptions & {
|
||||
param: ModelProperty;
|
||||
};
|
||||
|
||||
|
@ -362,12 +367,21 @@ export interface HttpService {
|
|||
|
||||
export interface HttpOperation {
|
||||
/**
|
||||
* Route path
|
||||
* The fully resolved uri template as defined by http://tools.ietf.org/html/rfc6570.
|
||||
* @example "/foo/{bar}/baz{?qux}"
|
||||
* @example "/foo/{+path}"
|
||||
*/
|
||||
readonly uriTemplate: string;
|
||||
|
||||
/**
|
||||
* Route path.
|
||||
* Not recommended use {@link uriTemplate} instead. This will not work for complex cases like not-escaping reserved chars.
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* Path segments
|
||||
* @deprecated use {@link uriTemplate} instead
|
||||
*/
|
||||
pathSegments: string[];
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
const operators = ["+", "#", ".", "/", ";", "?", "&"] as const;
|
||||
type Operator = (typeof operators)[number];
|
||||
|
||||
export interface UriTemplateParameter {
|
||||
readonly name: string;
|
||||
readonly operator?: Operator;
|
||||
readonly modifier?: { type: "explode" } | { type: "prefix"; value: number };
|
||||
}
|
||||
|
||||
export interface UriTemplate {
|
||||
readonly segments?: (string | UriTemplateParameter)[];
|
||||
readonly parameters: UriTemplateParameter[];
|
||||
}
|
||||
|
||||
const uriTemplateRegex = /\{([^{}]+)\}|([^{}]+)/g;
|
||||
const expressionRegex = /([^:*]*)(?::(\d+)|(\*))?/;
|
||||
|
||||
/**
|
||||
* Parse a URI template according to [RFC-6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3)
|
||||
*/
|
||||
export function parseUriTemplate(template: string): UriTemplate {
|
||||
const parameters: UriTemplateParameter[] = [];
|
||||
const segments: (string | UriTemplateParameter)[] = [];
|
||||
const matches = template.matchAll(uriTemplateRegex);
|
||||
for (let [_, expression, literal] of matches) {
|
||||
if (expression) {
|
||||
let operator: Operator | undefined;
|
||||
if (operators.includes(expression[0] as any)) {
|
||||
operator = expression[0] as any;
|
||||
expression = expression.slice(1);
|
||||
}
|
||||
|
||||
const items = expression.split(",");
|
||||
for (const item of items) {
|
||||
const match = item.match(expressionRegex)!;
|
||||
const name = match[1];
|
||||
const parameter: UriTemplateParameter = {
|
||||
name: name,
|
||||
operator,
|
||||
modifier: match[3]
|
||||
? { type: "explode" }
|
||||
: match[2]
|
||||
? { type: "prefix", value: Number(match[2]) }
|
||||
: undefined,
|
||||
};
|
||||
parameters.push(parameter);
|
||||
segments.push(parameter);
|
||||
}
|
||||
} else {
|
||||
segments.push(literal);
|
||||
}
|
||||
}
|
||||
return { segments, parameters };
|
||||
}
|
|
@ -157,8 +157,8 @@ describe("http: decorators", () => {
|
|||
it("emit diagnostics when query name is not a string or of type QueryOptions", async () => {
|
||||
const diagnostics = await runner.diagnose(`
|
||||
op test(@query(123) MyQuery: string): string;
|
||||
op test2(@query({name: 123}) MyQuery: string): string;
|
||||
op test3(@query({format: "invalid"}) MyQuery: string): string;
|
||||
op test2(@query(#{name: 123}) MyQuery: string): string;
|
||||
op test3(@query(#{format: "invalid"}) MyQuery: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, [
|
||||
|
@ -174,17 +174,6 @@ describe("http: decorators", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("emit diagnostics when query is not specifing format but is an array", async () => {
|
||||
const diagnostics = await runner.diagnose(`
|
||||
op test(@query select: string[]): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/query-format-required",
|
||||
message: `A format must be specified for @query when type is an array. e.g. @query({format: "multi"})`,
|
||||
});
|
||||
});
|
||||
|
||||
it("generate query name from property name", async () => {
|
||||
const { select } = await runner.compile(`
|
||||
op test(@test @query select: string): string;
|
||||
|
@ -202,15 +191,43 @@ describe("http: decorators", () => {
|
|||
strictEqual(getQueryParamName(runner.program, select), "$select");
|
||||
});
|
||||
|
||||
describe("change format for array value", () => {
|
||||
["csv", "tsv", "ssv", "simple", "form", "pipes"].forEach((format) => {
|
||||
it("specify explode: true", async () => {
|
||||
const { selects } = await runner.compile(`
|
||||
op test(@test @query(#{ explode: true }) selects: string[]): string;
|
||||
`);
|
||||
expect(getQueryParamOptions(runner.program, selects)).toEqual({
|
||||
type: "query",
|
||||
name: "selects",
|
||||
format: "multi",
|
||||
explode: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("LEGACY: change format for array value", () => {
|
||||
["csv", "tsv", "ssv", "simple", "pipes"].forEach((format) => {
|
||||
it(`set query format to "${format}"`, async () => {
|
||||
const { selects } = await runner.compile(`
|
||||
op test(@test @query({name: "$select", format: "${format}"}) selects: string[]): string;
|
||||
#suppress "deprecated" "Test"
|
||||
op test(@test @query(#{name: "$select", format: "${format}"}) selects: string[]): string;
|
||||
`);
|
||||
deepStrictEqual(getQueryParamOptions(runner.program, selects), {
|
||||
type: "query",
|
||||
name: "$select",
|
||||
explode: false,
|
||||
format,
|
||||
});
|
||||
});
|
||||
});
|
||||
["form"].forEach((format) => {
|
||||
it(`set query format to "${format}"`, async () => {
|
||||
const { selects } = await runner.compile(`
|
||||
#suppress "deprecated" "Test"
|
||||
op test(@test @query(#{name: "$select", format: "${format}"}) selects: string[]): string;
|
||||
`);
|
||||
deepStrictEqual(getQueryParamOptions(runner.program, selects), {
|
||||
type: "query",
|
||||
name: "$select",
|
||||
explode: true,
|
||||
format,
|
||||
});
|
||||
});
|
||||
|
@ -372,6 +389,9 @@ describe("http: decorators", () => {
|
|||
deepStrictEqual(getPathParamOptions(runner.program, select), {
|
||||
type: "path",
|
||||
name: "$select",
|
||||
allowReserved: false,
|
||||
explode: false,
|
||||
style: "simple",
|
||||
});
|
||||
strictEqual(getPathParamName(runner.program, select), "$select");
|
||||
});
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Operation } from "@typespec/compiler";
|
||||
import { expectDiagnosticEmpty, expectDiagnostics } from "@typespec/compiler/testing";
|
||||
import { deepStrictEqual, strictEqual } from "assert";
|
||||
import { describe, it } from "vitest";
|
||||
import { HttpOperation, getRoutePath } from "../src/index.js";
|
||||
import { deepStrictEqual, ok, strictEqual } from "assert";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PathOptions } from "../generated-defs/TypeSpec.Http.js";
|
||||
import { HttpOperation, HttpOperationParameter, getRoutePath } from "../src/index.js";
|
||||
import {
|
||||
compileOperations,
|
||||
createHttpTestRunner,
|
||||
|
@ -171,7 +172,7 @@ describe("http: routes", () => {
|
|||
`@route("/foo/{myParam}/") op test(@path other: string): void;`
|
||||
);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/missing-path-param",
|
||||
code: "@typespec/http/missing-uri-param",
|
||||
message: "Route reference parameter 'myParam' but wasn't found in operation parameters",
|
||||
});
|
||||
});
|
||||
|
@ -508,3 +509,152 @@ describe("http: routes", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("uri template", () => {
|
||||
async function getOp(code: string) {
|
||||
const ops = await getOperations(code);
|
||||
return ops[0];
|
||||
}
|
||||
describe("extract implicit parameters", () => {
|
||||
async function getParameter(code: string, name: string) {
|
||||
const op = await getOp(code);
|
||||
const param = op.parameters.parameters.find((x) => x.name === name);
|
||||
ok(param);
|
||||
expect(param.name).toEqual(name);
|
||||
return param;
|
||||
}
|
||||
|
||||
function expectPathParameter(param: HttpOperationParameter, expected: PathOptions) {
|
||||
strictEqual(param.type, "path");
|
||||
const { style, explode, allowReserved } = param;
|
||||
expect({ style, explode, allowReserved }).toEqual(expected);
|
||||
}
|
||||
|
||||
it("extract simple path parameter", async () => {
|
||||
const param = await getParameter(`@route("/bar/{foo}") op foo(foo: string): void;`, "foo");
|
||||
expectPathParameter(param, { style: "simple", allowReserved: false, explode: false });
|
||||
});
|
||||
|
||||
it("+ operator map to allowReserved", async () => {
|
||||
const param = await getParameter(`@route("/bar/{+foo}") op foo(foo: string): void;`, "foo");
|
||||
expectPathParameter(param, { style: "simple", allowReserved: true, explode: false });
|
||||
});
|
||||
|
||||
it.each([
|
||||
[";", "matrix"],
|
||||
["#", "fragment"],
|
||||
[".", "label"],
|
||||
["/", "path"],
|
||||
] as const)("%s map to style: %s", async (operator, style) => {
|
||||
const param = await getParameter(
|
||||
`@route("/bar/{${operator}foo}") op foo(foo: string): void;`,
|
||||
"foo"
|
||||
);
|
||||
expectPathParameter(param, { style, allowReserved: false, explode: false });
|
||||
});
|
||||
|
||||
function expectQueryParameter(param: HttpOperationParameter, expected: PathOptions) {
|
||||
strictEqual(param.type, "query");
|
||||
const { explode } = param;
|
||||
expect({ explode }).toEqual(expected);
|
||||
}
|
||||
|
||||
it("extract simple query parameter", async () => {
|
||||
const param = await getParameter(`@route("/bar{?foo}") op foo(foo: string): void;`, "foo");
|
||||
expectQueryParameter(param, { explode: false });
|
||||
});
|
||||
|
||||
it("extract explode query parameter", async () => {
|
||||
const param = await getParameter(`@route("/bar{?foo*}") op foo(foo: string): void;`, "foo");
|
||||
expectQueryParameter(param, { explode: true });
|
||||
});
|
||||
|
||||
it("extract simple query continuation parameter", async () => {
|
||||
const param = await getParameter(
|
||||
`@route("/bar?fixed=yes{&foo}") op foo(foo: string): void;`,
|
||||
"foo"
|
||||
);
|
||||
expectQueryParameter(param, { explode: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("build uriTemplate from parameter", () => {
|
||||
it.each([
|
||||
["@path one: string", "/foo/{one}"],
|
||||
["@path(#{allowReserved: true}) one: string", "/foo/{+one}"],
|
||||
["@path(#{explode: true}) one: string", "/foo/{one*}"],
|
||||
[`@path(#{style: "matrix"}) one: string`, "/foo/{;one}"],
|
||||
[`@path(#{style: "label"}) one: string`, "/foo/{.one}"],
|
||||
[`@path(#{style: "fragment"}) one: string`, "/foo/{#one}"],
|
||||
[`@path(#{style: "path"}) one: string`, "/foo/{/one}"],
|
||||
["@path(#{allowReserved: true, explode: true}) one: string", "/foo/{+one*}"],
|
||||
["@query one: string", "/foo{?one}"],
|
||||
])("%s -> %s", async (param, expectedUri) => {
|
||||
const op = await getOp(`@route("/foo") op foo(${param}): void;`);
|
||||
expect(op.uriTemplate).toEqual(expectedUri);
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic when annotating a path parameter with @query", async () => {
|
||||
const diagnostics = await diagnoseOperations(
|
||||
`@route("/bar/{foo}") op foo(@query foo: string): void;`
|
||||
);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/incompatible-uri-param",
|
||||
message: "Parameter 'foo' is defined in the uri as a path but is annotated as a query.",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic when annotating a query parameter with @path", async () => {
|
||||
const diagnostics = await diagnoseOperations(
|
||||
`@route("/bar/{?foo}") op foo(@path foo: string): void;`
|
||||
);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/incompatible-uri-param",
|
||||
message: "Parameter 'foo' is defined in the uri as a query but is annotated as a path.",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic when annotating a query continuation parameter with @path", async () => {
|
||||
const diagnostics = await diagnoseOperations(
|
||||
`@route("/bar/?bar=def{&foo}") op foo(@path foo: string): void;`
|
||||
);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/incompatible-uri-param",
|
||||
message: "Parameter 'foo' is defined in the uri as a query but is annotated as a path.",
|
||||
});
|
||||
});
|
||||
|
||||
describe("emit diagnostic if using any of the path options when parameter is already defined in the uri template", () => {
|
||||
it.each([
|
||||
"#{ allowReserved: true }",
|
||||
"#{ explode: true }",
|
||||
`#{ style: "label" }`,
|
||||
`#{ style: "matrix" }`,
|
||||
`#{ style: "fragment" }`,
|
||||
`#{ style: "path" }`,
|
||||
])("%s", async (options) => {
|
||||
const diagnostics = await diagnoseOperations(
|
||||
`@route("/bar/{foo}") op foo(@path(${options}) foo: string): void;`
|
||||
);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/use-uri-template",
|
||||
message:
|
||||
"Parameter 'foo' is already defined in the uri template. Explode, style and allowReserved property must be defined in the uri template as described by RFC 6570.",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("emit diagnostic if using any of the query options when parameter is already defined in the uri template", () => {
|
||||
it.each(["#{ explode: true }"])("%s", async (options) => {
|
||||
const diagnostics = await diagnoseOperations(
|
||||
`@route("/bar{?foo}") op foo(@query(${options}) foo: string): void;`
|
||||
);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/use-uri-template",
|
||||
message:
|
||||
"Parameter 'foo' is already defined in the uri template. Explode, style and allowReserved property must be defined in the uri template as described by RFC 6570.",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { parseUriTemplate } from "../src/uri-template.js";
|
||||
|
||||
it("no parameter", () => {
|
||||
expect(parseUriTemplate("/foo").parameters).toEqual([]);
|
||||
});
|
||||
|
||||
it("simple parameters", () => {
|
||||
expect(parseUriTemplate("/foo/{one}/bar/baz/{two}").parameters).toEqual([
|
||||
{ name: "one" },
|
||||
{ name: "two" },
|
||||
]);
|
||||
});
|
||||
|
||||
describe("operators", () => {
|
||||
it.each(["+", "#", ".", "/", ";", "?", "&"])("%s", (operator) => {
|
||||
expect(parseUriTemplate(`/foo/{${operator}one}`).parameters).toEqual([
|
||||
{ name: "one", operator },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("define explode parameter", () => {
|
||||
expect(parseUriTemplate("/foo/{one*}").parameters).toEqual([
|
||||
{ name: "one", modifier: { type: "explode" } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("define prefix parameter", () => {
|
||||
expect(parseUriTemplate("/foo/{one:3}").parameters).toEqual([
|
||||
{ name: "one", modifier: { type: "prefix", value: 3 } },
|
||||
]);
|
||||
});
|
|
@ -170,6 +170,18 @@ export const libDef = {
|
|||
default: paramMessage`Collection format '${"value"}' is not supported in OpenAPI3 ${"paramType"} parameters. Defaulting to type 'string'.`,
|
||||
},
|
||||
},
|
||||
"invalid-style": {
|
||||
severity: "warning",
|
||||
messages: {
|
||||
default: paramMessage`Style '${"style"}' is not supported in OpenAPI3 ${"paramType"} parameters. Defaulting to style 'simple'.`,
|
||||
},
|
||||
},
|
||||
"path-reserved-expansion": {
|
||||
severity: "warning",
|
||||
messages: {
|
||||
default: `Reserved expansion of path parameter with '+' operator #{allowReserved: true} is not supported in OpenAPI3.`,
|
||||
},
|
||||
},
|
||||
"resource-namespace": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
|
|
|
@ -68,7 +68,6 @@ import {
|
|||
isOrExtendsHttpFile,
|
||||
isOverloadSameEndpoint,
|
||||
MetadataInfo,
|
||||
QueryParameterOptions,
|
||||
reportIfNoRoutes,
|
||||
resolveAuthentication,
|
||||
resolveRequestVisibility,
|
||||
|
@ -1221,13 +1220,13 @@ function createOAPIEmitter(
|
|||
...getOpenAPIParameterBase(parameter.param, visibility),
|
||||
} as any;
|
||||
|
||||
const format = mapParameterFormat(parameter);
|
||||
if (format === undefined) {
|
||||
const attributes = getParameterAttributes(parameter);
|
||||
if (attributes === undefined) {
|
||||
param.schema = {
|
||||
type: "string",
|
||||
};
|
||||
} else {
|
||||
Object.assign(param, format);
|
||||
Object.assign(param, attributes);
|
||||
}
|
||||
|
||||
return param;
|
||||
|
@ -1403,16 +1402,87 @@ function createOAPIEmitter(
|
|||
return target;
|
||||
}
|
||||
|
||||
function mapParameterFormat(
|
||||
function getParameterAttributes(
|
||||
parameter: HttpOperationParameter
|
||||
): { style?: string; explode?: boolean } | undefined {
|
||||
switch (parameter.type) {
|
||||
case "header":
|
||||
return mapHeaderParameterFormat(parameter);
|
||||
case "query":
|
||||
return mapQueryParameterFormat(parameter);
|
||||
return getQueryParameterAttributes(parameter);
|
||||
case "path":
|
||||
return {};
|
||||
return getPathParameterAttributes(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
function getPathParameterAttributes(parameter: HttpOperationParameter & { type: "path" }) {
|
||||
if (parameter.allowReserved) {
|
||||
diagnostics.add(
|
||||
createDiagnostic({
|
||||
code: "path-reserved-expansion",
|
||||
target: parameter.param,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const attributes: { style?: string; explode?: boolean } = {};
|
||||
|
||||
if (parameter.explode) {
|
||||
attributes.explode = true;
|
||||
}
|
||||
|
||||
switch (parameter.style) {
|
||||
case "label":
|
||||
attributes.style = "label";
|
||||
break;
|
||||
case "matrix":
|
||||
attributes.style = "matrix";
|
||||
break;
|
||||
case "simple":
|
||||
break;
|
||||
default:
|
||||
diagnostics.add(
|
||||
createDiagnostic({
|
||||
code: "invalid-style",
|
||||
format: { style: parameter.style, paramType: "path" },
|
||||
target: parameter.param,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
function getQueryParameterAttributes(parameter: HttpOperationParameter & { type: "query" }) {
|
||||
const attributes: { style?: string; explode?: boolean } = {};
|
||||
|
||||
if (parameter.explode) {
|
||||
attributes.explode = true;
|
||||
}
|
||||
|
||||
switch (parameter.format) {
|
||||
case "ssv":
|
||||
return { style: "spaceDelimited", explode: false };
|
||||
case "pipes":
|
||||
return { style: "pipeDelimited", explode: false };
|
||||
case undefined:
|
||||
case "csv":
|
||||
case "simple":
|
||||
case "multi":
|
||||
case "form":
|
||||
return attributes;
|
||||
default:
|
||||
diagnostics.add(
|
||||
createDiagnostic({
|
||||
code: "invalid-format",
|
||||
format: {
|
||||
paramType: "query",
|
||||
value: parameter.format,
|
||||
},
|
||||
target: parameter.param,
|
||||
})
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1441,39 +1511,6 @@ function createOAPIEmitter(
|
|||
return undefined;
|
||||
}
|
||||
}
|
||||
function mapQueryParameterFormat(
|
||||
parameter: QueryParameterOptions & {
|
||||
param: ModelProperty;
|
||||
}
|
||||
): { style?: string; explode?: boolean } | undefined {
|
||||
switch (parameter.format) {
|
||||
case undefined:
|
||||
return {};
|
||||
case "csv":
|
||||
case "simple":
|
||||
return { style: "form", explode: false };
|
||||
case "multi":
|
||||
case "form":
|
||||
return { style: "form", explode: true };
|
||||
case "ssv":
|
||||
return { style: "spaceDelimited", explode: false };
|
||||
case "pipes":
|
||||
return { style: "pipeDelimited", explode: false };
|
||||
|
||||
default:
|
||||
diagnostics.add(
|
||||
createDiagnostic({
|
||||
code: "invalid-format",
|
||||
format: {
|
||||
paramType: "query",
|
||||
value: parameter.format,
|
||||
},
|
||||
target: parameter.param,
|
||||
})
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function emitParameters() {
|
||||
for (const [property, param] of params) {
|
||||
|
|
|
@ -1,33 +1,56 @@
|
|||
import { expectDiagnostics } from "@typespec/compiler/testing";
|
||||
import { deepStrictEqual, ok, strictEqual } from "assert";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { OpenAPI3PathParameter, OpenAPI3QueryParameter } from "../src/types.js";
|
||||
import { diagnoseOpenApiFor, openApiFor } from "./test-host.js";
|
||||
|
||||
describe("openapi3: parameters", () => {
|
||||
describe("query parameters", () => {
|
||||
async function getQueryParam(code: string): Promise<OpenAPI3QueryParameter> {
|
||||
const res = await openApiFor(code);
|
||||
const param = res.paths[`/`].get.parameters[0];
|
||||
strictEqual(param.in, "query");
|
||||
return param;
|
||||
}
|
||||
|
||||
it("create a query param", async () => {
|
||||
const res = await openApiFor(
|
||||
`
|
||||
op test(@query arg1: string): void;
|
||||
const param = await getQueryParam(
|
||||
`op test(@query myParam: string): void;
|
||||
`
|
||||
);
|
||||
strictEqual(res.paths["/"].get.parameters[0].in, "query");
|
||||
strictEqual(res.paths["/"].get.parameters[0].name, "arg1");
|
||||
deepStrictEqual(res.paths["/"].get.parameters[0].schema, { type: "string" });
|
||||
strictEqual(param.name, "myParam");
|
||||
deepStrictEqual(param.schema, { type: "string" });
|
||||
});
|
||||
|
||||
it("create a query param with a different name", async () => {
|
||||
const res = await openApiFor(
|
||||
const param = await getQueryParam(
|
||||
`
|
||||
op test(@query("$select") select: string): void;
|
||||
`
|
||||
);
|
||||
strictEqual(res.paths["/"].get.parameters[0].in, "query");
|
||||
strictEqual(res.paths["/"].get.parameters[0].name, "$select");
|
||||
strictEqual(param.in, "query");
|
||||
strictEqual(param.name, "$select");
|
||||
});
|
||||
|
||||
it("create a query param of array type", async () => {
|
||||
describe("set explode: true", () => {
|
||||
it("with option", async () => {
|
||||
const param = await getQueryParam(`op test(@query(#{explode: true}) myParam: string): void;`);
|
||||
expect(param).toMatchObject({
|
||||
explode: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("with uri template", async () => {
|
||||
const param = await getQueryParam(`@route("{?myParam*}") op test(myParam: string): void;`);
|
||||
expect(param).toMatchObject({
|
||||
explode: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("LEGACY: specify the format", async () => {
|
||||
const res = await openApiFor(
|
||||
`
|
||||
#suppress "deprecated" "test"
|
||||
op test(
|
||||
@query({name: "$multi", format: "multi"}) multis: string[],
|
||||
@query({name: "$csv", format: "csv"}) csvs: string[],
|
||||
|
@ -42,7 +65,6 @@ describe("openapi3: parameters", () => {
|
|||
deepStrictEqual(params[0], {
|
||||
in: "query",
|
||||
name: "$multi",
|
||||
style: "form",
|
||||
required: true,
|
||||
explode: true,
|
||||
schema: {
|
||||
|
@ -55,8 +77,6 @@ describe("openapi3: parameters", () => {
|
|||
deepStrictEqual(params[1], {
|
||||
in: "query",
|
||||
name: "$csv",
|
||||
style: "form",
|
||||
explode: false,
|
||||
schema: {
|
||||
type: "array",
|
||||
items: {
|
||||
|
@ -417,16 +437,136 @@ describe("openapi3: parameters", () => {
|
|||
ok(res.paths["/"].post.requestBody.content["application/json"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("path parameters", () => {
|
||||
it("figure out the route parameter from the name of the param", async () => {
|
||||
const res = await openApiFor(`op test(@path myParam: string): void;`);
|
||||
expect(res.paths).toHaveProperty("/{myParam}");
|
||||
describe("path parameters", () => {
|
||||
async function getPathParam(code: string, name = "myParam"): Promise<OpenAPI3PathParameter> {
|
||||
const res = await openApiFor(code);
|
||||
return res.paths[`/{${name}}`].get.parameters[0];
|
||||
}
|
||||
|
||||
it("figure out the route parameter from the name of the param", async () => {
|
||||
const res = await openApiFor(`op test(@path myParam: string): void;`);
|
||||
expect(res.paths).toHaveProperty("/{myParam}");
|
||||
});
|
||||
|
||||
it("uses explicit name provided from @path", async () => {
|
||||
const res = await openApiFor(`op test(@path("my-custom-path") myParam: string): void;`);
|
||||
expect(res.paths).toHaveProperty("/{my-custom-path}");
|
||||
});
|
||||
|
||||
describe("set explode: true", () => {
|
||||
it("with option", async () => {
|
||||
const param = await getPathParam(`op test(@path(#{explode: true}) myParam: string[]): void;`);
|
||||
expect(param).toMatchObject({
|
||||
explode: true,
|
||||
schema: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
});
|
||||
});
|
||||
it("with uri template", async () => {
|
||||
const param = await getPathParam(`@route("{myParam*}") op test(myParam: string[]): void;`);
|
||||
expect(param).toMatchObject({
|
||||
explode: true,
|
||||
schema: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("set style: simple", () => {
|
||||
it("with option", async () => {
|
||||
const param = await getPathParam(`op test(@path(#{style: "simple"}) myParam: string): void;`);
|
||||
expect(param).not.toHaveProperty("style");
|
||||
});
|
||||
|
||||
it("uses explicit name provided from @path", async () => {
|
||||
const res = await openApiFor(`op test(@path("my-custom-path") myParam: string): void;`);
|
||||
expect(res.paths).toHaveProperty("/{my-custom-path}");
|
||||
it("with uri template", async () => {
|
||||
const param = await getPathParam(`@route("{myParam}") op test(myParam: string): void;`);
|
||||
expect(param).not.toHaveProperty("style");
|
||||
});
|
||||
});
|
||||
|
||||
describe("set style: label", () => {
|
||||
it("with option", async () => {
|
||||
const param = await getPathParam(`op test(@path(#{style: "label"}) myParam: string): void;`);
|
||||
expect(param).toMatchObject({
|
||||
style: "label",
|
||||
});
|
||||
});
|
||||
|
||||
it("with uri template", async () => {
|
||||
const param = await getPathParam(`@route("{.myParam}") op test(myParam: string): void;`);
|
||||
expect(param).toMatchObject({
|
||||
style: "label",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("set style: matrix", () => {
|
||||
it("with option", async () => {
|
||||
const param = await getPathParam(`op test(@path(#{style: "matrix"}) myParam: string): void;`);
|
||||
expect(param).toMatchObject({
|
||||
style: "matrix",
|
||||
});
|
||||
});
|
||||
|
||||
it("with uri template", async () => {
|
||||
const param = await getPathParam(`@route("{;myParam}") op test(myParam: string): void;`);
|
||||
expect(param).toMatchObject({
|
||||
style: "matrix",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("emit diagnostic when using style: path", () => {
|
||||
it("with option", async () => {
|
||||
const diagnostics = await diagnoseOpenApiFor(
|
||||
`op test(@path(#{style: "path"}) myParam: string): void;`
|
||||
);
|
||||
expectDiagnostics(diagnostics, { code: "@typespec/openapi3/invalid-style" });
|
||||
});
|
||||
|
||||
it("with uri template", async () => {
|
||||
const diagnostics = await diagnoseOpenApiFor(
|
||||
`@route("{/myParam}") op test(myParam: string): void;`
|
||||
);
|
||||
expectDiagnostics(diagnostics, { code: "@typespec/openapi3/invalid-style" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("emit diagnostic when using style: fragment", () => {
|
||||
it("with option", async () => {
|
||||
const diagnostics = await diagnoseOpenApiFor(
|
||||
`op test(@path(#{style: "fragment"}) myParam: string): void;`
|
||||
);
|
||||
expectDiagnostics(diagnostics, { code: "@typespec/openapi3/invalid-style" });
|
||||
});
|
||||
|
||||
it("with uri template", async () => {
|
||||
const diagnostics = await diagnoseOpenApiFor(
|
||||
`@route("{#myParam}") op test(myParam: string): void;`
|
||||
);
|
||||
expectDiagnostics(diagnostics, { code: "@typespec/openapi3/invalid-style" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("emit diagnostic when using reserved expansion", () => {
|
||||
it("with option", async () => {
|
||||
const diagnostics = await diagnoseOpenApiFor(
|
||||
`op test(@path(#{allowReserved: true}) myParam: string): void;`
|
||||
);
|
||||
expectDiagnostics(diagnostics, { code: "@typespec/openapi3/path-reserved-expansion" });
|
||||
});
|
||||
|
||||
it("with uri template", async () => {
|
||||
const diagnostics = await diagnoseOpenApiFor(
|
||||
`@route("{+myParam}") op test(myParam: string): void;`
|
||||
);
|
||||
expectDiagnostics(diagnostics, { code: "@typespec/openapi3/path-reserved-expansion" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
HttpOperationParameter,
|
||||
HttpOperationParameters,
|
||||
HttpVerb,
|
||||
joinPathSegments,
|
||||
RouteOptions,
|
||||
RouteProducerResult,
|
||||
setRouteProducer,
|
||||
|
@ -114,7 +115,7 @@ function autoRouteProducer(
|
|||
};
|
||||
|
||||
const parameters: HttpOperationParameters = diagnostics.pipe(
|
||||
getOperationParameters(program, operation, undefined, [], paramOptions)
|
||||
getOperationParameters(program, operation, "", undefined, paramOptions)
|
||||
);
|
||||
|
||||
for (const httpParam of parameters.parameters) {
|
||||
|
@ -155,7 +156,7 @@ function autoRouteProducer(
|
|||
addActionFragment(program, operation, segments);
|
||||
|
||||
return diagnostics.wrap({
|
||||
segments,
|
||||
uriTemplate: joinPathSegments(segments),
|
||||
parameters: {
|
||||
...parameters,
|
||||
parameters: filteredParameters,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"references": [{ "path": "../compiler/tsconfig.json" }],
|
||||
"references": [{ "path": "../compiler/tsconfig.json" }, { "path": "../http/tsconfig.json" }],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
|
|
|
@ -64,9 +64,7 @@ namespace Hello {
|
|||
@get op read(
|
||||
@path id: string,
|
||||
|
||||
@query({
|
||||
format: "multi",
|
||||
})
|
||||
@query(#{ explode: true })
|
||||
fieldMask: string[],
|
||||
): ReadablePerson;
|
||||
@post op create(@body person: WritablePerson): ReadablePerson;
|
||||
|
|
|
@ -86,7 +86,6 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
type: string
|
||||
style: form
|
||||
explode: true
|
||||
responses:
|
||||
'200':
|
||||
|
|
Загрузка…
Ссылка в новой задаче