Skip to content
Merged
101 changes: 62 additions & 39 deletions src/SwaggerProvider.Runtime/RuntimeHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -61,43 +61,63 @@ 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<option<_>>
&& 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<string>; typeof<IFormatProvider> |])
|> Option.ofObj
with
| Some methodInfo ->
Some(methodInfo.Invoke(value, [| box "yyyy-MM-dd"; box Globalization.CultureInfo.InvariantCulture |]) :?> string)
| None -> None
else
None

Comment thread
sergey-tihon marked this conversation as resolved.
let rec toParam(obj: obj) =
match obj with
| :? DateTime as dt -> dt.ToString("O")
| :? DateTimeOffset as dto -> dto.ToString("O")
| null -> null
| _ ->
let ty = obj.GetType()

// Unwrap F# Option<T>: Some(x) -> toParam(x), None -> null
if
ty.IsGenericType
&& ty.GetGenericTypeDefinition() = typedefof<option<_>>
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<T>: Some(x) -> toParam(x), None -> null
if
ty.IsGenericType
&& ty.GetGenericTypeDefinition() = typedefof<option<_>>
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
Expand Down Expand Up @@ -127,19 +147,22 @@ module RuntimeHelpers =
| :? array<Option<string>> as xs -> xs |> toStrArrayOpt name
| :? array<Option<DateTime>> as xs -> xs |> toStrArrayDateTimeOpt name
| :? array<Option<DateTimeOffset>> as xs -> xs |> toStrArrayDateTimeOffsetOpt name
| :? array<Option<Guid>> as xs -> xs |> toStrArray name
| :? Option<bool> as x -> x |> toStrOpt name
| :? Option<int32> as x -> x |> toStrOpt name
| :? Option<int64> as x -> x |> toStrOpt name
| :? Option<float32> as x -> x |> toStrOpt name
| :? Option<double> as x -> x |> toStrOpt name
| :? Option<string> as x -> x |> toStrOpt name
| :? Option<DateTime> as x -> x |> toStrDateTimeOpt name
| :? Option<DateTimeOffset> as x -> x |> toStrDateTimeOffsetOpt name
| :? DateTime as x -> [ name, x.ToString("O") ]
| :? DateTimeOffset as x -> [ name, x.ToString("O") ]
| :? Option<Guid> as x -> x |> toStrOpt name
| _ -> [ name, obj.ToString() ]
| :? array<Option<Guid>> as xs -> xs |> toStrArrayOpt name
| :? Array as xs when
xs.GetType().GetElementType()
|> Option.ofObj
|> Option.exists isDateOnlyLikeType
->
xs
|> Seq.cast<obj>
|> 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.
Expand Down
56 changes: 56 additions & 0 deletions tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,23 @@ module ToParamTests =
let result = toParam(box(Some dto))
result |> shouldEqual(dto.ToString("O"))

[<Fact>]
let ``toParam formats DateOnly as ISO 8601``() =
let d = DateOnly(2025, 7, 4)
let result = toParam(box d)
result |> shouldEqual "2025-07-04"

[<Fact>]
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"

[<Fact>]
let ``toParam returns null for None(DateOnly)``() =
let result = toParam(box(None: DateOnly option))
result |> shouldEqual null


module ToQueryParamsTests =

Expand Down Expand Up @@ -201,6 +218,13 @@ module ToQueryParamsTests =
let result = toQueryParams "id" (box [| g1; g2 |]) stubClient
result |> shouldEqual [ ("id", g1.ToString()); ("id", g2.ToString()) ]

[<Fact>]
let ``toQueryParams skips None items in Option<Guid> array``() =
let g = Guid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
let values: Option<Guid>[] = [| Some g; None |]
let result = toQueryParams "id" (box values) stubClient
result |> shouldEqual [ ("id", g.ToString()) ]

[<Fact>]
let ``toQueryParams handles float32 array``() =
let result = toQueryParams "v" (box [| 1.5f; 2.5f |]) stubClient
Expand Down Expand Up @@ -295,6 +319,38 @@ module ToQueryParamsTests =
let result = toQueryParams "ts" (box values) stubClient
result |> shouldEqual [ ("ts", dto.ToString("O")) ]

[<Fact>]
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") ]

[<Fact>]
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") ]

[<Fact>]
let ``toQueryParams handles Option<DateOnly> Some``() =
let d = DateOnly(2025, 6, 15)
let result = toQueryParams "date" (box(Some d)) stubClient
result |> shouldEqual [ ("date", "2025-06-15") ]

[<Fact>]
let ``toQueryParams handles Option<DateOnly> None``() =
let result = toQueryParams "date" (box(None: DateOnly option)) stubClient
result |> shouldEqual []

[<Fact>]
let ``toQueryParams skips None items in Option<DateOnly> array``() =
let d = DateOnly(2025, 3, 1)
let values: Option<DateOnly>[] = [| Some d; None |]
let result = toQueryParams "dates" (box values) stubClient
result |> shouldEqual [ ("dates", "2025-03-01") ]


Comment thread
sergey-tihon marked this conversation as resolved.
module CombineUrlTests =

Expand Down
Loading