Skip to content

Commit b075432

Browse files
Smaug123claude
andauthored
Serialise list-typed query parameters as repeated key=value pairs (#541)
List- and array-typed [<Query>] parameters were stringified with .ToString(), producing F# list syntax in the URL (e.g. "status-types=%5Bx%3B%20y%5D" for Gitea's NotifyGetList); int[] didn't even compile under net9.0 nullness checking. Each element now contributes its own key=value pair (the "multi" collection format, which is what every array parameter in the Gitea spec declares, and RestEase's convention), individually escaped. The query string is now computed at runtime as a list of per-parameter string-list components, concatenated and appended to the URL (with separator) only when nonempty, so an empty list contributes nothing and an all-empty query leaves the URL bare. Required scalar parameters produce byte-identical URLs to before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 10f04ee commit b075432

8 files changed

Lines changed: 1904 additions & 770 deletions

File tree

ConsumePlugin/Generated2SwaggerGitea.fs

Lines changed: 1343 additions & 729 deletions
Large diffs are not rendered by default.

ConsumePlugin/GeneratedRestClient.fs

Lines changed: 281 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -427,21 +427,31 @@ module PureGymApi =
427427
async {
428428
let! ct = Async.CancellationToken
429429

430+
let queryString =
431+
[
432+
[
433+
"fromDate=" + ((fromDate.ToString "yyyy-MM-dd") |> System.Uri.EscapeDataString)
434+
]
435+
[ "toDate=" + ((toDate.ToString "yyyy-MM-dd") |> System.Uri.EscapeDataString) ]
436+
]
437+
|> List.concat
438+
|> String.concat "&"
439+
430440
let uri =
431441
System.Uri (
432442
(match client.BaseAddress with
433443
| null -> System.Uri "https://whatnot.com/"
434444
| v -> v),
435445
System.Uri (
436446
("/v2/gymSessions/member"
437-
+ (if "/v2/gymSessions/member".IndexOf (char 63) >= 0 then
438-
"&"
447+
+ (if queryString = "" then
448+
""
439449
else
440-
"?")
441-
+ "fromDate="
442-
+ ((fromDate.ToString "yyyy-MM-dd") |> System.Uri.EscapeDataString)
443-
+ "&toDate="
444-
+ ((toDate.ToString "yyyy-MM-dd") |> System.Uri.EscapeDataString)),
450+
((if "/v2/gymSessions/member".IndexOf (char 63) >= 0 then
451+
"&"
452+
else
453+
"?")
454+
+ queryString))),
445455
System.UriKind.Relative
446456
)
447457
)
@@ -478,21 +488,31 @@ module PureGymApi =
478488
async {
479489
let! ct = Async.CancellationToken
480490

491+
let queryString =
492+
[
493+
[
494+
"fromDate=" + ((fromDate.ToString "yyyy-MM-dd") |> System.Uri.EscapeDataString)
495+
]
496+
[ "toDate=" + ((toDate.ToString "yyyy-MM-dd") |> System.Uri.EscapeDataString) ]
497+
]
498+
|> List.concat
499+
|> String.concat "&"
500+
481501
let uri =
482502
System.Uri (
483503
(match client.BaseAddress with
484504
| null -> System.Uri "https://whatnot.com/"
485505
| v -> v),
486506
System.Uri (
487507
("/v2/gymSessions/member?foo=1"
488-
+ (if "/v2/gymSessions/member?foo=1".IndexOf (char 63) >= 0 then
489-
"&"
508+
+ (if queryString = "" then
509+
""
490510
else
491-
"?")
492-
+ "fromDate="
493-
+ ((fromDate.ToString "yyyy-MM-dd") |> System.Uri.EscapeDataString)
494-
+ "&toDate="
495-
+ ((toDate.ToString "yyyy-MM-dd") |> System.Uri.EscapeDataString)),
511+
((if "/v2/gymSessions/member?foo=1".IndexOf (char 63) >= 0 then
512+
"&"
513+
else
514+
"?")
515+
+ queryString))),
496516
System.UriKind.Relative
497517
)
498518
)
@@ -1967,6 +1987,253 @@ open System.Net
19671987
open System.Net.Http
19681988
open RestEase
19691989

1990+
/// Module for constructing a REST client.
1991+
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix) ; RequireQualifiedAccess>]
1992+
module ApiWithListQuery =
1993+
/// Create a REST client.
1994+
let make (client : System.Net.Http.HttpClient) : IApiWithListQuery =
1995+
{ new IApiWithListQuery with
1996+
member _.GetWithListQuery (tags : string list, limit : int, ct : CancellationToken option) =
1997+
async {
1998+
let! ct = Async.CancellationToken
1999+
2000+
let queryString =
2001+
[
2002+
tags
2003+
|> List.map (fun queryParam ->
2004+
"tag=" + ((queryParam.ToString ()) |> System.Uri.EscapeDataString)
2005+
)
2006+
[ "limit=" + ((limit.ToString ()) |> System.Uri.EscapeDataString) ]
2007+
]
2008+
|> List.concat
2009+
|> String.concat "&"
2010+
2011+
let uri =
2012+
System.Uri (
2013+
(match client.BaseAddress with
2014+
| null -> System.Uri "https://whatnot.com/"
2015+
| v -> v),
2016+
System.Uri (
2017+
("endpoint"
2018+
+ (if queryString = "" then
2019+
""
2020+
else
2021+
((if "endpoint".IndexOf (char 63) >= 0 then "&" else "?") + queryString))),
2022+
System.UriKind.Relative
2023+
)
2024+
)
2025+
2026+
use httpMessage =
2027+
new System.Net.Http.HttpRequestMessage (
2028+
Method = System.Net.Http.HttpMethod.Get,
2029+
RequestUri = uri
2030+
)
2031+
2032+
let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask
2033+
let response = response.EnsureSuccessStatusCode ()
2034+
use response = response
2035+
let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask
2036+
return responseString
2037+
}
2038+
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct))
2039+
2040+
member _.GetWithSoleListQuery (tags : string list, ct : CancellationToken option) =
2041+
async {
2042+
let! ct = Async.CancellationToken
2043+
2044+
let queryString =
2045+
[
2046+
tags
2047+
|> List.map (fun queryParam ->
2048+
"tag=" + ((queryParam.ToString ()) |> System.Uri.EscapeDataString)
2049+
)
2050+
]
2051+
|> List.concat
2052+
|> String.concat "&"
2053+
2054+
let uri =
2055+
System.Uri (
2056+
(match client.BaseAddress with
2057+
| null -> System.Uri "https://whatnot.com/"
2058+
| v -> v),
2059+
System.Uri (
2060+
("endpoint"
2061+
+ (if queryString = "" then
2062+
""
2063+
else
2064+
((if "endpoint".IndexOf (char 63) >= 0 then "&" else "?") + queryString))),
2065+
System.UriKind.Relative
2066+
)
2067+
)
2068+
2069+
use httpMessage =
2070+
new System.Net.Http.HttpRequestMessage (
2071+
Method = System.Net.Http.HttpMethod.Get,
2072+
RequestUri = uri
2073+
)
2074+
2075+
let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask
2076+
let response = response.EnsureSuccessStatusCode ()
2077+
use response = response
2078+
let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask
2079+
return responseString
2080+
}
2081+
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct))
2082+
2083+
member _.GetWithArrayQuery (ids : int[], ct : CancellationToken option) =
2084+
async {
2085+
let! ct = Async.CancellationToken
2086+
2087+
let queryString =
2088+
[
2089+
ids
2090+
|> Seq.map (fun queryParam ->
2091+
"id=" + ((queryParam.ToString ()) |> System.Uri.EscapeDataString)
2092+
)
2093+
|> List.ofSeq
2094+
]
2095+
|> List.concat
2096+
|> String.concat "&"
2097+
2098+
let uri =
2099+
System.Uri (
2100+
(match client.BaseAddress with
2101+
| null -> System.Uri "https://whatnot.com/"
2102+
| v -> v),
2103+
System.Uri (
2104+
("endpoint"
2105+
+ (if queryString = "" then
2106+
""
2107+
else
2108+
((if "endpoint".IndexOf (char 63) >= 0 then "&" else "?") + queryString))),
2109+
System.UriKind.Relative
2110+
)
2111+
)
2112+
2113+
use httpMessage =
2114+
new System.Net.Http.HttpRequestMessage (
2115+
Method = System.Net.Http.HttpMethod.Get,
2116+
RequestUri = uri
2117+
)
2118+
2119+
let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask
2120+
let response = response.EnsureSuccessStatusCode ()
2121+
use response = response
2122+
let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask
2123+
return responseString
2124+
}
2125+
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct))
2126+
}
2127+
namespace PureGym
2128+
2129+
open System
2130+
open System.Threading
2131+
open System.Threading.Tasks
2132+
open System.IO
2133+
open System.Net
2134+
open System.Net.Http
2135+
open RestEase
2136+
2137+
/// Module for constructing a REST client.
2138+
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix) ; RequireQualifiedAccess>]
2139+
module ApiShadowingGeneratedNames =
2140+
/// Create a REST client.
2141+
let make (client : System.Net.Http.HttpClient) : IApiShadowingGeneratedNames =
2142+
{ new IApiShadowingGeneratedNames with
2143+
member _.GetWithPathNamedQueryString (queryString : string, limit : int, ct : CancellationToken option) =
2144+
async {
2145+
let! ct = Async.CancellationToken
2146+
2147+
let queryString1 =
2148+
[ [ "limit=" + ((limit.ToString ()) |> System.Uri.EscapeDataString) ] ]
2149+
|> List.concat
2150+
|> String.concat "&"
2151+
2152+
let uri =
2153+
System.Uri (
2154+
(match client.BaseAddress with
2155+
| null -> System.Uri "https://whatnot.com/"
2156+
| v -> v),
2157+
System.Uri (
2158+
("endpoint/{queryString}"
2159+
.Replace ("{queryString}", queryString.ToString () |> System.Uri.EscapeDataString)
2160+
+ (if queryString1 = "" then
2161+
""
2162+
else
2163+
((if "endpoint/{queryString}".IndexOf (char 63) >= 0 then
2164+
"&"
2165+
else
2166+
"?")
2167+
+ queryString1))),
2168+
System.UriKind.Relative
2169+
)
2170+
)
2171+
2172+
use httpMessage =
2173+
new System.Net.Http.HttpRequestMessage (
2174+
Method = System.Net.Http.HttpMethod.Get,
2175+
RequestUri = uri
2176+
)
2177+
2178+
let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask
2179+
let response = response.EnsureSuccessStatusCode ()
2180+
use response = response
2181+
let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask
2182+
return responseString
2183+
}
2184+
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct))
2185+
2186+
member _.GetWithQueryNamedQueryString (queryString : string, limit : int, ct : CancellationToken option) =
2187+
async {
2188+
let! ct = Async.CancellationToken
2189+
2190+
let queryString1 =
2191+
[
2192+
[ "queryString=" + ((queryString.ToString ()) |> System.Uri.EscapeDataString) ]
2193+
[ "limit=" + ((limit.ToString ()) |> System.Uri.EscapeDataString) ]
2194+
]
2195+
|> List.concat
2196+
|> String.concat "&"
2197+
2198+
let uri =
2199+
System.Uri (
2200+
(match client.BaseAddress with
2201+
| null -> System.Uri "https://whatnot.com/"
2202+
| v -> v),
2203+
System.Uri (
2204+
("endpoint"
2205+
+ (if queryString1 = "" then
2206+
""
2207+
else
2208+
((if "endpoint".IndexOf (char 63) >= 0 then "&" else "?") + queryString1))),
2209+
System.UriKind.Relative
2210+
)
2211+
)
2212+
2213+
use httpMessage =
2214+
new System.Net.Http.HttpRequestMessage (
2215+
Method = System.Net.Http.HttpMethod.Get,
2216+
RequestUri = uri
2217+
)
2218+
2219+
let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask
2220+
let response = response.EnsureSuccessStatusCode ()
2221+
use response = response
2222+
let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask
2223+
return responseString
2224+
}
2225+
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct))
2226+
}
2227+
namespace PureGym
2228+
2229+
open System
2230+
open System.Threading
2231+
open System.Threading.Tasks
2232+
open System.IO
2233+
open System.Net
2234+
open System.Net.Http
2235+
open RestEase
2236+
19702237
/// Module for constructing a REST client.
19712238
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix) ; RequireQualifiedAccess>]
19722239
module ClientWithStringBody =

