Skip to content

Commit

Permalink
Multipart explicit parts (#3342)
Browse files Browse the repository at this point in the history
resolve #3046
[Playground](https://cadlplayground.z22.web.core.windows.net/prs/3342/)

Add the following:
- `@multipartBody` decorator
- `File` type
- `HttpPart<Type, Options>` type


Had to do a decent amount of refactoring to be able to reuse the body
parsing, this result in a much cleaner resolution of the body and other
metadata properties across request, response and metadata.

The way it works now is instead of calling `gatherMetadata` that would
just get the properties that are metadata but also ones with `@body` and
`@bodyRoot` we now call a `resolveHtpProperties`, this does the same
resolution in term of filtering properties but it also figure out what
is the kind of property in the concept of http(header, query, body,
etc.) this leaves the error resolution to this function for duplicate
annotations.
What is nice is now we don't need to keep asking oh is this a query or a
header or a body we can just check the kind of `HttpProperty`

also resolve #1311
  • Loading branch information
timotheeguerin committed Jun 3, 2024
1 parent 75f407c commit 40df1ec
Show file tree
Hide file tree
Showing 37 changed files with 1,773 additions and 585 deletions.
6 changes: 6 additions & 0 deletions .chronus/changes/feature-multipart-v2-2024-4-14-16-27-48.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
changeKind: internal
packages:
- "@typespec/rest"
---

15 changes: 15 additions & 0 deletions .chronus/changes/feature-multipart-v2-2024-4-14-22-58-52.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: feature
packages:
- "@typespec/http"
---

Add new multipart handling. Using `@multipartBody` with `HttpPart<Type, Options>`. See [multipart docs] for more information https://typespec.io/docs/next/libraries/http/multipart

```tsp
op upload(@header contentType: "multipart/mixed", @multipartBody body: {
name: HttpPart<string>;
avatar: HttpPart<bytes>[];
}): void;
```
8 changes: 8 additions & 0 deletions .chronus/changes/feature-multipart-v2-2024-4-14-23-5-59.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: fix
packages:
- "@typespec/openapi3"
---

Add support for new multipart constructs in http library
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"**/node_modules/**": true,
"packages/compiler/templates/__snapshots__/**": true,
"packages/website/versioned_docs/**": true,
"packages/http-client-csharp/**/Generated/**": true,
"packages/samples/scratch/**": false // Those files are in gitignore but we still want to search for them
},
"files.exclude": {
Expand Down
43 changes: 43 additions & 0 deletions docs/libraries/http/reference/data-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,20 @@ model TypeSpec.Http.CreatedResponse
| ---------- | ----- | ---------------- |
| statusCode | `201` | The status code. |

### `File` {#TypeSpec.Http.File}

```typespec
model TypeSpec.Http.File
```

#### Properties

| Name | Type | Description |
| ------------ | -------- | ----------- |
| contentType? | `string` | |
| filename? | `string` | |
| contents | `bytes` | |

### `ForbiddenResponse` {#TypeSpec.Http.ForbiddenResponse}

Access is forbidden.
Expand Down Expand Up @@ -234,6 +248,35 @@ model TypeSpec.Http.HeaderOptions
| name? | `string` | Name of the header when sent over HTTP. |
| format? | `"csv" \| "multi" \| "tsv" \| "ssv" \| "pipes" \| "simple" \| "form"` | Determines the format of the array if type array is used. |

### `HttpPart` {#TypeSpec.Http.HttpPart}

```typespec
model TypeSpec.Http.HttpPart<Type, Options>
```

#### Template Parameters

| Name | Description |
| ------- | ----------- |
| Type | |
| Options | |

#### Properties

None

### `HttpPartOptions` {#TypeSpec.Http.HttpPartOptions}

```typespec
model TypeSpec.Http.HttpPartOptions
```

#### Properties

| Name | Type | Description |
| ----- | -------- | ------------------------------------------- |
| name? | `string` | Name of the part when using the array form. |

### `ImplicitFlow` {#TypeSpec.Http.ImplicitFlow}

Implicit flow
Expand Down
26 changes: 26 additions & 0 deletions docs/libraries/http/reference/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,32 @@ Specify if inapplicable metadata should be included in the payload for the given
| ----- | ----------------- | --------------------------------------------------------------- |
| value | `valueof boolean` | If true, inapplicable metadata will be included in the payload. |

### `@multipartBody` {#@TypeSpec.Http.multipartBody}

```typespec
@TypeSpec.Http.multipartBody
```

#### Target

`ModelProperty`

#### Parameters

None

#### Examples

```tsp
op upload(
@header `content-type`: "multipart/form-data",
@multipartBody body: {
fullName: HttpPart<string>;
headShots: HttpPart<Image>[];
},
): void;
```

### `@patch` {#@TypeSpec.Http.patch}

Specify the HTTP verb for the target operation to be `PATCH`.
Expand Down
4 changes: 4 additions & 0 deletions docs/libraries/http/reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ npm install --save-peer @typespec/http
- [`@head`](./decorators.md#@TypeSpec.Http.head)
- [`@header`](./decorators.md#@TypeSpec.Http.header)
- [`@includeInapplicableMetadataInPayload`](./decorators.md#@TypeSpec.Http.includeInapplicableMetadataInPayload)
- [`@multipartBody`](./decorators.md#@TypeSpec.Http.multipartBody)
- [`@patch`](./decorators.md#@TypeSpec.Http.patch)
- [`@path`](./decorators.md#@TypeSpec.Http.path)
- [`@post`](./decorators.md#@TypeSpec.Http.post)
Expand All @@ -66,8 +67,11 @@ npm install --save-peer @typespec/http
- [`ClientCredentialsFlow`](./data-types.md#TypeSpec.Http.ClientCredentialsFlow)
- [`ConflictResponse`](./data-types.md#TypeSpec.Http.ConflictResponse)
- [`CreatedResponse`](./data-types.md#TypeSpec.Http.CreatedResponse)
- [`File`](./data-types.md#TypeSpec.Http.File)
- [`ForbiddenResponse`](./data-types.md#TypeSpec.Http.ForbiddenResponse)
- [`HeaderOptions`](./data-types.md#TypeSpec.Http.HeaderOptions)
- [`HttpPart`](./data-types.md#TypeSpec.Http.HttpPart)
- [`HttpPartOptions`](./data-types.md#TypeSpec.Http.HttpPartOptions)
- [`ImplicitFlow`](./data-types.md#TypeSpec.Http.ImplicitFlow)
- [`LocationHeader`](./data-types.md#TypeSpec.Http.LocationHeader)
- [`MovedResponse`](./data-types.md#TypeSpec.Http.MovedResponse)
Expand Down
27 changes: 27 additions & 0 deletions packages/http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Available ruleSets:
- [`@head`](#@head)
- [`@header`](#@header)
- [`@includeInapplicableMetadataInPayload`](#@includeinapplicablemetadatainpayload)
- [`@multipartBody`](#@multipartbody)
- [`@patch`](#@patch)
- [`@path`](#@path)
- [`@post`](#@post)
Expand Down Expand Up @@ -272,6 +273,32 @@ Specify if inapplicable metadata should be included in the payload for the given
| ----- | ----------------- | --------------------------------------------------------------- |
| value | `valueof boolean` | If true, inapplicable metadata will be included in the payload. |

#### `@multipartBody`

```typespec
@TypeSpec.Http.multipartBody
```

##### Target

`ModelProperty`

##### Parameters

None

##### Examples

```tsp
op upload(
@header `content-type`: "multipart/form-data",
@multipartBody body: {
fullName: HttpPart<string>;
headShots: HttpPart<Image>[];
},
): void;
```
#### `@patch`

Specify the HTTP verb for the target operation to be `PATCH`.
Expand Down
11 changes: 10 additions & 1 deletion packages/http/generated-defs/TypeSpec.Http.Private.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import type { DecoratorContext, Model } from "@typespec/compiler";
import type { DecoratorContext, Model, Type } from "@typespec/compiler";

export type PlainDataDecorator = (context: DecoratorContext, target: Model) => void;

export type HttpFileDecorator = (context: DecoratorContext, target: Model) => void;

export type HttpPartDecorator = (
context: DecoratorContext,
target: Model,
type: Type,
options: unknown
) => void;
17 changes: 17 additions & 0 deletions packages/http/generated-defs/TypeSpec.Http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@ export type BodyRootDecorator = (context: DecoratorContext, target: ModelPropert
*/
export type BodyIgnoreDecorator = (context: DecoratorContext, target: ModelProperty) => void;

/**
*
*
*
* @example
* ```tsp
* op upload(
* @header `content-type`: "multipart/form-data",
* @multipartBody body: {
* fullName: HttpPart<string>,
* headShots: HttpPart<Image>[]
* }
* ): void;
* ```
*/
export type MultipartBodyDecorator = (context: DecoratorContext, target: ModelProperty) => void;

/**
* Specify the HTTP verb for the target operation to be `GET`.
*
Expand Down
4 changes: 4 additions & 0 deletions packages/http/generated-defs/TypeSpec.Http.ts-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
$head,
$header,
$includeInapplicableMetadataInPayload,
$multipartBody,
$patch,
$path,
$post,
Expand All @@ -28,6 +29,7 @@ import type {
HeadDecorator,
HeaderDecorator,
IncludeInapplicableMetadataInPayloadDecorator,
MultipartBodyDecorator,
PatchDecorator,
PathDecorator,
PostDecorator,
Expand All @@ -48,6 +50,7 @@ type Decorators = {
$path: PathDecorator;
$bodyRoot: BodyRootDecorator;
$bodyIgnore: BodyIgnoreDecorator;
$multipartBody: MultipartBodyDecorator;
$get: GetDecorator;
$put: PutDecorator;
$post: PostDecorator;
Expand All @@ -70,6 +73,7 @@ const _: Decorators = {
$path,
$bodyRoot,
$bodyIgnore,
$multipartBody,
$get,
$put,
$post,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,21 @@ extern dec bodyRoot(target: ModelProperty);
*/
extern dec bodyIgnore(target: ModelProperty);

/**
* @example
*
* ```tsp
* op upload(
* @header `content-type`: "multipart/form-data",
* @multipartBody body: {
* fullName: HttpPart<string>,
* headShots: HttpPart<Image>[]
* }
* ): void;
* ```
*/
extern dec multipartBody(target: ModelProperty);

/**
* Specify the status code for this response. Property type must be a status code integer or a union of status code integer.
*
Expand Down Expand Up @@ -296,10 +311,3 @@ extern dec route(
* ```
*/
extern dec sharedRoute(target: Operation);

/**
* Private decorators. Those are meant for internal use inside Http types only.
*/
namespace Private {
extern dec plainData(target: TypeSpec.Reflection.Model);
}
18 changes: 17 additions & 1 deletion packages/http/lib/http.tsp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "../dist/src/index.js";
import "./http-decorators.tsp";
import "./decorators.tsp";
import "./private.decorators.tsp";
import "./auth.tsp";

namespace TypeSpec.Http;
Expand Down Expand Up @@ -104,3 +105,18 @@ model ConflictResponse is Response<409>;
model PlainData<Data> {
...Data;
}

@Private.httpFile
model File {
contentType?: string;
filename?: string;
contents: bytes;
}

model HttpPartOptions {
/** Name of the part when using the array form. */
name?: string;
}

@Private.httpPart(Type, Options)
model HttpPart<Type, Options extends valueof HttpPartOptions = #{}> {}
14 changes: 14 additions & 0 deletions packages/http/lib/private.decorators.tsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import "../dist/src/private.decorators.js";

/**
* Private decorators. Those are meant for internal use inside Http types only.
*/
namespace TypeSpec.Http.Private;

extern dec plainData(target: TypeSpec.Reflection.Model);
extern dec httpFile(target: TypeSpec.Reflection.Model);
extern dec httpPart(
target: TypeSpec.Reflection.Model,
type: unknown,
options: valueof HttpPartOptions
);
Loading

0 comments on commit 40df1ec

Please sign in to comment.