From ae3eff6b297f12e55d121a62019d1d2badfda7e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:37:40 +0000 Subject: [PATCH 1/6] Initial plan From 6ae6c723f43146dd74cbe5c1b58869c494f10e02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:56:47 +0000 Subject: [PATCH 2/6] Add commaDelimited and newlineDelimited to ArrayEncoding enum Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- ...oding-comma-newline-2025-11-13-20-53-33.md | 8 +++ packages/compiler/lib/std/decorators.tsp | 30 ++++++++++- .../test/decorators/decorators.test.ts | 50 +++++++++++++++++++ packages/openapi3/src/encoding.ts | 7 ++- packages/openapi3/src/examples.ts | 6 ++- packages/openapi3/src/parameters.ts | 6 ++- 6 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 .chronus/changes/array-encoding-comma-newline-2025-11-13-20-53-33.md diff --git a/.chronus/changes/array-encoding-comma-newline-2025-11-13-20-53-33.md b/.chronus/changes/array-encoding-comma-newline-2025-11-13-20-53-33.md new file mode 100644 index 00000000000..2f6c1a3c6c7 --- /dev/null +++ b/.chronus/changes/array-encoding-comma-newline-2025-11-13-20-53-33.md @@ -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 diff --git a/packages/compiler/lib/std/decorators.tsp b/packages/compiler/lib/std/decorators.tsp index 840579fa438..0c35a010e26 100644 --- a/packages/compiler/lib/std/decorators.tsp +++ b/packages/compiler/lib/std/decorators.tsp @@ -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 */ + /** + * 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, } /** diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index 648283e2901..51dd3e7cf5a 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -795,6 +795,56 @@ describe("compiler: built-in decorators", () => { }); }); }); + + describe("ArrayEncoding enum", () => { + it("can use ArrayEncoding.pipeDelimited", async () => { + const { prop } = (await runner.compile(` + model Foo { + @encode(ArrayEncoding.pipeDelimited) + @test + prop: string[]; + } + `)) as { prop: ModelProperty }; + + strictEqual(getEncode(runner.program, prop)?.encoding, "ArrayEncoding.pipeDelimited"); + }); + + it("can use ArrayEncoding.spaceDelimited", async () => { + const { prop } = (await runner.compile(` + model Foo { + @encode(ArrayEncoding.spaceDelimited) + @test + prop: string[]; + } + `)) as { prop: ModelProperty }; + + strictEqual(getEncode(runner.program, prop)?.encoding, "ArrayEncoding.spaceDelimited"); + }); + + it("can use ArrayEncoding.commaDelimited", async () => { + const { prop } = (await runner.compile(` + model Foo { + @encode(ArrayEncoding.commaDelimited) + @test + prop: string[]; + } + `)) as { prop: ModelProperty }; + + strictEqual(getEncode(runner.program, prop)?.encoding, "ArrayEncoding.commaDelimited"); + }); + + it("can use ArrayEncoding.newlineDelimited", async () => { + const { prop } = (await runner.compile(` + model Foo { + @encode(ArrayEncoding.newlineDelimited) + @test + prop: string[]; + } + `)) as { prop: ModelProperty }; + + strictEqual(getEncode(runner.program, prop)?.encoding, "ArrayEncoding.newlineDelimited"); + }); + }); }); describe("@withoutOmittedProperties", () => { diff --git a/packages/openapi3/src/encoding.ts b/packages/openapi3/src/encoding.ts index acce72d6012..a7e9de5832d 100644 --- a/packages/openapi3/src/encoding.ts +++ b/packages/openapi3/src/encoding.ts @@ -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( diff --git a/packages/openapi3/src/examples.ts b/packages/openapi3/src/examples.ts index d39223e2722..fdc7ced5ab8 100644 --- a/packages/openapi3/src/examples.ts +++ b/packages/openapi3/src/examples.ts @@ -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"); } } @@ -518,7 +522,7 @@ function getParameterDelimitedValue( program: Program, originalValue: Value, property: Extract, - delimiter: " " | "|", + delimiter: " " | "|" | "," | "\n", ): Value | undefined { const { explode, name } = property.options; // Serialization is undefined for explode=true diff --git a/packages/openapi3/src/parameters.ts b/packages/openapi3/src/parameters.ts index d2c22a753e2..bef5d8e9f1a 100644 --- a/packages/openapi3/src/parameters.ts +++ b/packages/openapi3/src/parameters.ts @@ -3,7 +3,7 @@ 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; @@ -11,6 +11,10 @@ export function getParameterStyle( 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; } From ed42d141d165e1a7e4fd24b51ad8b3b6c3db851b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:04:00 +0000 Subject: [PATCH 3/6] Add comprehensive tests for commaDelimited and newlineDelimited Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- packages/openapi3/test/examples.test.ts | 72 +++++++++++++++++++++++ packages/openapi3/test/parameters.test.ts | 2 + 2 files changed, 74 insertions(+) diff --git a/packages/openapi3/test/examples.test.ts b/packages/openapi3/test/examples.test.ts index eb159fe0a2f..b31fc32b01d 100644 --- a/packages/openapi3/test/examples.test.ts +++ b/packages/openapi3/test/examples.test.ts @@ -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`, + 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`, + 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`, + 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`, + paramExample: `#{R: 100, G: 200, B: 150}`, + expectedExample: undefined, + }, ])("$desc", async ({ param, paramExample, expectedExample }) => { const res = await openApiFor( ` diff --git a/packages/openapi3/test/parameters.test.ts b/packages/openapi3/test/parameters.test.ts index 6ca60fd0f1d..6a861660dc5 100644 --- a/packages/openapi3/test/parameters.test.ts +++ b/packages/openapi3/test/parameters.test.ts @@ -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;`, From fbaaabdcb9582c17ab2a787bc833a14c88008af9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 22:58:39 +0000 Subject: [PATCH 4/6] Regenerate JS documentation for ArrayEncoding enum Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- .../docs/docs/standard-library/built-in-data-types.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/website/src/content/docs/docs/standard-library/built-in-data-types.md b/website/src/content/docs/docs/standard-library/built-in-data-types.md index 1fa307bd4d9..663e1bca85d 100644 --- a/website/src/content/docs/docs/standard-library/built-in-data-types.md +++ b/website/src/content/docs/docs/standard-library/built-in-data-types.md @@ -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 | +| pipeDelimited | | 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 | +| spaceDelimited | | 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 | +| commaDelimited | | 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 | +| newlineDelimited | | 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 | ### `BytesKnownEncoding` {#BytesKnownEncoding} From bac10d048792418b4dd87a5d7bf176bee20ee60a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 23:03:56 +0000 Subject: [PATCH 5/6] Fix cspell violations - add URL-encoded color names to exceptions Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- cspell.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cspell.yaml b/cspell.yaml index b555dc79a7d..3d5a5009cdb 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -13,6 +13,7 @@ words: - AQID - Arize - arizeaiobservabilityeval + - Ablack - arraya - astimezone - astro @@ -35,6 +36,8 @@ words: - cadl - cadleditor - cadleng + - Cblack + - Cbrown - cadlplayground - canonicalizer - clsx From 20f3be2026ac1817330e46077ee5a2619fe53f2c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:34:49 +0000 Subject: [PATCH 6/6] Update ArrayEncoding tests to use new Tester pattern Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../test/decorators/decorators.test.ts | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index 51dd3e7cf5a..111972c7e49 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -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; @@ -798,51 +800,47 @@ describe("compiler: built-in decorators", () => { describe("ArrayEncoding enum", () => { it("can use ArrayEncoding.pipeDelimited", async () => { - const { prop } = (await runner.compile(` + const { prop, program } = await Tester.compile(t.code` model Foo { @encode(ArrayEncoding.pipeDelimited) - @test - prop: string[]; + ${t.modelProperty("prop")}: string[]; } - `)) as { prop: ModelProperty }; + `); - strictEqual(getEncode(runner.program, prop)?.encoding, "ArrayEncoding.pipeDelimited"); + strictEqual(getEncode(program, prop)?.encoding, "ArrayEncoding.pipeDelimited"); }); it("can use ArrayEncoding.spaceDelimited", async () => { - const { prop } = (await runner.compile(` + const { prop, program } = await Tester.compile(t.code` model Foo { @encode(ArrayEncoding.spaceDelimited) - @test - prop: string[]; + ${t.modelProperty("prop")}: string[]; } - `)) as { prop: ModelProperty }; + `); - strictEqual(getEncode(runner.program, prop)?.encoding, "ArrayEncoding.spaceDelimited"); + strictEqual(getEncode(program, prop)?.encoding, "ArrayEncoding.spaceDelimited"); }); it("can use ArrayEncoding.commaDelimited", async () => { - const { prop } = (await runner.compile(` + const { prop, program } = await Tester.compile(t.code` model Foo { @encode(ArrayEncoding.commaDelimited) - @test - prop: string[]; + ${t.modelProperty("prop")}: string[]; } - `)) as { prop: ModelProperty }; + `); - strictEqual(getEncode(runner.program, prop)?.encoding, "ArrayEncoding.commaDelimited"); + strictEqual(getEncode(program, prop)?.encoding, "ArrayEncoding.commaDelimited"); }); it("can use ArrayEncoding.newlineDelimited", async () => { - const { prop } = (await runner.compile(` + const { prop, program } = await Tester.compile(t.code` model Foo { @encode(ArrayEncoding.newlineDelimited) - @test - prop: string[]; + ${t.modelProperty("prop")}: string[]; } - `)) as { prop: ModelProperty }; + `); - strictEqual(getEncode(runner.program, prop)?.encoding, "ArrayEncoding.newlineDelimited"); + strictEqual(getEncode(program, prop)?.encoding, "ArrayEncoding.newlineDelimited"); }); }); });