ConsumePlugin/RestApiExample.fs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,36 @@ type IClientWithJsonBodyOverridden =
226226
?ct : CancellationToken ->
227227
Task<string>
228228

229+
[<WoofWare.Myriad.Plugins.HttpClient>]
230+
[<BaseAddress "https://whatnot.com">]
231+
type IApiWithListQuery =
232+
// A list-typed query parameter contributes one key=value pair per element.
233+
[<Get "endpoint">]
234+
abstract GetWithListQuery :
235+
[<Query "tag">] tags : string list * [<Query "limit">] limit : int * ?ct : CancellationToken -> Task<string>
236+
237+
[<Get "endpoint">]
238+
abstract GetWithSoleListQuery : [<Query "tag">] tags : string list * ?ct : CancellationToken -> Task<string>
239+
240+
[<Get "endpoint">]
241+
abstract GetWithArrayQuery : [<Query "id">] ids : int[] * ?ct : CancellationToken -> Task<string>
242+
243+
[<WoofWare.Myriad.Plugins.HttpClient>]
244+
[<BaseAddress "https://whatnot.com">]
245+
type IApiShadowingGeneratedNames =
246+
// The generator emits a local binding holding the serialised query string. That binding
247+
// must not shadow a user parameter which happens to share its name.
248+
[<Get "endpoint/{queryString}">]
249+
abstract GetWithPathNamedQueryString :
250+
[<Path "queryString">] queryString : string * [<Query "limit">] limit : int * ?ct : CancellationToken ->
251+
Task<string>
252+
253+
// Same, but the colliding parameter is itself a query parameter.
254+
[<Get "endpoint">]
255+
abstract GetWithQueryNamedQueryString :
256+
[<Query "queryString">] queryString : string * [<Query "limit">] limit : int * ?ct : CancellationToken ->
257+
Task<string>
258+
229259
[<WoofWare.Myriad.Plugins.HttpClient>]
230260
type IClientWithStringBody =
231261
// As a POST request of a bare string body, we don't override the Content-Type.

0 commit comments

Comments
 (0)