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 {
/** 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");
});

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