Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
- "@typespec/openapi3"
---

Add `commaDelimited` and `newlineDelimited` values to `ArrayEncoding` enum for serializing arrays with comma and newline delimiters
3 changes: 3 additions & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ words:
- AQID
- Arize
- arizeaiobservabilityeval
- Ablack
- arraya
- astimezone
- astro
Expand All @@ -35,6 +36,8 @@ words:
- cadl
- cadleditor
- cadleng
- Cblack
- Cbrown
- cadlplayground
- canonicalizer
- clsx
Expand Down
30 changes: 28 additions & 2 deletions packages/compiler/lib/std/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -531,11 +531,37 @@ enum BytesKnownEncoding {
* Encoding for serializing arrays
*/
enum ArrayEncoding {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayEncoding is not used in the encode decorator behavior code. Should we add lint to only allow it to be set on array type? @timotheeguerin

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hhm yeah could do that, can you file an issue

/** Each values of the array is separated by a | */
/**
* Each value of the array is separated by a pipe character (|).
* Values can only contain | if the underlying protocol supports encoding them.
* - json -> error
* - http -> %7C
*/
pipeDelimited,

/** Each values of the array is separated by a <space> */
/**
* Each value of the array is separated by a space character.
* Values can only contain spaces if the underlying protocol supports encoding them.
* - json -> error
* - http -> %20
*/
spaceDelimited,

/**
* Each value of the array is separated by a comma (,).
* Values can only contain commas if the underlying protocol supports encoding them.
* - json -> error
* - http -> %2C
*/
commaDelimited,

/**
* Each value of the array is separated by a newline character (\n).
* Values can only contain newlines if the underlying protocol supports encoding them.
* - json -> error
* - http -> %0A
*/
newlineDelimited,
}

/**
Expand Down
48 changes: 48 additions & 0 deletions packages/compiler/test/decorators/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import {
createTestRunner,
expectDiagnosticEmpty,
expectDiagnostics,
t,
} from "../../src/testing/index.js";
import { Tester } from "../tester.js";

describe("compiler: built-in decorators", () => {
let runner: BasicTestRunner;
Expand Down Expand Up @@ -795,6 +797,52 @@ describe("compiler: built-in decorators", () => {
});
});
});

describe("ArrayEncoding enum", () => {
it("can use ArrayEncoding.pipeDelimited", async () => {
const { prop, program } = await Tester.compile(t.code`
model Foo {
@encode(ArrayEncoding.pipeDelimited)
${t.modelProperty("prop")}: string[];
}
`);

strictEqual(getEncode(program, prop)?.encoding, "ArrayEncoding.pipeDelimited");
});

it("can use ArrayEncoding.spaceDelimited", async () => {
const { prop, program } = await Tester.compile(t.code`
model Foo {
@encode(ArrayEncoding.spaceDelimited)
${t.modelProperty("prop")}: string[];
}
`);

strictEqual(getEncode(program, prop)?.encoding, "ArrayEncoding.spaceDelimited");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array encoding's usage is different from bytes/datetime/duration. It uses the TypeSpec enum value name instead a defined type in ts lib.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes this is expected, same as xml libraries. Using the fqn allows it to make sure it doesn't conflict with anything else. We couldn'tchange the other one for back compat

});

it("can use ArrayEncoding.commaDelimited", async () => {
const { prop, program } = await Tester.compile(t.code`
model Foo {
@encode(ArrayEncoding.commaDelimited)
${t.modelProperty("prop")}: string[];
}
`);

strictEqual(getEncode(program, prop)?.encoding, "ArrayEncoding.commaDelimited");
});

it("can use ArrayEncoding.newlineDelimited", async () => {
const { prop, program } = await Tester.compile(t.code`
model Foo {
@encode(ArrayEncoding.newlineDelimited)
${t.modelProperty("prop")}: string[];
}
`);

strictEqual(getEncode(program, prop)?.encoding, "ArrayEncoding.newlineDelimited");
});
});
});

describe("@withoutOmittedProperties", () => {
Expand Down
7 changes: 6 additions & 1 deletion packages/openapi3/src/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import type { OpenAPI3Schema, OpenAPISchema3_1 } from "./types.js";

function isParameterStyleEncoding(encoding: string | undefined): boolean {
if (!encoding) return false;
return ["ArrayEncoding.pipeDelimited", "ArrayEncoding.spaceDelimited"].includes(encoding);
return [
"ArrayEncoding.pipeDelimited",
"ArrayEncoding.spaceDelimited",
"ArrayEncoding.commaDelimited",
"ArrayEncoding.newlineDelimited",
].includes(encoding);
}

export function applyEncoding(
Expand Down
6 changes: 5 additions & 1 deletion packages/openapi3/src/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@ function getQueryParameterValue(
return getParameterDelimitedValue(program, originalValue, property, " ");
case "pipeDelimited":
return getParameterDelimitedValue(program, originalValue, property, "|");
case "commaDelimited":
return getParameterDelimitedValue(program, originalValue, property, ",");
case "newlineDelimited":
return getParameterDelimitedValue(program, originalValue, property, "\n");
}
}

Expand Down Expand Up @@ -518,7 +522,7 @@ function getParameterDelimitedValue(
program: Program,
originalValue: Value,
property: Extract<HttpParameterProperties, { kind: "query" }>,
delimiter: " " | "|",
delimiter: " " | "|" | "," | "\n",
): Value | undefined {
const { explode, name } = property.options;
// Serialization is undefined for explode=true
Expand Down
6 changes: 5 additions & 1 deletion packages/openapi3/src/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ import { getEncode, ModelProperty, Program } from "@typespec/compiler";
export function getParameterStyle(
program: Program,
type: ModelProperty,
): "pipeDelimited" | "spaceDelimited" | undefined {
): "pipeDelimited" | "spaceDelimited" | "commaDelimited" | "newlineDelimited" | undefined {
const encode = getEncode(program, type);
if (!encode) return;

if (encode.encoding === "ArrayEncoding.pipeDelimited") {
return "pipeDelimited";
} else if (encode.encoding === "ArrayEncoding.spaceDelimited") {
return "spaceDelimited";
} else if (encode.encoding === "ArrayEncoding.commaDelimited") {
return "commaDelimited";
} else if (encode.encoding === "ArrayEncoding.newlineDelimited") {
return "newlineDelimited";
}
return;
}
72 changes: 72 additions & 0 deletions packages/openapi3/test/examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,78 @@ worksFor(supportedVersions, ({ openApiFor }) => {
paramExample: `#{R: 100, G: 200, B: 150}`,
expectedExample: undefined,
},
{
desc: "commaDelimited (undefined)",
param: `@query @encode(ArrayEncoding.commaDelimited) color: string | null`,
paramExample: `null`,
expectedExample: undefined,
},
{
desc: "commaDelimited (string)",
param: `@query @encode(ArrayEncoding.commaDelimited) color: string`,
paramExample: `"blue"`,
expectedExample: undefined,
},
{
desc: "commaDelimited (array) explode: false",
param: `@query @encode(ArrayEncoding.commaDelimited) color: string[]`,
paramExample: `#["blue", "black", "brown"]`,
expectedExample: "color=blue%2Cblack%2Cbrown",
},
{
desc: "commaDelimited (array) explode: true",
param: `@query(#{ explode: true }) @encode(ArrayEncoding.commaDelimited) color: string[]`,
paramExample: `#["blue", "black", "brown"]`,
expectedExample: undefined,
},
{
desc: "commaDelimited (object) explode: false",
param: `@query @encode(ArrayEncoding.commaDelimited) color: Record<int32>`,
paramExample: `#{R: 100, G: 200, B: 150}`,
expectedExample: "color=R%2C100%2CG%2C200%2CB%2C150",
},
{
desc: "commaDelimited (object) explode: true",
param: `@query(#{ explode: true }) @encode(ArrayEncoding.commaDelimited) color: Record<int32>`,
paramExample: `#{R: 100, G: 200, B: 150}`,
expectedExample: undefined,
},
{
desc: "newlineDelimited (undefined)",
param: `@query @encode(ArrayEncoding.newlineDelimited) color: string | null`,
paramExample: `null`,
expectedExample: undefined,
},
{
desc: "newlineDelimited (string)",
param: `@query @encode(ArrayEncoding.newlineDelimited) color: string`,
paramExample: `"blue"`,
expectedExample: undefined,
},
{
desc: "newlineDelimited (array) explode: false",
param: `@query @encode(ArrayEncoding.newlineDelimited) color: string[]`,
paramExample: `#["blue", "black", "brown"]`,
expectedExample: "color=blue%0Ablack%0Abrown",
},
{
desc: "newlineDelimited (array) explode: true",
param: `@query(#{ explode: true }) @encode(ArrayEncoding.newlineDelimited) color: string[]`,
paramExample: `#["blue", "black", "brown"]`,
expectedExample: undefined,
},
{
desc: "newlineDelimited (object) explode: false",
param: `@query @encode(ArrayEncoding.newlineDelimited) color: Record<int32>`,
paramExample: `#{R: 100, G: 200, B: 150}`,
expectedExample: "color=R%0A100%0AG%0A200%0AB%0A150",
},
{
desc: "newlineDelimited (object) explode: true",
param: `@query(#{ explode: true }) @encode(ArrayEncoding.newlineDelimited) color: Record<int32>`,
paramExample: `#{R: 100, G: 200, B: 150}`,
expectedExample: undefined,
},
])("$desc", async ({ param, paramExample, expectedExample }) => {
const res = await openApiFor(
`
Expand Down
2 changes: 2 additions & 0 deletions packages/openapi3/test/parameters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ worksFor(supportedVersions, ({ diagnoseOpenApiFor, openApiFor }) => {
it.each([
{ encoding: "ArrayEncoding.pipeDelimited", style: "pipeDelimited" },
{ encoding: "ArrayEncoding.spaceDelimited", style: "spaceDelimited" },
{ encoding: "ArrayEncoding.commaDelimited", style: "commaDelimited" },
{ encoding: "ArrayEncoding.newlineDelimited", style: "newlineDelimited" },
])("can set style to $style with @encode($encoding)", async ({ encoding, style }) => {
const param = await getQueryParam(
`op test(@query @encode(${encoding}) myParam: string[]): void;`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,10 @@ 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> |
| pipeDelimited | | Each value of the array is separated by a pipe character (\|).<br />Values can only contain \| if the underlying protocol supports encoding them.<br />- json -> error<br />- http -> %7C |
| spaceDelimited | | Each value of the array is separated by a space character.<br />Values can only contain spaces if the underlying protocol supports encoding them.<br />- json -> error<br />- http -> %20 |
| commaDelimited | | Each value of the array is separated by a comma (,).<br />Values can only contain commas if the underlying protocol supports encoding them.<br />- json -> error<br />- http -> %2C |
| newlineDelimited | | Each value of the array is separated by a newline character (\n).<br />Values can only contain newlines if the underlying protocol supports encoding them.<br />- json -> error<br />- http -> %0A |


### `BytesKnownEncoding` {#BytesKnownEncoding}
Expand Down
Loading