diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 5ef888e..9f2390c 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -61,21 +61,38 @@ module RuntimeHelpers = let inline private toStrArrayDateTimeOffsetOpt name values = values |> Array.choose(id) |> toStrArrayDateTimeOffset name + let private dateOnlyTypeName = "System.DateOnly" - let inline private toStrOpt name value = - match value with - | Some(x) -> [ name, x.ToString() ] - | None -> [] + let private isDateOnlyType(t: Type) = + not(isNull t) && t.FullName = dateOnlyTypeName - let inline private toStrDateTimeOpt name (value: DateTime option) = - match value with - | Some(x) -> [ name, x.ToString("O") ] - | None -> [] + let private isOptionOfDateOnlyType(t: Type) = + t.IsGenericType + && t.GetGenericTypeDefinition() = typedefof> + && isDateOnlyType(t.GetGenericArguments().[0]) - let inline private toStrDateTimeOffsetOpt name (value: DateTimeOffset option) = - match value with - | Some(x) -> [ name, x.ToString("O") ] - | None -> [] + let private isDateOnlyLikeType(t: Type) = + isDateOnlyType t || isOptionOfDateOnlyType t + + let private tryFormatDateOnly(value: obj) = + if isNull value then + None + else + let ty = value.GetType() + + if isDateOnlyType ty then + match value with + | :? IFormattable as formattable -> Some(formattable.ToString("yyyy-MM-dd", Globalization.CultureInfo.InvariantCulture)) + | _ -> + match + ty.GetMethod("ToString", [| typeof; typeof |]) + |> Option.ofObj + with + | Some methodInfo -> + Some(methodInfo.Invoke(value, [| box "yyyy-MM-dd"; box Globalization.CultureInfo.InvariantCulture |]) :?> string) + | None -> None + else + None let rec toParam(obj: obj) = match obj with @@ -83,21 +100,24 @@ module RuntimeHelpers = | :? DateTimeOffset as dto -> dto.ToString("O") | null -> null | _ -> - let ty = obj.GetType() - - // Unwrap F# Option: Some(x) -> toParam(x), None -> null - if - ty.IsGenericType - && ty.GetGenericTypeDefinition() = typedefof> - then - let (case, values) = Microsoft.FSharp.Reflection.FSharpValue.GetUnionFields(obj, ty) - - if case.Name = "Some" && values.Length > 0 then - toParam values.[0] + match tryFormatDateOnly obj with + | Some formatted -> formatted + | None -> + let ty = obj.GetType() + + // Unwrap F# Option: Some(x) -> toParam(x), None -> null + if + ty.IsGenericType + && ty.GetGenericTypeDefinition() = typedefof> + then + let (case, values) = Microsoft.FSharp.Reflection.FSharpValue.GetUnionFields(obj, ty) + + if case.Name = "Some" && values.Length > 0 then + toParam values.[0] + else + null else - null - else - obj.ToString() + obj.ToString() let toQueryParams (name: string) (obj: obj) (client: Swagger.ProvidedApiClientBase) = if isNull obj then @@ -127,19 +147,22 @@ module RuntimeHelpers = | :? array> as xs -> xs |> toStrArrayOpt name | :? array> as xs -> xs |> toStrArrayDateTimeOpt name | :? array> as xs -> xs |> toStrArrayDateTimeOffsetOpt name - | :? array> as xs -> xs |> toStrArray name - | :? Option as x -> x |> toStrOpt name - | :? Option as x -> x |> toStrOpt name - | :? Option as x -> x |> toStrOpt name - | :? Option as x -> x |> toStrOpt name - | :? Option as x -> x |> toStrOpt name - | :? Option as x -> x |> toStrOpt name - | :? Option as x -> x |> toStrDateTimeOpt name - | :? Option as x -> x |> toStrDateTimeOffsetOpt name - | :? DateTime as x -> [ name, x.ToString("O") ] - | :? DateTimeOffset as x -> [ name, x.ToString("O") ] - | :? Option as x -> x |> toStrOpt name - | _ -> [ name, obj.ToString() ] + | :? array> as xs -> xs |> toStrArrayOpt name + | :? Array as xs when + xs.GetType().GetElementType() + |> Option.ofObj + |> Option.exists isDateOnlyLikeType + -> + xs + |> Seq.cast + |> Seq.choose(fun value -> + let param = toParam value + + if isNull param then None else Some(name, param)) + |> Seq.toList + | _ -> + let param = toParam obj + if isNull param then [] else [ name, param ] /// Cache of sorted declared public instance properties per type, to avoid repeated /// reflection and sorting overhead when formatObject is called frequently. diff --git a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs index 4bad731..e2d0b10 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -76,6 +76,23 @@ module ToParamTests = let result = toParam(box(Some dto)) result |> shouldEqual(dto.ToString("O")) + [] + let ``toParam formats DateOnly as ISO 8601``() = + let d = DateOnly(2025, 7, 4) + let result = toParam(box d) + result |> shouldEqual "2025-07-04" + + [] + let ``toParam unwraps Some(DateOnly) and formats as ISO 8601``() = + let d = DateOnly(2025, 1, 31) + let result = toParam(box(Some d)) + result |> shouldEqual "2025-01-31" + + [] + let ``toParam returns null for None(DateOnly)``() = + let result = toParam(box(None: DateOnly option)) + result |> shouldEqual null + module ToQueryParamsTests = @@ -201,6 +218,13 @@ module ToQueryParamsTests = let result = toQueryParams "id" (box [| g1; g2 |]) stubClient result |> shouldEqual [ ("id", g1.ToString()); ("id", g2.ToString()) ] + [] + let ``toQueryParams skips None items in Option array``() = + let g = Guid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + let values: Option[] = [| Some g; None |] + let result = toQueryParams "id" (box values) stubClient + result |> shouldEqual [ ("id", g.ToString()) ] + [] let ``toQueryParams handles float32 array``() = let result = toQueryParams "v" (box [| 1.5f; 2.5f |]) stubClient @@ -295,6 +319,38 @@ module ToQueryParamsTests = let result = toQueryParams "ts" (box values) stubClient result |> shouldEqual [ ("ts", dto.ToString("O")) ] + [] + let ``toQueryParams handles DateOnly as ISO 8601``() = + let d = DateOnly(2025, 7, 4) + let result = toQueryParams "date" (box d) stubClient + result |> shouldEqual [ ("date", "2025-07-04") ] + + [] + let ``toQueryParams handles DateOnly array``() = + let values: DateOnly[] = [| DateOnly(2025, 1, 1); DateOnly(2025, 12, 31) |] + let result = toQueryParams "dates" (box values) stubClient + + result + |> shouldEqual [ ("dates", "2025-01-01"); ("dates", "2025-12-31") ] + + [] + let ``toQueryParams handles Option Some``() = + let d = DateOnly(2025, 6, 15) + let result = toQueryParams "date" (box(Some d)) stubClient + result |> shouldEqual [ ("date", "2025-06-15") ] + + [] + let ``toQueryParams handles Option None``() = + let result = toQueryParams "date" (box(None: DateOnly option)) stubClient + result |> shouldEqual [] + + [] + let ``toQueryParams skips None items in Option array``() = + let d = DateOnly(2025, 3, 1) + let values: Option[] = [| Some d; None |] + let result = toQueryParams "dates" (box values) stubClient + result |> shouldEqual [ ("dates", "2025-03-01") ] + module CombineUrlTests =