Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/SwaggerProvider.Runtime/RuntimeHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -378,13 +378,32 @@ module RuntimeHelpers =
let props = getProperties(obj.GetType())
let sb = System.Text.StringBuilder()

let appendFormattedArray(v: obj) =
let rec appendFormattedArray(v: obj) =
let vTy = v.GetType()
sb.Append('[') |> ignore
let mutable firstEl = true
let elTy = vTy.GetElementType()
let isDateOnly = not(isNull elTy) && elTy.FullName = dateOnlyTypeName
let isTimeOnly = not(isNull elTy) && elTy.FullName = timeOnlyTypeName
// Detect array<Option<T>> so elements are correctly unwrapped and formatted.
// Without this, Option<DateOnly>/Option<TimeOnly> elements fall through to
// obj.ToString(), producing locale-specific "Some(07/04/2025)" instead of ISO 8601.
let isOptionEl =
not(isNull elTy)
&& elTy.IsGenericType
&& elTy.GetGenericTypeDefinition() = typedefof<option<_>>

let optionTagReader =
if isOptionEl then
Some(optionTagReaderCache.GetOrAdd(elTy, optionTagReaderFactory))
else
None

let optionValueProp =
if isOptionEl then
Some(optionValueCache.GetOrAdd(elTy, optionValueFactory))
else
None

for x in (v :?> Array) |> Seq.cast<obj> do
if not firstEl then
Expand All @@ -398,6 +417,21 @@ module RuntimeHelpers =
sb.Append(formatDateOrTimeValue "yyyy-MM-dd" elTy x) |> ignore
elif isTimeOnly then
sb.Append(formatDateOrTimeValue "HH:mm:ss.FFFFFFF" elTy x) |> ignore
elif isOptionEl then
if optionTagReader.Value x = 1 then // 1 = Some
let inner = optionValueProp.Value.GetValue(x)

if isNull inner then
sb.Append("null") |> ignore
else
let innerTy = inner.GetType()

if innerTy.IsArray then
appendFormattedArray inner
else
appendFormattedValue sb inner innerTy
else
sb.Append("null") |> ignore
else
sb.Append(x.ToString()) |> ignore

Expand Down
37 changes: 37 additions & 0 deletions tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1184,6 +1184,15 @@ type FmtOptionStringArray(tags: string[] option) =
type FmtOptionDateOnlyArray(dates: DateOnly[] option) =
member _.Dates = dates

type FmtOptionDateOnlyElements(dates: DateOnly option[]) =
member _.Dates = dates

type FmtOptionTimeOnlyElements(times: TimeOnly option[]) =
member _.Times = times

type FmtOptionStringElements(names: string option[]) =
member _.Names = names


module FormatObjectTests =

Expand Down Expand Up @@ -1310,6 +1319,34 @@ module FormatObjectTests =

formatObject obj |> shouldEqual "{Dates=[2025-01-01; 2025-12-31]}"

[<Fact>]
let ``formatObject formats array of Option DateOnly Some elements as ISO 8601``() =
let obj =
FmtOptionDateOnlyElements([| Some(DateOnly(2025, 1, 1)); Some(DateOnly(2025, 12, 31)) |])

formatObject obj |> shouldEqual "{Dates=[2025-01-01; 2025-12-31]}"

[<Fact>]
let ``formatObject formats array of Option DateOnly with None elements as null``() =
let obj = FmtOptionDateOnlyElements([| Some(DateOnly(2025, 7, 4)); None |])
formatObject obj |> shouldEqual "{Dates=[2025-07-04; null]}"

[<Fact>]
let ``formatObject formats array of Option TimeOnly Some elements using HH:mm:ss.FFFFFFF``() =
let obj =
FmtOptionTimeOnlyElements([| Some(TimeOnly(14, 30, 0)); Some(TimeOnly(9, 5, 3, 123)) |])

formatObject obj |> shouldEqual "{Times=[14:30:00; 09:05:03.123]}"

[<Fact>]
let ``formatObject formats array of Option TimeOnly with None elements as null``() =
let obj = FmtOptionTimeOnlyElements([| None; Some(TimeOnly(8, 0, 0)) |])
formatObject obj |> shouldEqual "{Times=[null; 08:00:00]}"

[<Fact>]
let ``formatObject formats array of Option string elements``() =
let obj = FmtOptionStringElements([| Some "hello"; None; Some "world" |])
formatObject obj |> shouldEqual "{Names=[\"hello\"; null; \"world\"]}"


[<Fact>]
Expand Down
Loading