Skip to content

Commit 4c7260c

Browse files
vdayanandtanmaykm
andauthored
feat: support for query parameter serialization style "deepObject" (#78)
* query param support deepObject * fix * remove stray comment * Update src/client.jl Co-authored-by: Tanmay Mohapatra <[email protected]> * Update src/json.jl Co-authored-by: Tanmay Mohapatra <[email protected]> * Update src/server.jl Co-authored-by: Tanmay Mohapatra <[email protected]> * fix * fix * fix location --------- Co-authored-by: Tanmay Mohapatra <[email protected]>
1 parent ca35820 commit 4c7260c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1363
-58
lines changed

src/client.jl

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ end
7777

7878
function get_api_return_type(return_types::Dict{Regex,Type}, ::Nothing, response_data::String)
7979
# this is the async case, where we do not have the response code yet
80-
# in such cases we look for the 200 response code
80+
# in such cases we look for the 200 response code
8181
return get_api_return_type(return_types, 200, response_data)
8282
end
8383
function get_api_return_type(return_types::Dict{Regex,Type}, response_code::Integer, response_data::String)
@@ -191,7 +191,7 @@ set_user_agent(client::Client, ua::String) = set_header(client, "User-Agent", ua
191191
Set the Cookie header to be sent with all API calls.
192192
"""
193193
set_cookie(client::Client, ck::String) = set_header(client, "Cookie", ck)
194-
194+
195195
"""
196196
set_header(client::Client, name::String, value::String)
197197
@@ -291,8 +291,26 @@ function set_header_content_type(ctx::Ctx, ctypes::Vector{String})
291291
return nothing
292292
end
293293

294-
set_param(params::Dict{String,String}, name::String, value::Nothing; collection_format=",") = nothing
295-
function set_param(params::Dict{String,String}, name::String, value; collection_format=",")
294+
set_param(params::Dict{String,String}, name::String, value::Nothing; collection_format=",", style="form", location=:query, is_explode=default_param_explode(style)) = nothing
295+
# Choose the default collection_format based on spec.
296+
# Overriding it may not match the spec and there's no check.
297+
# But we do not prevent it to allow for wiggle room, since there are many interpretations in the wild over the loosely defined spec around this.
298+
# TODO: `default_param_explode` needs to be improved to handle location too (query, header, cookie...)
299+
function default_param_explode(style::String)
300+
if style == "deepObject"
301+
true
302+
elseif style == "form"
303+
true
304+
else
305+
false
306+
end
307+
end
308+
function set_param(params::Dict{String,String}, name::String, value; collection_format=",", style="form", location::Symbol=:query, is_explode=default_param_explode(style))
309+
deep_explode = style == "deepObject" && is_explode
310+
if deep_explode
311+
merge!(params, deep_object_serialize(Dict(name=>value)))
312+
return nothing
313+
end
296314
if isa(value, Dict)
297315
# implements the default serialization (style=form, explode=true, location=queryparams)
298316
# as mentioned in https://swagger.io/docs/specification/serialization/
@@ -789,7 +807,7 @@ function storefile(api_call::Function;
789807
folder::AbstractString = pwd(),
790808
filename::Union{String,Nothing} = nothing,
791809
)::Tuple{Any,ApiResponse,String}
792-
810+
793811
result, http_response = api_call()
794812

795813
if isnothing(filename)
@@ -828,4 +846,21 @@ function extract_filename(resp::Downloads.Response)::String
828846
return string("response", extension_from_mime(MIME(content_type_str)))
829847
end
830848

849+
function deep_object_serialize(dict::Dict, parent_key::String = "")
850+
parts = Pair[]
851+
for (key, value) in dict
852+
new_key = parent_key == "" ? key : "$parent_key[$key]"
853+
if isa(value, Dict)
854+
append!(parts, collect(deep_object_serialize(value, new_key)))
855+
elseif isa(value, Vector)
856+
for (i, v) in enumerate(value)
857+
push!(parts, "$new_key[$(i-1)]"=>"$v")
858+
end
859+
else
860+
push!(parts, "$new_key"=>"$value")
861+
end
862+
end
863+
return Dict(parts)
864+
end
865+
831866
end # module Clients

src/json.jl

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,41 +34,78 @@ function lower(o::T) where {T<:UnionAPIModel}
3434
end
3535
end
3636

37+
struct StyleCtx
38+
location::Symbol
39+
name::String
40+
is_explode::Bool
41+
end
42+
43+
is_deep_explode(sctx::StyleCtx) = sctx.name == "deepObject" && sctx.is_explode
44+
45+
function deep_object_to_array(src::Dict)
46+
keys_are_int = all(key -> occursin(r"^\d+$", key), keys(src))
47+
if keys_are_int
48+
sorted_keys = sort(collect(keys(src)), by=x->parse(Int, x))
49+
final = []
50+
for key in sorted_keys
51+
push!(final, src[key])
52+
end
53+
return final
54+
else
55+
src
56+
end
57+
end
58+
3759
to_json(o) = JSON.json(o)
3860

39-
from_json(::Type{Union{Nothing,T}}, json::Dict{String,Any}) where {T} = from_json(T, json)
40-
from_json(::Type{T}, json::Dict{String,Any}) where {T} = from_json(T(), json)
41-
from_json(::Type{T}, json::Dict{String,Any}) where {T <: Dict} = convert(T, json)
42-
from_json(::Type{T}, j::Dict{String,Any}) where {T <: String} = to_json(j)
43-
from_json(::Type{Any}, j::Dict{String,Any}) = j
61+
from_json(::Type{Union{Nothing,T}}, json::Dict{String,Any}; stylectx=nothing) where {T} = from_json(T, json; stylectx)
62+
from_json(::Type{T}, json::Dict{String,Any}; stylectx=nothing) where {T} = from_json(T(), json; stylectx)
63+
from_json(::Type{T}, json::Dict{String,Any}; stylectx=nothing) where {T <: Dict} = convert(T, json)
64+
from_json(::Type{T}, j::Dict{String,Any}; stylectx=nothing) where {T <: String} = to_json(j)
65+
from_json(::Type{Any}, j::Dict{String,Any}; stylectx=nothing) = j
66+
from_json(::Type{Vector{T}}, j::Vector{Any}; stylectx=nothing) where {T} = j
67+
68+
function from_json(::Type{Vector{T}}, json::Dict{String, Any}; stylectx=nothing) where {T}
69+
if !isnothing(stylectx) && is_deep_explode(stylectx)
70+
cvt = deep_object_to_array(json)
71+
if isa(cvt, Vector)
72+
return from_json(Vector{T}, cvt; stylectx)
73+
else
74+
return from_json(T, json; stylectx)
75+
end
76+
else
77+
return from_json(T, json; stylectx)
78+
end
79+
end
4480

45-
function from_json(o::T, json::Dict{String,Any}) where {T <: UnionAPIModel}
46-
return from_json(o, :value, json)
81+
function from_json(o::T, json::Dict{String,Any};stylectx=nothing) where {T <: UnionAPIModel}
82+
return from_json(o, :value, json;stylectx)
4783
end
4884

49-
from_json(::Type{T}, val::Union{String,Real}) where {T <: UnionAPIModel} = T(val)
50-
function from_json(o::T, val::Union{String,Real}) where {T <: UnionAPIModel}
85+
from_json(::Type{T}, val::Union{String,Real};stylectx=nothing) where {T <: UnionAPIModel} = T(val)
86+
function from_json(o::T, val::Union{String,Real};stylectx=nothing) where {T <: UnionAPIModel}
5187
o.value = val
5288
return o
5389
end
5490

55-
function from_json(o::T, json::Dict{String,Any}) where {T <: APIModel}
91+
function from_json(o::T, json::Dict{String,Any};stylectx=nothing) where {T <: APIModel}
5692
jsonkeys = [Symbol(k) for k in keys(json)]
5793
for name in intersect(propertynames(o), jsonkeys)
58-
from_json(o, name, json[String(name)])
94+
from_json(o, name, json[String(name)];stylectx)
5995
end
6096
return o
6197
end
6298

63-
function from_json(o::T, name::Symbol, json::Dict{String,Any}) where {T <: APIModel}
99+
function from_json(o::T, name::Symbol, json::Dict{String,Any};stylectx=nothing) where {T <: APIModel}
64100
ftype = (T <: UnionAPIModel) ? property_type(T, name, json) : property_type(T, name)
65-
fval = from_json(ftype, json)
101+
fval = from_json(ftype, json; stylectx)
66102
setfield!(o, name, convert(ftype, fval))
67103
return o
68104
end
69105

70-
function from_json(o::T, name::Symbol, v) where {T <: APIModel}
106+
function from_json(o::T, name::Symbol, v; stylectx=nothing) where {T <: APIModel}
71107
ftype = (T <: UnionAPIModel) ? property_type(T, name, Dict{String,Any}()) : property_type(T, name)
108+
atype = isa(ftype, Union) ? ((ftype.a === Nothing) ? ftype.b : ftype.a) : ftype
72109
if ftype === Any
73110
setfield!(o, name, v)
74111
elseif ZonedDateTime <: ftype
@@ -80,13 +117,15 @@ function from_json(o::T, name::Symbol, v) where {T <: APIModel}
80117
elseif String <: ftype && isa(v, Real)
81118
# string numbers can have format specifiers that allow numbers, ensure they are converted to strings
82119
setfield!(o, name, string(v))
120+
elseif atype <: Real && isa(v, AbstractString)
121+
setfield!(o, name, parse(atype, v))
83122
else
84123
setfield!(o, name, convert(ftype, v))
85124
end
86125
return o
87126
end
88127

89-
function from_json(o::T, name::Symbol, v::Vector) where {T <: APIModel}
128+
function from_json(o::T, name::Symbol, v::Vector; stylectx=nothing) where {T <: APIModel}
90129
# in Julia we can not support JSON null unless the element type is explicitly set to support it
91130
ftype = property_type(T, name)
92131

@@ -111,7 +150,7 @@ function from_json(o::T, name::Symbol, v::Vector) where {T <: APIModel}
111150
if (vtype <: Vector) && (veltype <: OpenAPI.UnionAPIModel)
112151
vec = veltype[]
113152
for vecelem in v
114-
push!(vec, from_json(veltype(), :value, vecelem))
153+
push!(vec, from_json(veltype(), :value, vecelem;stylectx))
115154
end
116155
setfield!(o, name, vec)
117156
elseif (vtype <: Vector) && (veltype <: OpenAPI.APIModel)
@@ -129,7 +168,7 @@ function from_json(o::T, name::Symbol, v::Vector) where {T <: APIModel}
129168
return o
130169
end
131170

132-
function from_json(o::T, name::Symbol, ::Nothing) where {T <: APIModel}
171+
function from_json(o::T, name::Symbol, ::Nothing;stylectx=nothing) where {T <: APIModel}
133172
setfield!(o, name, nothing)
134173
return o
135-
end
174+
end

src/server.jl

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module Servers
33
using JSON
44
using HTTP
55

6-
import ..OpenAPI: APIModel, ValidationException, from_json, to_json
6+
import ..OpenAPI: APIModel, ValidationException, from_json, to_json, deep_object_to_array, StyleCtx, is_deep_explode
77

88
function middleware(impl, read, validate, invoke;
99
init=nothing,
@@ -29,6 +29,36 @@ end
2929
##############################
3030
# server parameter conversions
3131
##############################
32+
struct Param
33+
keylist::Vector{String}
34+
value::String
35+
end
36+
37+
function parse_query_dict(query_dict::Dict{String, String})::Vector{Param}
38+
params = Vector{Param}()
39+
for (key, value) in query_dict
40+
keylist = replace.(split(key, "["), "]"=>"")
41+
push!(params, Param(keylist, value))
42+
end
43+
44+
return params
45+
end
46+
47+
function deep_dict_repr(qp::Dict)
48+
params = parse_query_dict(qp)
49+
deserialized_dict = Dict{String, Any}()
50+
for param in params
51+
current = deserialized_dict
52+
for part in param.keylist[1:end-1]
53+
current = get!(current, part) do
54+
return Dict{String, Any}()
55+
end
56+
end
57+
current[param.keylist[end]] = param.value
58+
end
59+
return deserialized_dict
60+
end
61+
3262
function get_param(source::Dict, name::String, required::Bool)
3363
val = get(source, name, nothing)
3464
if required && isnothing(val)
@@ -48,36 +78,50 @@ function get_param(source::Vector{HTTP.Forms.Multipart}, name::String, required:
4878
end
4979
end
5080

51-
52-
function to_param_type(::Type{T}, strval::String) where {T <: Number}
81+
function to_param_type(::Type{T}, strval::String; stylectx=nothing) where {T <: Number}
5382
parse(T, strval)
5483
end
5584

56-
to_param_type(::Type{T}, val::T) where {T} = val
57-
to_param_type(::Type{T}, ::Nothing) where {T} = nothing
58-
to_param_type(::Type{String}, val::Vector{UInt8}) = String(copy(val))
59-
to_param_type(::Type{Vector{UInt8}}, val::String) = convert(Vector{UInt8}, copy(codeunits(val)))
60-
to_param_type(::Type{Vector{T}}, val::Vector{T}, _collection_format::Union{String,Nothing}) where {T} = val
85+
to_param_type(::Type{T}, val::T; stylectx=nothing) where {T} = val
86+
to_param_type(::Type{T}, ::Nothing; stylectx=nothing) where {T} = nothing
87+
to_param_type(::Type{String}, val::Vector{UInt8}; stylectx=nothing) = String(copy(val))
88+
to_param_type(::Type{Vector{UInt8}}, val::String; stylectx=nothing) = convert(Vector{UInt8}, copy(codeunits(val)))
89+
to_param_type(::Type{Vector{T}}, val::Vector{T}, _collection_format::Union{String,Nothing}; stylectx=nothing) where {T} = val
90+
to_param_type(::Type{Vector{T}}, json::Vector{Any}; stylectx=nothing) where {T} = [to_param_type(T, x; stylectx) for x in json]
91+
92+
function to_param_type(::Type{Vector{T}}, json::Dict{String, Any}; stylectx=nothing) where {T}
93+
if !isnothing(stylectx) && is_deep_explode(stylectx)
94+
cvt = deep_object_to_array(json)
95+
if isa(cvt, Vector)
96+
return to_param_type(Vector{T}, cvt; stylectx)
97+
end
98+
end
99+
error("Unable to convert $json to $(Vector{T})")
100+
end
61101

62-
function to_param_type(::Type{T}, strval::String) where {T <: APIModel}
63-
from_json(T, JSON.parse(strval))
102+
function to_param_type(::Type{T}, strval::String; stylectx=nothing) where {T <: APIModel}
103+
from_json(T, JSON.parse(strval); stylectx)
64104
end
65105

66-
function to_param_type(::Type{T}, json::Dict{String,Any}) where {T <: APIModel}
67-
from_json(T, json)
106+
function to_param_type(::Type{T}, json::Dict{String,Any}; stylectx=nothing) where {T <: APIModel}
107+
from_json(T, json; stylectx)
68108
end
69109

70-
function to_param_type(::Type{Vector{T}}, strval::String, delim::String) where {T}
110+
function to_param_type(::Type{Vector{T}}, strval::String, delim::String; stylectx=nothing) where {T}
71111
elems = string.(strip.(split(strval, delim)))
72-
return map(x->to_param_type(T, x), elems)
112+
return map(x->to_param_type(T, x; stylectx), elems)
73113
end
74114

75-
function to_param_type(::Type{Vector{T}}, strval::String) where {T}
115+
function to_param_type(::Type{Vector{T}}, strval::String; stylectx=nothing) where {T}
76116
elems = JSON.parse(strval)
77-
return map(x->to_param_type(T, x), elems)
117+
return map(x->to_param_type(T, x; stylectx), elems)
78118
end
79119

80-
function to_param(T, source::Dict, name::String; required::Bool=false, collection_format::Union{String,Nothing}=",", multipart::Bool=false, isfile::Bool=false)
120+
function to_param(T, source::Dict, name::String; required::Bool=false, collection_format::Union{String,Nothing}=",", multipart::Bool=false, isfile::Bool=false, style::String="form", is_explode::Bool=true, location=:query)
121+
deep_explode = style == "deepObject" && is_explode
122+
if deep_explode
123+
source = deep_dict_repr(source)
124+
end
81125
param = get_param(source, name, required)
82126
if param === nothing
83127
return nothing
@@ -86,10 +130,13 @@ function to_param(T, source::Dict, name::String; required::Bool=false, collectio
86130
# param is a Multipart
87131
param = isfile ? param.data : String(param.data)
88132
end
133+
if deep_explode
134+
return to_param_type(T, param; stylectx=StyleCtx(location, style, is_explode))
135+
end
89136
if T <: Vector
90-
return to_param_type(T, param, collection_format)
137+
to_param_type(T, param, collection_format)
91138
else
92-
return to_param_type(T, param)
139+
to_param_type(T, param)
93140
end
94141
end
95142

test/client/param_serialize.jl

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using OpenAPI.Clients: deep_object_serialize
2+
3+
@testset "Test deep_object_serialize" begin
4+
@testset "Single level object" begin
5+
dict = Dict("key1" => "value1", "key2" => "value2")
6+
expected = Dict("key1" => "value1", "key2" => "value2")
7+
@test deep_object_serialize(dict) == expected
8+
end
9+
10+
@testset "Nested object" begin
11+
dict = Dict("outer" => Dict("inner" => "value"))
12+
expected = Dict("outer[inner]" => "value")
13+
@test deep_object_serialize(dict) == expected
14+
end
15+
16+
@testset "Deeply nested object" begin
17+
dict = Dict("a" => Dict("b" => Dict("c" => Dict("d" => "value"))))
18+
expected = Dict("a[b][c][d]" => "value")
19+
@test deep_object_serialize(dict) == expected
20+
end
21+
22+
@testset "Multiple nested objects" begin
23+
dict = Dict("a" => Dict("b" => "value1", "c" => "value2"))
24+
expected = Dict("a[b]" => "value1", "a[c]" => "value2")
25+
@test deep_object_serialize(dict) == expected
26+
end
27+
28+
@testset "Dictionary represented array" begin
29+
dict = Dict("a" => ["value1", "value2"])
30+
expected = Dict("a[0]" => "value1", "a[1]" => "value2")
31+
@test deep_object_serialize(dict) == expected
32+
end
33+
34+
@testset "Mixed structure" begin
35+
dict = Dict("a" => Dict("b" => "value1", "c" => ["value2", "value3"]))
36+
expected = Dict("a[b]" => "value1", "a[c][0]" => "value2", "a[c][1]" => "value3")
37+
@test deep_object_serialize(dict) == expected
38+
end
39+
40+
@testset "Blank values" begin
41+
dict = Dict("a" => Dict("b" => "", "c" => ""))
42+
expected = Dict("a[b]" => "", "a[c]" => "")
43+
@test deep_object_serialize(dict) == expected
44+
end
45+
end

test/client/runtests.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ include("openapigenerator_petstore_v3/runtests.jl")
1111

1212
function runtests(; skip_petstore=false, test_file_upload=false)
1313
@testset "Client" begin
14+
@testset "deepObj query param serialization" begin
15+
include("client/param_serialize.jl")
16+
end
1417
@testset "Utils" begin
1518
test_longpoll_exception_check()
1619
test_request_interrupted_exception_check()
@@ -52,4 +55,4 @@ function run_openapigenerator_tests(; test_file_upload=false)
5255
end
5356
end
5457

55-
end # module OpenAPIClientTests
58+
end # module OpenAPIClientTests

0 commit comments

Comments
 (0)