From 38c067144a4b1f173f3289b1a4e6c5b0d705c3c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:55:08 +0000 Subject: [PATCH 1/3] fix: correct string enum array serialisation in toQueryParams; add enum query-param tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toQueryParams had a silent bug: any array of an int32-backed enum type (e.g. Status[]) matched the '| :? array' arm via CLR array variance before the generic Array arm could run. For integer enums this happened to produce the right result (the integer string), but for string enums (annotated with JsonStringEnumConverter) it discarded the wire-name lookup and returned the underlying integer instead ('0' instead of 'active'). Fix: add a dedicated enum-array arm that fires before all the concrete array arms. It uses the cached buildEnumSerializer per element type so both integer and string enums are serialised correctly. Also simplifies createHttpRequestFromQueryLists by replacing the explicit ResizeArray accumulation loop with Seq.concat, which lazily flattens without an intermediate heap allocation (createHttpRequest already filters nulls in its own loop). New tests in ToQueryParamsTests: - single integer enum value - single string enum value - string enum value with special-character wire name (e.g. 'in-progress') - integer enum array - string enum array ← previously produced wrong integer strings Test count: 512 → 517 (5 new). All 517 pass; 1 pre-existing skip unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 26 ++++++---- .../RuntimeHelpersTests.fs | 47 +++++++++++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index aaa94854..c7f07e3a 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -266,6 +266,19 @@ module RuntimeHelpers = match x with | Some xs -> [ name, (client.Serialize xs).Trim('\"') ] | None -> [] + // Handle enum arrays before the concrete int32/int64/bool array arms. + // CLR array variance lets any int32-backed enum[] match "| :? array", which + // would lose the wire-name information for string enums. We intercept enum arrays + // first and serialise each element via the cached buildEnumSerializer. + | :? Array as xs when (let et = xs.GetType().GetElementType() in not(isNull et) && et.IsEnum) -> + let elTy = xs.GetType().GetElementType() + let serializer = enumSerializerCache.GetOrAdd(elTy, enumSerializerFactory) + + [ for i in 0 .. xs.Length - 1 do + let param = serializer(xs.GetValue(i)) + + if not(isNull param) then + yield name, param ] | :? array as xs -> xs |> toStrArray name | :? array as xs -> xs |> toStrArray name | :? array as xs -> xs |> toStrArray name @@ -287,7 +300,7 @@ module RuntimeHelpers = | :? Array as xs when xs.GetType().GetElementType() |> Option.ofObj - |> Option.exists(fun t -> isDateOnlyLikeType t || isTimeOnlyLikeType t || t.IsEnum) + |> Option.exists(fun t -> isDateOnlyLikeType t || isTimeOnlyLikeType t) -> xs |> Seq.cast @@ -608,14 +621,9 @@ module RuntimeHelpers = new HttpRequestMessage(method, Uri(requestUrl, UriKind.Relative)) let createHttpRequestFromQueryLists (httpMethod: string) (address: string) (queryParamLists: seq<#seq>) = - let queryParams = ResizeArray() - - for queryParamList in queryParamLists do - for name, value in queryParamList do - if not(isNull value) then - queryParams.Add(name, value) - - createHttpRequest httpMethod address queryParams + // Seq.concat lazily flattens the nested sequences without allocating an intermediate ResizeArray. + // createHttpRequest already filters out null values in its own loop. + createHttpRequest httpMethod address (Seq.concat queryParamLists) let fillHeaders (msg: HttpRequestMessage) (headers: (string * string) seq) = headers diff --git a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs index 5e70792b..190568bb 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -509,6 +509,53 @@ module ToQueryParamsTests = let result = toQueryParams "times" (box values) stubClient result |> shouldEqual [ ("times", "08:00:00") ] + [] + let ``toQueryParams handles single integer enum value``() = + // Single CLI enum values fall through to toParam, which uses buildEnumSerializer + // (cached) to produce the underlying integer as a string. + let result = + toQueryParams "status" (box EnumToParamTests.IntStatus.Running) stubClient + + result |> shouldEqual [ ("status", "2") ] + + [] + let ``toQueryParams handles single string enum value``() = + // String enums (annotated with JsonStringEnumConverter) are serialised to their + // OpenAPI wire value via the cached buildEnumSerializer. + let result = + toQueryParams "status" (box EnumToParamTests.StringStatus.Active) stubClient + + result |> shouldEqual [ ("status", "active") ] + + [] + let ``toQueryParams handles string enum value with special-character wire name``() = + let result = + toQueryParams "status" (box EnumToParamTests.StringStatus.InProgress) stubClient + + result |> shouldEqual [ ("status", "in-progress") ] + + [] + let ``toQueryParams handles integer enum array``() = + // Arrays of CLI enum types go through the generic Array branch (t.IsEnum check), + // which calls toParam on each element to produce the underlying integer string. + let values: EnumToParamTests.IntStatus[] = + [| EnumToParamTests.IntStatus.Pending; EnumToParamTests.IntStatus.Done |] + + let result = toQueryParams "status" (box values) stubClient + result |> shouldEqual [ ("status", "1"); ("status", "3") ] + + [] + let ``toQueryParams handles string enum array``() = + // Each element is serialised to its OpenAPI wire value. + let values: EnumToParamTests.StringStatus[] = + [| EnumToParamTests.StringStatus.Active + EnumToParamTests.StringStatus.InProgress |] + + let result = toQueryParams "status" (box values) stubClient + + result + |> shouldEqual [ ("status", "active"); ("status", "in-progress") ] + module CombineUrlTests = From 5e4430774d66d30a26b9d44fd81c7eb94702f4a9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Jun 2026 03:55:11 +0000 Subject: [PATCH 2/3] ci: trigger checks From 7c816af1b31afe54a160fbc9b1a06f5cb00d52da Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Sun, 21 Jun 2026 08:24:55 +0200 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs index 190568bb..b45c9f7a 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -536,8 +536,8 @@ module ToQueryParamsTests = [] let ``toQueryParams handles integer enum array``() = - // Arrays of CLI enum types go through the generic Array branch (t.IsEnum check), - // which calls toParam on each element to produce the underlying integer string. + // Arrays of CLI enum types are handled by the dedicated enum-array branch in toQueryParams, + // which serialises each element via the cached buildEnumSerializer. let values: EnumToParamTests.IntStatus[] = [| EnumToParamTests.IntStatus.Pending; EnumToParamTests.IntStatus.Done |]