Skip to content

Commit 072285f

Browse files
github-actions[bot]CopilotCopilotsergey-tihon
authored
[Repo Assist] fix: use toParam in form content helpers for correct date/time serialization (+4 tests, 296→300) (#395)
* fix: use toParam in toFormUrlEncodedContent and toMultipartFormDataContent for correct date/time serialization (+4 tests, 296→300) Previously both functions used ToString() which gives locale-dependent output for DateTime/DateTimeOffset and incorrect formatting for DateOnly values. They now delegate to toParam, which uses ISO 8601 (O specifier) for DateTime and DateTimeOffset, and yyyy-MM-dd for DateOnly — the same formats used for query parameters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger checks * test: strengthen DateTime/DateTimeOffset form serialization assertions Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/872fbd3c-11f7-4cfc-b551-87b698e37b4b Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> * test: add DateOnly and None-omission form content regression tests Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/0d67ee77-3aa2-4d0c-9a7f-907ce2203d48 Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> * test: align multipart None omission test with async pattern Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/0d67ee77-3aa2-4d0c-9a7f-907ce2203d48 Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> * test: clarify nested option omission cases in form content tests Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/0d67ee77-3aa2-4d0c-9a7f-907ce2203d48 Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com>
1 parent 8ef3c54 commit 072285f

2 files changed

Lines changed: 123 additions & 3 deletions

File tree

src/SwaggerProvider.Runtime/RuntimeHelpers.fs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,16 +312,24 @@ module RuntimeHelpers =
312312
| :? IO.Stream as stream -> addFileStream name stream
313313
| :? (IO.Stream[]) as streams -> streams |> Seq.iter(addFileStream name)
314314
| x ->
315-
let strValue = x.ToString() // TODO: serialize? does not work with arrays probably
316-
cnt.Add(toStringContent strValue, name)
315+
let strValue = toParam x
316+
317+
if not(isNull strValue) then
318+
cnt.Add(toStringContent strValue, name)
317319

318320
cnt
319321

320322
let toFormUrlEncodedContent(keyValues: seq<string * obj>) =
321323
let keyValues =
322324
keyValues
323325
|> Seq.filter(snd >> isNull >> not)
324-
|> Seq.map(fun (k, v) -> Collections.Generic.KeyValuePair(k, v.ToString()))
326+
|> Seq.choose(fun (k, v) ->
327+
let param = toParam v
328+
329+
if isNull param then
330+
None
331+
else
332+
Some(Collections.Generic.KeyValuePair(k, param)))
325333

326334
new FormUrlEncodedContent(keyValues)
327335

tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,65 @@ module ToFormUrlEncodedContentTests =
738738
body |> shouldEqual ""
739739
}
740740

741+
[<Fact>]
742+
let ``toFormUrlEncodedContent formats DateTime as ISO 8601``() =
743+
task {
744+
let dt = DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc)
745+
746+
use content = toFormUrlEncodedContent(seq { ("ts", box dt) })
747+
748+
let! body = content.ReadAsStringAsync()
749+
let encodedValue = body.Substring("ts=".Length)
750+
let decodedValue = WebUtility.UrlDecode(encodedValue)
751+
752+
decodedValue |> shouldEqual(dt.ToString("O"))
753+
}
754+
755+
[<Fact>]
756+
let ``toFormUrlEncodedContent formats DateTimeOffset as ISO 8601``() =
757+
task {
758+
let dto = DateTimeOffset(2024, 6, 15, 10, 30, 0, TimeSpan.Zero)
759+
760+
use content = toFormUrlEncodedContent(seq { ("ts", box dto) })
761+
762+
let! body = content.ReadAsStringAsync()
763+
let encodedValue = body.Substring("ts=".Length)
764+
let decodedValue = WebUtility.UrlDecode(encodedValue)
765+
766+
decodedValue |> shouldEqual(dto.ToString("O"))
767+
}
768+
769+
[<Fact>]
770+
let ``toFormUrlEncodedContent formats DateOnly as ISO 8601``() =
771+
task {
772+
let d = DateOnly(2025, 7, 4)
773+
use content = toFormUrlEncodedContent(seq { ("date", box d) })
774+
775+
let! body = content.ReadAsStringAsync()
776+
let encodedValue = body.Substring("date=".Length)
777+
let decodedValue = WebUtility.UrlDecode(encodedValue)
778+
779+
decodedValue |> shouldEqual "2025-07-04"
780+
}
781+
782+
[<Fact>]
783+
let ``toFormUrlEncodedContent skips values when toParam returns null``() =
784+
task {
785+
let nestedNone = box(Some(None: string option))
786+
787+
use content =
788+
toFormUrlEncodedContent(
789+
seq {
790+
("present", box "yes")
791+
("nestedNone", nestedNone)
792+
}
793+
)
794+
795+
let! body = content.ReadAsStringAsync()
796+
body |> shouldContainText "present=yes"
797+
body |> shouldNotContainText "nestedNone"
798+
}
799+
741800

742801
module ToMultipartFormDataContentTests =
743802

@@ -778,6 +837,59 @@ module ToMultipartFormDataContentTests =
778837

779838
hasFileName |> shouldEqual true
780839

840+
[<Fact>]
841+
let ``toMultipartFormDataContent formats DateTime as ISO 8601``() =
842+
task {
843+
let dt = DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc)
844+
use content = toMultipartFormDataContent(seq { ("ts", box dt) })
845+
let part = content |> Seq.exactlyOne
846+
let! body = part.ReadAsStringAsync()
847+
body |> shouldEqual(dt.ToString("O"))
848+
}
849+
850+
[<Fact>]
851+
let ``toMultipartFormDataContent formats DateTimeOffset as ISO 8601``() =
852+
task {
853+
let dto = DateTimeOffset(2024, 6, 15, 10, 30, 0, TimeSpan.Zero)
854+
use content = toMultipartFormDataContent(seq { ("ts", box dto) })
855+
let part = content |> Seq.exactlyOne
856+
let! body = part.ReadAsStringAsync()
857+
body |> shouldEqual(dto.ToString("O"))
858+
}
859+
860+
[<Fact>]
861+
let ``toMultipartFormDataContent formats DateOnly as ISO 8601``() =
862+
task {
863+
let d = DateOnly(2025, 7, 4)
864+
use content = toMultipartFormDataContent(seq { ("date", box d) })
865+
let part = content |> Seq.exactlyOne
866+
let! body = part.ReadAsStringAsync()
867+
body |> shouldEqual "2025-07-04"
868+
}
869+
870+
[<Fact>]
871+
let ``toMultipartFormDataContent skips values when toParam returns null``() =
872+
task {
873+
let nestedNone = box(Some(None: string option))
874+
875+
use content =
876+
toMultipartFormDataContent(
877+
seq {
878+
("present", box "yes")
879+
("nestedNone", nestedNone)
880+
}
881+
)
882+
883+
content |> Seq.length |> shouldEqual 1
884+
let part = content |> Seq.exactlyOne
885+
let! body = part.ReadAsStringAsync()
886+
887+
part.Headers.ContentDisposition.Name.Trim('"')
888+
|> shouldEqual "present"
889+
890+
body |> shouldEqual "yes"
891+
}
892+
781893

782894
/// Test types for getPropertyValues tests.
783895
type PropValWithAttr(value: string) =

0 commit comments

Comments
 (0)