From 0fb96cb139c7a17801774f2b873a2cf9af223913 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:26:01 +0000 Subject: [PATCH 01/16] Initial plan From 26d634df6b8d2f23a977e37e78abcd02df80a209 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:34:48 +0000 Subject: [PATCH 02/16] feat(compiler): allow @encode(string) on boolean Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/generated-defs/TypeSpec.ts | 12 +++++++++++- packages/compiler/lib/std/decorators.tsp | 12 +++++++++++- packages/compiler/src/core/messages.ts | 2 +- packages/compiler/src/lib/decorators.ts | 6 ++++-- .../compiler/test/decorators/decorators.test.ts | 16 +++++++++++++++- .../docs/standard-library/built-in-decorators.md | 13 +++++++++++-- 6 files changed, 53 insertions(+), 8 deletions(-) diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index 2f066713ac6..66b1f3e984d 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -77,7 +77,7 @@ export type MediaTypeHintDecorator = ( /** * 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). + * @param encodingOrEncodeAs Known name of an encoding or a scalar type to encode as(Only for numeric and boolean types to encode as string). * @param encodedAs What target type is this being encoded as. Default to string. * @example offsetDateTime encoded with rfc7231 * @@ -98,6 +98,16 @@ export type MediaTypeHintDecorator = ( * @encode(string) id: int64; * } * ``` + * + * @example encode boolean type to string + * + * `@encode(string)` on boolean uses only `true` or `false`. Parsing is case-insensitive for those two values only. + * + * ```tsp + * model FeatureFlags { + * @encode(string) enabled: boolean; + * } + * ``` */ export type EncodeDecorator = ( context: DecoratorContext, diff --git a/packages/compiler/lib/std/decorators.tsp b/packages/compiler/lib/std/decorators.tsp index 0c35a010e26..dd99b403dd9 100644 --- a/packages/compiler/lib/std/decorators.tsp +++ b/packages/compiler/lib/std/decorators.tsp @@ -566,7 +566,7 @@ enum ArrayEncoding { /** * 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). + * @param encodingOrEncodeAs Known name of an encoding or a scalar type to encode as(Only for numeric and boolean types to encode as string). * @param encodedAs What target type is this being encoded as. Default to string. * * @example offsetDateTime encoded with rfc7231 @@ -590,6 +590,16 @@ enum ArrayEncoding { * @encode(string) id: int64; * } * ``` + * + * @example encode boolean type to string + * + * `@encode(string)` on boolean uses only `true` or `false`. Parsing is case-insensitive for those two values only. + * + * ```tsp + * model FeatureFlags { + * @encode(string) enabled: boolean; + * } + * ``` */ extern dec encode( target: Scalar | ModelProperty, diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 9008e0f6be6..11d8ea2c29c 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -942,7 +942,7 @@ const diagnostics = { wrongType: paramMessage`Encoding '${"encoding"}' cannot be used on type '${"type"}'. Expected: ${"expected"}.`, wrongEncodingType: paramMessage`Encoding '${"encoding"}' on type '${"type"}' is expected to be serialized as '${"expected"}' but got '${"actual"}'.`, wrongNumericEncodingType: paramMessage`Encoding '${"encoding"}' on type '${"type"}' is expected to be serialized as '${"expected"}' but got '${"actual"}'. Set '@encode' 2nd parameter to be of type ${"expected"}. e.g. '@encode("${"encoding"}", int32)'`, - firstArg: `First argument of "@encode" must be the encoding name or the string type when encoding numeric types.`, + firstArg: `First argument of "@encode" must be the encoding name or the string type when encoding numeric or boolean types.`, }, }, diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index 3290a9dec74..18bb45e365a 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -873,7 +873,9 @@ export type BytesKnownEncoding = "base64" | "base64url"; export interface EncodeData { /** * Known encoding key. - * Can be undefined when `@encode(string)` is used on a numeric type. In that case it just means using the base10 decimal representation of the number. + * Can be undefined when `@encode(string)` is used on a numeric or boolean type. + * For numeric this means using the base10 decimal representation of the number. + * For boolean this means using `true` or `false`. */ encoding?: DateTimeKnownEncoding | DurationKnownEncoding | BytesKnownEncoding | string; type: Scalar; @@ -995,7 +997,7 @@ function validateEncodeData(context: DecoratorContext, target: Type, encodeData: case "base64url": return check(["bytes"], ["string"]); case undefined: - return check(["numeric"], ["string"]); + return check(["numeric", "boolean"], ["string"]); } } diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index 5a1ec399d76..1c46022acb0 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -665,6 +665,20 @@ describe("compiler: built-in decorators", () => { strictEqual(encodeData.encoding, undefined); strictEqual(encodeData.type.name, "string"); }); + + it(`@encode(string) on boolean model property`, async () => { + const { prop, program } = await Tester.compile(t.code` + model Foo { + @encode(string) + ${t.modelProperty("prop")}: boolean; + } + `); + + const encodeData = getEncode(program, prop); + ok(encodeData); + strictEqual(encodeData.encoding, undefined); + strictEqual(encodeData.type.name, "string"); + }); }); describe("invalid", () => { invalidCases.forEach(([target, encoding, encodeAs, expectedCode, expectedMessage]) => { @@ -693,7 +707,7 @@ describe("compiler: built-in decorators", () => { expectDiagnostics(diagnostics, { code: "invalid-encode", severity: "error", - message: "Encoding 'string' cannot be used on type 's'. Expected: numeric.", + message: "Encoding 'string' cannot be used on type 's'. Expected: numeric, boolean.", }); }); }); diff --git a/website/src/content/docs/docs/standard-library/built-in-decorators.md b/website/src/content/docs/docs/standard-library/built-in-decorators.md index e38804062e6..c60e5132e26 100644 --- a/website/src/content/docs/docs/standard-library/built-in-decorators.md +++ b/website/src/content/docs/docs/standard-library/built-in-decorators.md @@ -192,7 +192,7 @@ Specify how to encode the target type. #### Parameters | Name | Type | Description | |------|------|-------------| -| encodingOrEncodeAs | `Scalar` \| `valueof string \| EnumMember` | Known name of an encoding or a scalar type to encode as(Only for numeric types to encode as string). | +| encodingOrEncodeAs | `Scalar` \| `valueof string \| EnumMember` | Known name of an encoding or a scalar type to encode as(Only for numeric and boolean types to encode as string). | | encodedAs | `Scalar` | What target type is this being encoded as. Default to string. | #### Examples @@ -221,6 +221,16 @@ model Pet { } ``` +##### encode boolean type to string + +`@encode(string)` on boolean uses only `true` or `false`. Parsing is case-insensitive for those two values only. + +```tsp +model FeatureFlags { + @encode(string) enabled: boolean; +} +``` + ### `@encodedName` {#@encodedName} @@ -1548,4 +1558,3 @@ model DogRead { ...Dog } ``` - From dbd1269d0e577c11abae06642a1e3fb88ffc338c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:47:38 +0000 Subject: [PATCH 03/16] docs: simplify boolean @encode(string) wording Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../content/docs/docs/standard-library/built-in-decorators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/content/docs/docs/standard-library/built-in-decorators.md b/website/src/content/docs/docs/standard-library/built-in-decorators.md index c60e5132e26..8e5054ce83f 100644 --- a/website/src/content/docs/docs/standard-library/built-in-decorators.md +++ b/website/src/content/docs/docs/standard-library/built-in-decorators.md @@ -223,7 +223,7 @@ model Pet { ##### encode boolean type to string -`@encode(string)` on boolean uses only `true` or `false`. Parsing is case-insensitive for those two values only. +`@encode(string)` on boolean uses only case-insensitive `true` or `false` values. ```tsp model FeatureFlags { From 8237396d75a96090440fc44678b71db77cfffe5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:04:08 +0000 Subject: [PATCH 04/16] test(http-specs): add boolean @encode(string) spector scenario Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/spec-summary.md | 21 ++++++++++++++++ .../http-specs/specs/encode/numeric/main.tsp | 25 +++++++++++++++++++ .../specs/encode/numeric/mockapi.ts | 4 +++ 3 files changed, 50 insertions(+) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index c623a91cc95..510a7ce5b81 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -1310,6 +1310,27 @@ Expected query parameter `input=120` Test iso8601 encode for a duration parameter. Expected query parameter `input=P40D` +### Encode_Numeric_Property_boolAsString + +- Endpoint: `post /encode/numeric/property/bool` + +Test operation with request and response model contains property of boolean type with string encode. +Expected request body: + +```json +{ + "value": "true" +} +``` + +Expected response body: + +```json +{ + "value": "true" +} +``` + ### Encode_Numeric_Property_safeintAsString - Endpoint: `post /encode/numeric/property/safeint` diff --git a/packages/http-specs/specs/encode/numeric/main.tsp b/packages/http-specs/specs/encode/numeric/main.tsp index 5677d2c673c..d0fe45bdf57 100644 --- a/packages/http-specs/specs/encode/numeric/main.tsp +++ b/packages/http-specs/specs/encode/numeric/main.tsp @@ -40,6 +40,31 @@ namespace Property { value: uint8; } + @route("/bool") + @scenario + @scenarioDoc(""" + Test operation with request and response model contains property of boolean type with string encode. + Expected request body: + ```json + { + "value": "true" + } + ``` + Expected response body: + ```json + { + "value": "true" + } + ``` + """) + @post + op boolAsString(@body value: BoolAsStringProperty): BoolAsStringProperty; + + model BoolAsStringProperty { + @encode(string) + value: boolean; + } + interface SendIntAsString { @scenario @scenarioDoc( diff --git a/packages/http-specs/specs/encode/numeric/mockapi.ts b/packages/http-specs/specs/encode/numeric/mockapi.ts index fe5feaaed25..d18f042211e 100644 --- a/packages/http-specs/specs/encode/numeric/mockapi.ts +++ b/packages/http-specs/specs/encode/numeric/mockapi.ts @@ -30,3 +30,7 @@ Scenarios.Encode_Numeric_Property_uint8AsString = createTests( "/encode/numeric/property/uint8", "255", ); +Scenarios.Encode_Numeric_Property_boolAsString = createTests( + "/encode/numeric/property/bool", + "true", +); From ab2b2d1fb7e22a7352faabbcddc6bb9460dc55da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:05:26 +0000 Subject: [PATCH 05/16] test(http-specs): refine boolean encode scenario naming Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/spec-summary.md | 4 ++-- packages/http-specs/specs/encode/numeric/main.tsp | 4 ++-- packages/http-specs/specs/encode/numeric/mockapi.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 510a7ce5b81..048b1fb86ce 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -1312,9 +1312,9 @@ Expected query parameter `input=P40D` ### Encode_Numeric_Property_boolAsString -- Endpoint: `post /encode/numeric/property/bool` +- Endpoint: `post /encode/numeric/property/boolean` -Test operation with request and response model contains property of boolean type with string encode. +Test operation with request and response model containing a property of boolean type with string encode. Expected request body: ```json diff --git a/packages/http-specs/specs/encode/numeric/main.tsp b/packages/http-specs/specs/encode/numeric/main.tsp index d0fe45bdf57..446ff4da661 100644 --- a/packages/http-specs/specs/encode/numeric/main.tsp +++ b/packages/http-specs/specs/encode/numeric/main.tsp @@ -40,10 +40,10 @@ namespace Property { value: uint8; } - @route("/bool") + @route("/boolean") @scenario @scenarioDoc(""" - Test operation with request and response model contains property of boolean type with string encode. + Test operation with request and response model containing a property of boolean type with string encode. Expected request body: ```json { diff --git a/packages/http-specs/specs/encode/numeric/mockapi.ts b/packages/http-specs/specs/encode/numeric/mockapi.ts index d18f042211e..38818f6deb9 100644 --- a/packages/http-specs/specs/encode/numeric/mockapi.ts +++ b/packages/http-specs/specs/encode/numeric/mockapi.ts @@ -31,6 +31,6 @@ Scenarios.Encode_Numeric_Property_uint8AsString = createTests( "255", ); Scenarios.Encode_Numeric_Property_boolAsString = createTests( - "/encode/numeric/property/bool", + "/encode/numeric/property/boolean", "true", ); From 2e01d673e2b00610b91271b12da96ca92cc758d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:06:25 +0000 Subject: [PATCH 06/16] test(http-specs): clarify boolean string encode route Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/spec-summary.md | 2 +- packages/http-specs/specs/encode/numeric/main.tsp | 2 +- packages/http-specs/specs/encode/numeric/mockapi.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 048b1fb86ce..66ebdccfb9e 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -1312,7 +1312,7 @@ Expected query parameter `input=P40D` ### Encode_Numeric_Property_boolAsString -- Endpoint: `post /encode/numeric/property/boolean` +- Endpoint: `post /encode/numeric/property/bool-as-string` Test operation with request and response model containing a property of boolean type with string encode. Expected request body: diff --git a/packages/http-specs/specs/encode/numeric/main.tsp b/packages/http-specs/specs/encode/numeric/main.tsp index 446ff4da661..c39e35486a1 100644 --- a/packages/http-specs/specs/encode/numeric/main.tsp +++ b/packages/http-specs/specs/encode/numeric/main.tsp @@ -40,7 +40,7 @@ namespace Property { value: uint8; } - @route("/boolean") + @route("/bool-as-string") @scenario @scenarioDoc(""" Test operation with request and response model containing a property of boolean type with string encode. diff --git a/packages/http-specs/specs/encode/numeric/mockapi.ts b/packages/http-specs/specs/encode/numeric/mockapi.ts index 38818f6deb9..8de224d65d3 100644 --- a/packages/http-specs/specs/encode/numeric/mockapi.ts +++ b/packages/http-specs/specs/encode/numeric/mockapi.ts @@ -31,6 +31,6 @@ Scenarios.Encode_Numeric_Property_uint8AsString = createTests( "255", ); Scenarios.Encode_Numeric_Property_boolAsString = createTests( - "/encode/numeric/property/boolean", + "/encode/numeric/property/bool-as-string", "true", ); From 1f88780b529d1d3fca0cdf16366b03e3bd9036ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:07:59 +0000 Subject: [PATCH 07/16] test(http-specs): move boolean encode scenario to encode/boolean Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/spec-summary.md | 42 +++++++++---------- .../http-specs/specs/encode/boolean/main.tsp | 37 ++++++++++++++++ .../specs/encode/boolean/mockapi.ts | 20 +++++++++ .../http-specs/specs/encode/numeric/main.tsp | 25 ----------- .../specs/encode/numeric/mockapi.ts | 4 -- 5 files changed, 78 insertions(+), 50 deletions(-) create mode 100644 packages/http-specs/specs/encode/boolean/main.tsp create mode 100644 packages/http-specs/specs/encode/boolean/mockapi.ts diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 66ebdccfb9e..1541210fdfa 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -369,6 +369,27 @@ Expected response body: } ``` +### Encode_Boolean_Property_boolAsString + +- Endpoint: `post /encode/boolean/property/bool-as-string` + +Test operation with request and response model containing a property of boolean type with string encode. +Expected request body: + +```json +{ + "value": "true" +} +``` + +Expected response body: + +```json +{ + "value": "true" +} +``` + ### Encode_Bytes_Header_base64 - Endpoint: `get /encode/bytes/header/base64` @@ -1310,27 +1331,6 @@ Expected query parameter `input=120` Test iso8601 encode for a duration parameter. Expected query parameter `input=P40D` -### Encode_Numeric_Property_boolAsString - -- Endpoint: `post /encode/numeric/property/bool-as-string` - -Test operation with request and response model containing a property of boolean type with string encode. -Expected request body: - -```json -{ - "value": "true" -} -``` - -Expected response body: - -```json -{ - "value": "true" -} -``` - ### Encode_Numeric_Property_safeintAsString - Endpoint: `post /encode/numeric/property/safeint` diff --git a/packages/http-specs/specs/encode/boolean/main.tsp b/packages/http-specs/specs/encode/boolean/main.tsp new file mode 100644 index 00000000000..72ae77b8e94 --- /dev/null +++ b/packages/http-specs/specs/encode/boolean/main.tsp @@ -0,0 +1,37 @@ +import "@typespec/http"; +import "@typespec/spector"; + +using Http; +using Spector; + +@doc("Test for encode decorator on boolean.") +@scenarioService("/encode/boolean") +namespace Encode.Boolean; + +@route("/property") +namespace Property { + @route("/bool-as-string") + @scenario + @scenarioDoc(""" + Test operation with request and response model containing a property of boolean type with string encode. + Expected request body: + ```json + { + "value": "true" + } + ``` + Expected response body: + ```json + { + "value": "true" + } + ``` + """) + @post + op boolAsString(@body value: BoolAsStringProperty): BoolAsStringProperty; + + model BoolAsStringProperty { + @encode(string) + value: boolean; + } +} diff --git a/packages/http-specs/specs/encode/boolean/mockapi.ts b/packages/http-specs/specs/encode/boolean/mockapi.ts new file mode 100644 index 00000000000..89a41d5d23e --- /dev/null +++ b/packages/http-specs/specs/encode/boolean/mockapi.ts @@ -0,0 +1,20 @@ +import { json, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api"; + +export const Scenarios: Record = {}; + +Scenarios.Encode_Boolean_Property_boolAsString = passOnSuccess({ + uri: "/encode/boolean/property/bool-as-string", + method: "post", + request: { + body: json({ + value: "true", + }), + }, + response: { + status: 200, + body: json({ + value: "true", + }), + }, + kind: "MockApiDefinition", +}); diff --git a/packages/http-specs/specs/encode/numeric/main.tsp b/packages/http-specs/specs/encode/numeric/main.tsp index c39e35486a1..5677d2c673c 100644 --- a/packages/http-specs/specs/encode/numeric/main.tsp +++ b/packages/http-specs/specs/encode/numeric/main.tsp @@ -40,31 +40,6 @@ namespace Property { value: uint8; } - @route("/bool-as-string") - @scenario - @scenarioDoc(""" - Test operation with request and response model containing a property of boolean type with string encode. - Expected request body: - ```json - { - "value": "true" - } - ``` - Expected response body: - ```json - { - "value": "true" - } - ``` - """) - @post - op boolAsString(@body value: BoolAsStringProperty): BoolAsStringProperty; - - model BoolAsStringProperty { - @encode(string) - value: boolean; - } - interface SendIntAsString { @scenario @scenarioDoc( diff --git a/packages/http-specs/specs/encode/numeric/mockapi.ts b/packages/http-specs/specs/encode/numeric/mockapi.ts index 8de224d65d3..fe5feaaed25 100644 --- a/packages/http-specs/specs/encode/numeric/mockapi.ts +++ b/packages/http-specs/specs/encode/numeric/mockapi.ts @@ -30,7 +30,3 @@ Scenarios.Encode_Numeric_Property_uint8AsString = createTests( "/encode/numeric/property/uint8", "255", ); -Scenarios.Encode_Numeric_Property_boolAsString = createTests( - "/encode/numeric/property/bool-as-string", - "true", -); From d0152901776a4e3b42aa4eba08423a4d3faebb1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:17:00 +0000 Subject: [PATCH 08/16] Address encode boolean docs and scenario feedback Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/generated-defs/TypeSpec.ts | 2 +- packages/compiler/lib/std/decorators.tsp | 2 +- packages/http-specs/spec-summary.md | 67 ++++++++++++++++++- .../http-specs/specs/encode/boolean/main.tsp | 64 ++++++++++++------ .../specs/encode/boolean/mockapi.ts | 51 +++++++++----- .../standard-library/built-in-decorators.md | 2 +- 6 files changed, 147 insertions(+), 41 deletions(-) diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index 66b1f3e984d..d93788afbeb 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -101,7 +101,7 @@ export type MediaTypeHintDecorator = ( * * @example encode boolean type to string * - * `@encode(string)` on boolean uses only `true` or `false`. Parsing is case-insensitive for those two values only. + * `@encode(string)` on boolean uses case-insensitive `true` / `false` values. * * ```tsp * model FeatureFlags { diff --git a/packages/compiler/lib/std/decorators.tsp b/packages/compiler/lib/std/decorators.tsp index dd99b403dd9..fed9f685237 100644 --- a/packages/compiler/lib/std/decorators.tsp +++ b/packages/compiler/lib/std/decorators.tsp @@ -593,7 +593,7 @@ enum ArrayEncoding { * * @example encode boolean type to string * - * `@encode(string)` on boolean uses only `true` or `false`. Parsing is case-insensitive for those two values only. + * `@encode(string)` on boolean uses case-insensitive `true` / `false` values. * * ```tsp * model FeatureFlags { diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 1541210fdfa..17e173563c2 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -369,9 +369,51 @@ Expected response body: } ``` -### Encode_Boolean_Property_boolAsString +### Encode_Boolean_Property_falseLower -- Endpoint: `post /encode/boolean/property/bool-as-string` +- Endpoint: `post /encode/boolean/property/false-lower` + +Test operation with request and response model containing a property of boolean type with string encode. +Expected request body: + +```json +{ + "value": "false" +} +``` + +Expected response body: + +```json +{ + "value": "false" +} +``` + +### Encode_Boolean_Property_falseMixed + +- Endpoint: `post /encode/boolean/property/false-mixed` + +Test operation with request and response model containing a property of boolean type with string encode. +Expected request body: + +```json +{ + "value": "FaLsE" +} +``` + +Expected response body: + +```json +{ + "value": "FaLsE" +} +``` + +### Encode_Boolean_Property_trueLower + +- Endpoint: `post /encode/boolean/property/true-lower` Test operation with request and response model containing a property of boolean type with string encode. Expected request body: @@ -390,6 +432,27 @@ Expected response body: } ``` +### Encode_Boolean_Property_trueUpper + +- Endpoint: `post /encode/boolean/property/true-upper` + +Test operation with request and response model containing a property of boolean type with string encode. +Expected request body: + +```json +{ + "value": "TRUE" +} +``` + +Expected response body: + +```json +{ + "value": "TRUE" +} +``` + ### Encode_Bytes_Header_base64 - Endpoint: `get /encode/bytes/header/base64` diff --git a/packages/http-specs/specs/encode/boolean/main.tsp b/packages/http-specs/specs/encode/boolean/main.tsp index 72ae77b8e94..0a4771271cd 100644 --- a/packages/http-specs/specs/encode/boolean/main.tsp +++ b/packages/http-specs/specs/encode/boolean/main.tsp @@ -10,28 +10,52 @@ namespace Encode.Boolean; @route("/property") namespace Property { - @route("/bool-as-string") - @scenario - @scenarioDoc(""" - Test operation with request and response model containing a property of boolean type with string encode. - Expected request body: - ```json - { - "value": "true" - } - ``` - Expected response body: - ```json - { - "value": "true" - } - ``` - """) - @post - op boolAsString(@body value: BoolAsStringProperty): BoolAsStringProperty; - model BoolAsStringProperty { @encode(string) value: boolean; } + + alias SendBoolTrueLower = SendBoolAsString<"true">; + + @route("/true-lower") + op trueLower is SendBoolTrueLower.sendBoolAsString; + + alias SendBoolFalseLower = SendBoolAsString<"false">; + + @route("/false-lower") + op falseLower is SendBoolFalseLower.sendBoolAsString; + + alias SendBoolTrueUpper = SendBoolAsString<"TRUE">; + + @route("/true-upper") + op trueUpper is SendBoolTrueUpper.sendBoolAsString; + + alias SendBoolFalseMixed = SendBoolAsString<"FaLsE">; + + @route("/false-mixed") + op falseMixed is SendBoolFalseMixed.sendBoolAsString; + + interface SendBoolAsString { + @scenario + @scenarioDoc( + """ + Test operation with request and response model containing a property of boolean type with string encode. + Expected request body: + ```json + { + "value": "{value}" + } + ``` + Expected response body: + ```json + { + "value": "{value}" + } + ``` + """, + { value: StringValue } + ) + @post + sendBoolAsString(@body value: BoolAsStringProperty): BoolAsStringProperty; + } } diff --git a/packages/http-specs/specs/encode/boolean/mockapi.ts b/packages/http-specs/specs/encode/boolean/mockapi.ts index 89a41d5d23e..5a430256cd3 100644 --- a/packages/http-specs/specs/encode/boolean/mockapi.ts +++ b/packages/http-specs/specs/encode/boolean/mockapi.ts @@ -2,19 +2,38 @@ import { json, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api"; export const Scenarios: Record = {}; -Scenarios.Encode_Boolean_Property_boolAsString = passOnSuccess({ - uri: "/encode/boolean/property/bool-as-string", - method: "post", - request: { - body: json({ - value: "true", - }), - }, - response: { - status: 200, - body: json({ - value: "true", - }), - }, - kind: "MockApiDefinition", -}); +function createBodyServerTests(uri: string, value: string) { + return passOnSuccess({ + uri, + method: "post", + request: { + body: json({ + value, + }), + }, + response: { + status: 200, + body: json({ + value, + }), + }, + kind: "MockApiDefinition", + }); +} + +Scenarios.Encode_Boolean_Property_trueLower = createBodyServerTests( + "/encode/boolean/property/true-lower", + "true", +); +Scenarios.Encode_Boolean_Property_falseLower = createBodyServerTests( + "/encode/boolean/property/false-lower", + "false", +); +Scenarios.Encode_Boolean_Property_trueUpper = createBodyServerTests( + "/encode/boolean/property/true-upper", + "TRUE", +); +Scenarios.Encode_Boolean_Property_falseMixed = createBodyServerTests( + "/encode/boolean/property/false-mixed", + "FaLsE", +); diff --git a/website/src/content/docs/docs/standard-library/built-in-decorators.md b/website/src/content/docs/docs/standard-library/built-in-decorators.md index 8e5054ce83f..d5827c149b9 100644 --- a/website/src/content/docs/docs/standard-library/built-in-decorators.md +++ b/website/src/content/docs/docs/standard-library/built-in-decorators.md @@ -223,7 +223,7 @@ model Pet { ##### encode boolean type to string -`@encode(string)` on boolean uses only case-insensitive `true` or `false` values. +`@encode(string)` on boolean uses case-insensitive `true` / `false` values. ```tsp model FeatureFlags { From 888a49e8881b07a7ff4c1db1f914d4bfb3e0dad5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:23:32 +0000 Subject: [PATCH 09/16] test(http-specs): use doc comment in boolean encode spec Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/specs/encode/boolean/main.tsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-specs/specs/encode/boolean/main.tsp b/packages/http-specs/specs/encode/boolean/main.tsp index 0a4771271cd..d0b146d17a9 100644 --- a/packages/http-specs/specs/encode/boolean/main.tsp +++ b/packages/http-specs/specs/encode/boolean/main.tsp @@ -4,7 +4,7 @@ import "@typespec/spector"; using Http; using Spector; -@doc("Test for encode decorator on boolean.") +/** Test for encode decorator on boolean. */ @scenarioService("/encode/boolean") namespace Encode.Boolean; From 397cbd1cf4ababba5167b00287a84c7ad4a1da20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:39:26 +0000 Subject: [PATCH 10/16] test(http-specs): accept case-insensitive boolean request casing Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../specs/encode/boolean/mockapi.ts | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/http-specs/specs/encode/boolean/mockapi.ts b/packages/http-specs/specs/encode/boolean/mockapi.ts index 5a430256cd3..fac2bee83ec 100644 --- a/packages/http-specs/specs/encode/boolean/mockapi.ts +++ b/packages/http-specs/specs/encode/boolean/mockapi.ts @@ -1,20 +1,35 @@ -import { json, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api"; +import { createMatcher, err, json, ok, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api"; export const Scenarios: Record = {}; -function createBodyServerTests(uri: string, value: string) { +function createCaseInsensitiveBooleanMatcher(value: boolean) { + const normalized = String(value); + return createMatcher({ + check: (actual) => { + if (typeof actual !== "string") { + return err(`Expected string "${normalized}" but got ${typeof actual}`); + } + return actual.toLowerCase() === normalized + ? ok() + : err(`Expected case-insensitive "${normalized}" but got "${actual}"`); + }, + serialize: () => normalized, + }); +} + +function createBodyServerTests(uri: string, responseValue: string, requestValue: boolean) { return passOnSuccess({ uri, method: "post", request: { body: json({ - value, + value: createCaseInsensitiveBooleanMatcher(requestValue), }), }, response: { status: 200, body: json({ - value, + value: responseValue, }), }, kind: "MockApiDefinition", @@ -24,16 +39,20 @@ function createBodyServerTests(uri: string, value: string) { Scenarios.Encode_Boolean_Property_trueLower = createBodyServerTests( "/encode/boolean/property/true-lower", "true", + true, ); Scenarios.Encode_Boolean_Property_falseLower = createBodyServerTests( "/encode/boolean/property/false-lower", "false", + false, ); Scenarios.Encode_Boolean_Property_trueUpper = createBodyServerTests( "/encode/boolean/property/true-upper", "TRUE", + true, ); Scenarios.Encode_Boolean_Property_falseMixed = createBodyServerTests( "/encode/boolean/property/false-mixed", "FaLsE", + false, ); From 5df2924991266be0fc8115fed65ceffa60f083c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:59:38 +0000 Subject: [PATCH 11/16] feat(spec-api): add case-insensitive boolean string matcher Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../specs/encode/boolean/mockapi.ts | 19 +------ packages/spec-api/src/matchers/boolean.ts | 28 ++++++++++ packages/spec-api/src/matchers/index.ts | 11 ++++ .../spec-api/test/matchers/boolean.test.ts | 54 +++++++++++++++++++ 4 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 packages/spec-api/src/matchers/boolean.ts create mode 100644 packages/spec-api/test/matchers/boolean.test.ts diff --git a/packages/http-specs/specs/encode/boolean/mockapi.ts b/packages/http-specs/specs/encode/boolean/mockapi.ts index fac2bee83ec..641d09f7783 100644 --- a/packages/http-specs/specs/encode/boolean/mockapi.ts +++ b/packages/http-specs/specs/encode/boolean/mockapi.ts @@ -1,29 +1,14 @@ -import { createMatcher, err, json, ok, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api"; +import { json, match, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api"; export const Scenarios: Record = {}; -function createCaseInsensitiveBooleanMatcher(value: boolean) { - const normalized = String(value); - return createMatcher({ - check: (actual) => { - if (typeof actual !== "string") { - return err(`Expected string "${normalized}" but got ${typeof actual}`); - } - return actual.toLowerCase() === normalized - ? ok() - : err(`Expected case-insensitive "${normalized}" but got "${actual}"`); - }, - serialize: () => normalized, - }); -} - function createBodyServerTests(uri: string, responseValue: string, requestValue: boolean) { return passOnSuccess({ uri, method: "post", request: { body: json({ - value: createCaseInsensitiveBooleanMatcher(requestValue), + value: match.boolean.caseInsensitiveString(requestValue), }), }, response: { diff --git a/packages/spec-api/src/matchers/boolean.ts b/packages/spec-api/src/matchers/boolean.ts new file mode 100644 index 00000000000..2f16df33083 --- /dev/null +++ b/packages/spec-api/src/matchers/boolean.ts @@ -0,0 +1,28 @@ +import { createMatcher, err, type MockValueMatcher, ok } from "../match-engine.js"; + +export const booleanMatcher = { + caseInsensitiveString(value: boolean): MockValueMatcher { + const normalized = String(value); + return createMatcher({ + check(actual: unknown) { + if (typeof actual !== "string") { + return err( + `match.boolean.caseInsensitiveString: expected a string but got ${typeof actual} (${JSON.stringify(actual)})`, + ); + } + if (actual.toLowerCase() !== normalized) { + return err( + `match.boolean.caseInsensitiveString: expected case-insensitive "${normalized}" but got "${actual}"`, + ); + } + return ok(); + }, + serialize() { + return normalized; + }, + toString() { + return `match.boolean.caseInsensitiveString(${normalized})`; + }, + }); + }, +}; diff --git a/packages/spec-api/src/matchers/index.ts b/packages/spec-api/src/matchers/index.ts index 054a5bb5c3e..3b4b229228e 100644 --- a/packages/spec-api/src/matchers/index.ts +++ b/packages/spec-api/src/matchers/index.ts @@ -1,3 +1,4 @@ +import { booleanMatcher } from "./boolean.js"; import { dateTimeMatcher } from "./datetime.js"; import { baseUrlMatcher } from "./local-url.js"; @@ -18,6 +19,16 @@ export { dateTimeMatcher } from "./datetime.js"; * Namespace for built-in matchers. */ export const match = { + /** + * Matchers for comparing boolean values encoded as strings. + * + * @example + * ```ts + * match.boolean.caseInsensitiveString(true) + * ``` + */ + boolean: booleanMatcher, + /** * Matchers for comparing datetime values semantically. * Validates that the actual value is in the correct format and represents diff --git a/packages/spec-api/test/matchers/boolean.test.ts b/packages/spec-api/test/matchers/boolean.test.ts new file mode 100644 index 00000000000..21a9889f5b6 --- /dev/null +++ b/packages/spec-api/test/matchers/boolean.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { isMatcher } from "../../src/match-engine.js"; +import { match } from "../../src/matchers/index.js"; +import { expectFail, expectPass } from "./matcher-test-utils.js"; + +describe("match.boolean.caseInsensitiveString()", () => { + it("should be identified by isMatcher", () => { + expect(isMatcher(match.boolean.caseInsensitiveString(true))).toBe(true); + }); + + describe("check()", () => { + it("should match lower-case true", () => { + expectPass(match.boolean.caseInsensitiveString(true).check("true")); + }); + + it("should match upper-case true", () => { + expectPass(match.boolean.caseInsensitiveString(true).check("TRUE")); + }); + + it("should match mixed-case false", () => { + expectPass(match.boolean.caseInsensitiveString(false).check("FaLsE")); + }); + + it("should reject the opposite boolean value", () => { + expectFail(match.boolean.caseInsensitiveString(true).check("false"), 'expected case-insensitive "true"'); + }); + + it("should reject non-string values", () => { + expectFail( + match.boolean.caseInsensitiveString(true).check(true), + "expected a string but got boolean", + ); + expectFail( + match.boolean.caseInsensitiveString(false).check(null), + "expected a string but got object", + ); + }); + }); + + describe("serialize()", () => { + it("should serialize to canonical lowercase value", () => { + expect(match.boolean.caseInsensitiveString(true).serialize()).toBe("true"); + expect(match.boolean.caseInsensitiveString(false).serialize()).toBe("false"); + }); + }); + + describe("toString()", () => { + it("should return a descriptive string", () => { + expect(match.boolean.caseInsensitiveString(true).toString()).toBe( + "match.boolean.caseInsensitiveString(true)", + ); + }); + }); +}); From 4c3aba625149aa75be20c7ad674caec47af8a257 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:27:24 +0000 Subject: [PATCH 12/16] refactor(spec-api): generalize case-insensitive string matcher Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../specs/encode/boolean/mockapi.ts | 2 +- packages/spec-api/src/matchers/boolean.ts | 28 ---------- packages/spec-api/src/matchers/index.ts | 13 ++--- packages/spec-api/src/matchers/string.ts | 28 ++++++++++ .../spec-api/test/matchers/boolean.test.ts | 54 ------------------- .../spec-api/test/matchers/string.test.ts | 48 +++++++++++++++++ 6 files changed, 80 insertions(+), 93 deletions(-) delete mode 100644 packages/spec-api/src/matchers/boolean.ts create mode 100644 packages/spec-api/src/matchers/string.ts delete mode 100644 packages/spec-api/test/matchers/boolean.test.ts create mode 100644 packages/spec-api/test/matchers/string.test.ts diff --git a/packages/http-specs/specs/encode/boolean/mockapi.ts b/packages/http-specs/specs/encode/boolean/mockapi.ts index 641d09f7783..590645176d3 100644 --- a/packages/http-specs/specs/encode/boolean/mockapi.ts +++ b/packages/http-specs/specs/encode/boolean/mockapi.ts @@ -8,7 +8,7 @@ function createBodyServerTests(uri: string, responseValue: string, requestValue: method: "post", request: { body: json({ - value: match.boolean.caseInsensitiveString(requestValue), + value: match.string.caseInsensitive(String(requestValue)), }), }, response: { diff --git a/packages/spec-api/src/matchers/boolean.ts b/packages/spec-api/src/matchers/boolean.ts deleted file mode 100644 index 2f16df33083..00000000000 --- a/packages/spec-api/src/matchers/boolean.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createMatcher, err, type MockValueMatcher, ok } from "../match-engine.js"; - -export const booleanMatcher = { - caseInsensitiveString(value: boolean): MockValueMatcher { - const normalized = String(value); - return createMatcher({ - check(actual: unknown) { - if (typeof actual !== "string") { - return err( - `match.boolean.caseInsensitiveString: expected a string but got ${typeof actual} (${JSON.stringify(actual)})`, - ); - } - if (actual.toLowerCase() !== normalized) { - return err( - `match.boolean.caseInsensitiveString: expected case-insensitive "${normalized}" but got "${actual}"`, - ); - } - return ok(); - }, - serialize() { - return normalized; - }, - toString() { - return `match.boolean.caseInsensitiveString(${normalized})`; - }, - }); - }, -}; diff --git a/packages/spec-api/src/matchers/index.ts b/packages/spec-api/src/matchers/index.ts index 3b4b229228e..c28fbf7d986 100644 --- a/packages/spec-api/src/matchers/index.ts +++ b/packages/spec-api/src/matchers/index.ts @@ -1,6 +1,6 @@ -import { booleanMatcher } from "./boolean.js"; import { dateTimeMatcher } from "./datetime.js"; import { baseUrlMatcher } from "./local-url.js"; +import { stringMatcher } from "./string.js"; export { createMatcher, @@ -19,15 +19,8 @@ export { dateTimeMatcher } from "./datetime.js"; * Namespace for built-in matchers. */ export const match = { - /** - * Matchers for comparing boolean values encoded as strings. - * - * @example - * ```ts - * match.boolean.caseInsensitiveString(true) - * ``` - */ - boolean: booleanMatcher, + /** Matchers for comparing string values. */ + string: stringMatcher, /** * Matchers for comparing datetime values semantically. diff --git a/packages/spec-api/src/matchers/string.ts b/packages/spec-api/src/matchers/string.ts new file mode 100644 index 00000000000..322ac3cafea --- /dev/null +++ b/packages/spec-api/src/matchers/string.ts @@ -0,0 +1,28 @@ +import { createMatcher, err, type MockValueMatcher, ok } from "../match-engine.js"; + +export const stringMatcher = { + caseInsensitive(value: string): MockValueMatcher { + const normalized = value.toLowerCase(); + return createMatcher({ + check(actual: unknown) { + if (typeof actual !== "string") { + return err( + `match.string.caseInsensitive: expected a string but got ${typeof actual}`, + ); + } + if (actual.toLowerCase() !== normalized) { + return err( + `match.string.caseInsensitive: expected case-insensitive "${value}" but got "${actual}"`, + ); + } + return ok(); + }, + serialize() { + return value; + }, + toString() { + return `match.string.caseInsensitive("${value}")`; + }, + }); + }, +}; diff --git a/packages/spec-api/test/matchers/boolean.test.ts b/packages/spec-api/test/matchers/boolean.test.ts deleted file mode 100644 index 21a9889f5b6..00000000000 --- a/packages/spec-api/test/matchers/boolean.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { isMatcher } from "../../src/match-engine.js"; -import { match } from "../../src/matchers/index.js"; -import { expectFail, expectPass } from "./matcher-test-utils.js"; - -describe("match.boolean.caseInsensitiveString()", () => { - it("should be identified by isMatcher", () => { - expect(isMatcher(match.boolean.caseInsensitiveString(true))).toBe(true); - }); - - describe("check()", () => { - it("should match lower-case true", () => { - expectPass(match.boolean.caseInsensitiveString(true).check("true")); - }); - - it("should match upper-case true", () => { - expectPass(match.boolean.caseInsensitiveString(true).check("TRUE")); - }); - - it("should match mixed-case false", () => { - expectPass(match.boolean.caseInsensitiveString(false).check("FaLsE")); - }); - - it("should reject the opposite boolean value", () => { - expectFail(match.boolean.caseInsensitiveString(true).check("false"), 'expected case-insensitive "true"'); - }); - - it("should reject non-string values", () => { - expectFail( - match.boolean.caseInsensitiveString(true).check(true), - "expected a string but got boolean", - ); - expectFail( - match.boolean.caseInsensitiveString(false).check(null), - "expected a string but got object", - ); - }); - }); - - describe("serialize()", () => { - it("should serialize to canonical lowercase value", () => { - expect(match.boolean.caseInsensitiveString(true).serialize()).toBe("true"); - expect(match.boolean.caseInsensitiveString(false).serialize()).toBe("false"); - }); - }); - - describe("toString()", () => { - it("should return a descriptive string", () => { - expect(match.boolean.caseInsensitiveString(true).toString()).toBe( - "match.boolean.caseInsensitiveString(true)", - ); - }); - }); -}); diff --git a/packages/spec-api/test/matchers/string.test.ts b/packages/spec-api/test/matchers/string.test.ts new file mode 100644 index 00000000000..046818de0de --- /dev/null +++ b/packages/spec-api/test/matchers/string.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { isMatcher } from "../../src/match-engine.js"; +import { match } from "../../src/matchers/index.js"; +import { expectFail, expectPass } from "./matcher-test-utils.js"; + +describe("match.string.caseInsensitive()", () => { + it("should be identified by isMatcher", () => { + expect(isMatcher(match.string.caseInsensitive("true"))).toBe(true); + }); + + describe("check()", () => { + it("should match lower-case true", () => { + expectPass(match.string.caseInsensitive("true").check("true")); + }); + + it("should match upper-case true", () => { + expectPass(match.string.caseInsensitive("true").check("TRUE")); + }); + + it("should match mixed-case false", () => { + expectPass(match.string.caseInsensitive("false").check("FaLsE")); + }); + + it("should reject a different string value", () => { + expectFail(match.string.caseInsensitive("true").check("false"), 'expected case-insensitive "true"'); + }); + + it("should reject non-string values", () => { + expectFail(match.string.caseInsensitive("true").check(true), "expected a string but got boolean"); + expectFail(match.string.caseInsensitive("false").check(null), "expected a string but got object"); + }); + }); + + describe("serialize()", () => { + it("should serialize the original value", () => { + expect(match.string.caseInsensitive("TrUe").serialize()).toBe("TrUe"); + expect(match.string.caseInsensitive("FALSE").serialize()).toBe("FALSE"); + }); + }); + + describe("toString()", () => { + it("should return a descriptive string", () => { + expect(match.string.caseInsensitive("true").toString()).toBe( + 'match.string.caseInsensitive("true")', + ); + }); + }); +}); From 8cfc9132f26dd47c7fcb3c1dd3d6f40ea8ba8ed3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:28:12 +0000 Subject: [PATCH 13/16] chore(spec-api): simplify string matcher error messages Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/spec-api/src/matchers/string.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/spec-api/src/matchers/string.ts b/packages/spec-api/src/matchers/string.ts index 322ac3cafea..a2b2908e79c 100644 --- a/packages/spec-api/src/matchers/string.ts +++ b/packages/spec-api/src/matchers/string.ts @@ -6,14 +6,10 @@ export const stringMatcher = { return createMatcher({ check(actual: unknown) { if (typeof actual !== "string") { - return err( - `match.string.caseInsensitive: expected a string but got ${typeof actual}`, - ); + return err(`expected a string but got ${typeof actual}`); } if (actual.toLowerCase() !== normalized) { - return err( - `match.string.caseInsensitive: expected case-insensitive "${value}" but got "${actual}"`, - ); + return err(`expected case-insensitive "${value}" but got "${actual}"`); } return ok(); }, From f095d136ad90a204035d1d46ddfbfe06e65ad573 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:08:25 +0000 Subject: [PATCH 14/16] chore(compiler): regenerate generated defs Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/generated-defs/TypeSpec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index d93788afbeb..0fa7f3838e6 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -98,7 +98,6 @@ export type MediaTypeHintDecorator = ( * @encode(string) id: int64; * } * ``` - * * @example encode boolean type to string * * `@encode(string)` on boolean uses case-insensitive `true` / `false` values. From 7d5c39165ea17d975613e56a463d577f689ffe72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:16:13 +0000 Subject: [PATCH 15/16] style: format boolean encode spec and string matcher tests Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/specs/encode/boolean/main.tsp | 4 +++- packages/spec-api/test/matchers/string.test.ts | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/http-specs/specs/encode/boolean/main.tsp b/packages/http-specs/specs/encode/boolean/main.tsp index d0b146d17a9..9d4eba49d28 100644 --- a/packages/http-specs/specs/encode/boolean/main.tsp +++ b/packages/http-specs/specs/encode/boolean/main.tsp @@ -53,7 +53,9 @@ namespace Property { } ``` """, - { value: StringValue } + { + value: StringValue, + } ) @post sendBoolAsString(@body value: BoolAsStringProperty): BoolAsStringProperty; diff --git a/packages/spec-api/test/matchers/string.test.ts b/packages/spec-api/test/matchers/string.test.ts index 046818de0de..2d48d48a5da 100644 --- a/packages/spec-api/test/matchers/string.test.ts +++ b/packages/spec-api/test/matchers/string.test.ts @@ -22,12 +22,21 @@ describe("match.string.caseInsensitive()", () => { }); it("should reject a different string value", () => { - expectFail(match.string.caseInsensitive("true").check("false"), 'expected case-insensitive "true"'); + expectFail( + match.string.caseInsensitive("true").check("false"), + 'expected case-insensitive "true"', + ); }); it("should reject non-string values", () => { - expectFail(match.string.caseInsensitive("true").check(true), "expected a string but got boolean"); - expectFail(match.string.caseInsensitive("false").check(null), "expected a string but got object"); + expectFail( + match.string.caseInsensitive("true").check(true), + "expected a string but got boolean", + ); + expectFail( + match.string.caseInsensitive("false").check(null), + "expected a string but got object", + ); }); }); From c58089fe42977cca0d30f45bfb9c80679ea48d07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:26:11 +0000 Subject: [PATCH 16/16] chore: add chronus changelog entries for encode boolean updates Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...t-encode-string-boolean-changelog-2026-6-5-17-25-0.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .chronus/changes/copilot-encode-string-boolean-changelog-2026-6-5-17-25-0.md diff --git a/.chronus/changes/copilot-encode-string-boolean-changelog-2026-6-5-17-25-0.md b/.chronus/changes/copilot-encode-string-boolean-changelog-2026-6-5-17-25-0.md new file mode 100644 index 00000000000..316436bef04 --- /dev/null +++ b/.chronus/changes/copilot-encode-string-boolean-changelog-2026-6-5-17-25-0.md @@ -0,0 +1,9 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" + - "@typespec/spec-api" + - "@typespec/http-specs" +--- + +Allow `@encode(string)` on boolean targets, define case-insensitive `true`/`false` string semantics, and add shared case-insensitive string matcher support with encode/boolean Spector coverage.