Skip to content

Commit 02f72b8

Browse files
committed
query param support deepObject
1 parent ca35820 commit 02f72b8

Some content is hidden

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

52 files changed

+1404
-57
lines changed

src/client.jl

Lines changed: 26 additions & 4 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
@@ -292,7 +292,12 @@ function set_header_content_type(ctx::Ctx, ctypes::Vector{String})
292292
end
293293

294294
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=",")
295+
function set_param(params::Dict{String,String}, name::String, value; collection_format=",", style="form", is_explode=false)
296+
deep_explode = style == "deepObject" && is_explode
297+
if deep_explode
298+
merge!(params, deep_object_serialize(Dict(name=>value)))
299+
return nothing
300+
end
296301
if isa(value, Dict)
297302
# implements the default serialization (style=form, explode=true, location=queryparams)
298303
# as mentioned in https://swagger.io/docs/specification/serialization/
@@ -789,7 +794,7 @@ function storefile(api_call::Function;
789794
folder::AbstractString = pwd(),
790795
filename::Union{String,Nothing} = nothing,
791796
)::Tuple{Any,ApiResponse,String}
792-
797+
793798
result, http_response = api_call()
794799

795800
if isnothing(filename)
@@ -828,4 +833,21 @@ function extract_filename(resp::Downloads.Response)::String
828833
return string("response", extension_from_mime(MIME(content_type_str)))
829834
end
830835

836+
function deep_object_serialize(dict::Dict, parent_key::String = "")
837+
parts = Pair[]
838+
for (key, value) in dict
839+
new_key = parent_key == "" ? key : "$parent_key[$key]"
840+
if isa(value, Dict)
841+
append!(parts, collect(deep_object_serialize(value, new_key)))
842+
elseif isa(value, Vector)
843+
for (i, v) in enumerate(value)
844+
push!(parts, "$new_key[$(i-1)]"=>"$v")
845+
end
846+
else
847+
push!(parts, "$new_key"=>"$value")
848+
end
849+
end
850+
return Dict(parts)
851+
end
852+
831853
end # module Clients

src/json.jl

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

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

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
60+
from_json(::Type{Union{Nothing,T}}, json::Dict{String,Any}; stylectx=nothing) where {T} = from_json(T, json; stylectx)
61+
from_json(::Type{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 <: Dict} = convert(T, json)
63+
from_json(::Type{T}, j::Dict{String,Any}; stylectx=nothing) where {T <: String} = to_json(j)
64+
from_json(::Type{Any}, j::Dict{String,Any}; stylectx=nothing) = j
65+
from_json(::Type{Vector{T}}, j::Vector{Any}; stylectx=nothing) where {T} = j
66+
67+
function from_json(::Type{Vector{T}}, json::Dict{String, Any}; stylectx=nothing) where {T}
68+
if !isnothing(stylectx) && is_deep_explode(stylectx)
69+
cvt = deep_object_to_array(json)
70+
if isa(cvt, Vector)
71+
return from_json(Vector{T}, cvt; stylectx)
72+
else
73+
return from_json(T, json; stylectx)
74+
end
75+
else
76+
return from_json(T, json; stylectx)
77+
end
78+
end
4479

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

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}
84+
from_json(::Type{T}, val::Union{String,Real};stylectx=nothing) where {T <: UnionAPIModel} = T(val)
85+
function from_json(o::T, val::Union{String,Real};stylectx=nothing) where {T <: UnionAPIModel}
5186
o.value = val
5287
return o
5388
end
5489

55-
function from_json(o::T, json::Dict{String,Any}) where {T <: APIModel}
90+
function from_json(o::T, json::Dict{String,Any};stylectx=nothing) where {T <: APIModel}
5691
jsonkeys = [Symbol(k) for k in keys(json)]
5792
for name in intersect(propertynames(o), jsonkeys)
58-
from_json(o, name, json[String(name)])
93+
from_json(o, name, json[String(name)];stylectx)
5994
end
6095
return o
6196
end
6297

63-
function from_json(o::T, name::Symbol, json::Dict{String,Any}) where {T <: APIModel}
98+
function from_json(o::T, name::Symbol, json::Dict{String,Any};stylectx=nothing) where {T <: APIModel}
6499
ftype = (T <: UnionAPIModel) ? property_type(T, name, json) : property_type(T, name)
65-
fval = from_json(ftype, json)
100+
fval = from_json(ftype, json; stylectx)
66101
setfield!(o, name, convert(ftype, fval))
67102
return o
68103
end
69104

70-
function from_json(o::T, name::Symbol, v) where {T <: APIModel}
105+
function from_json(o::T, name::Symbol, v; stylectx=nothing) where {T <: APIModel}
71106
ftype = (T <: UnionAPIModel) ? property_type(T, name, Dict{String,Any}()) : property_type(T, name)
107+
atype = isa(ftype, Union) ? ((ftype.a === Nothing) ? ftype.b : ftype.a) : ftype
72108
if ftype === Any
73109
setfield!(o, name, v)
74110
elseif ZonedDateTime <: ftype
@@ -80,13 +116,15 @@ function from_json(o::T, name::Symbol, v) where {T <: APIModel}
80116
elseif String <: ftype && isa(v, Real)
81117
# string numbers can have format specifiers that allow numbers, ensure they are converted to strings
82118
setfield!(o, name, string(v))
119+
elseif atype <: Real && isa(v, AbstractString)
120+
setfield!(o, name, parse(atype, v))
83121
else
84122
setfield!(o, name, convert(ftype, v))
85123
end
86124
return o
87125
end
88126

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

@@ -111,7 +149,7 @@ function from_json(o::T, name::Symbol, v::Vector) where {T <: APIModel}
111149
if (vtype <: Vector) && (veltype <: OpenAPI.UnionAPIModel)
112150
vec = veltype[]
113151
for vecelem in v
114-
push!(vec, from_json(veltype(), :value, vecelem))
152+
push!(vec, from_json(veltype(), :value, vecelem;stylectx))
115153
end
116154
setfield!(o, name, vec)
117155
elseif (vtype <: Vector) && (veltype <: OpenAPI.APIModel)
@@ -129,7 +167,7 @@ function from_json(o::T, name::Symbol, v::Vector) where {T <: APIModel}
129167
return o
130168
end
131169

132-
function from_json(o::T, name::Symbol, ::Nothing) where {T <: APIModel}
170+
function from_json(o::T, name::Symbol, ::Nothing;stylectx=nothing) where {T <: APIModel}
133171
setfield!(o, name, nothing)
134172
return o
135-
end
173+
end

src/server.jl

Lines changed: 68 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,38 @@ 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 convert_to_dict(params::Vector{Param})::Dict{String, Any}
48+
deserialized_dict = Dict{String, Any}()
49+
50+
for param in params
51+
current = deserialized_dict
52+
for part in param.keylist[1:end-1]
53+
current = get!(current, part, Dict{String, Any}())
54+
end
55+
current[param.keylist[end]] = param.value
56+
end
57+
return deserialized_dict
58+
end
59+
60+
function deep_dict_repr(qp::Dict)
61+
convert_to_dict(parse_query_dict(qp))
62+
end
63+
3264
function get_param(source::Dict, name::String, required::Bool)
3365
val = get(source, name, nothing)
3466
if required && isnothing(val)
@@ -48,36 +80,50 @@ function get_param(source::Vector{HTTP.Forms.Multipart}, name::String, required:
4880
end
4981
end
5082

51-
52-
function to_param_type(::Type{T}, strval::String) where {T <: Number}
83+
function to_param_type(::Type{T}, strval::String; stylectx=nothing) where {T <: Number}
5384
parse(T, strval)
5485
end
5586

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
87+
to_param_type(::Type{T}, val::T; stylectx=nothing) where {T} = val
88+
to_param_type(::Type{T}, ::Nothing; stylectx=nothing) where {T} = nothing
89+
to_param_type(::Type{String}, val::Vector{UInt8}; stylectx=nothing) = String(copy(val))
90+
to_param_type(::Type{Vector{UInt8}}, val::String; stylectx=nothing) = convert(Vector{UInt8}, copy(codeunits(val)))
91+
to_param_type(::Type{Vector{T}}, val::Vector{T}, _collection_format::Union{String,Nothing}; stylectx=nothing) where {T} = val
92+
to_param_type(::Type{Vector{T}}, json::Vector{Any}; stylectx=nothing) where {T} = [to_param_type(T, x; stylectx) for x in json]
93+
94+
function to_param_type(::Type{Vector{T}}, json::Dict{String, Any}; stylectx=nothing) where {T}
95+
if !isnothing(stylectx) && is_deep_explode(stylectx)
96+
cvt = deep_object_to_array(json)
97+
if isa(cvt, Vector)
98+
to_param_type(Vector{T}, cvt; stylectx)
99+
end
100+
end
101+
error("Unable to convert $json to $(Vector{T})")
102+
end
61103

62-
function to_param_type(::Type{T}, strval::String) where {T <: APIModel}
63-
from_json(T, JSON.parse(strval))
104+
function to_param_type(::Type{T}, strval::String; stylectx=nothing) where {T <: APIModel}
105+
from_json(T, JSON.parse(strval); stylectx)
64106
end
65107

66-
function to_param_type(::Type{T}, json::Dict{String,Any}) where {T <: APIModel}
67-
from_json(T, json)
108+
function to_param_type(::Type{T}, json::Dict{String,Any}; stylectx=nothing) where {T <: APIModel}
109+
from_json(T, json; stylectx)
68110
end
69111

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

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

80-
function to_param(T, source::Dict, name::String; required::Bool=false, collection_format::Union{String,Nothing}=",", multipart::Bool=false, isfile::Bool=false)
122+
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)
123+
deep_explode = style == "deepObject" && is_explode
124+
if deep_explode
125+
source = deep_dict_repr(source)
126+
end
81127
param = get_param(source, name, required)
82128
if param === nothing
83129
return nothing
@@ -86,10 +132,13 @@ function to_param(T, source::Dict, name::String; required::Bool=false, collectio
86132
# param is a Multipart
87133
param = isfile ? param.data : String(param.data)
88134
end
135+
if deep_explode
136+
return to_param_type(T, param; stylectx=StyleCtx(style, is_explode))
137+
end
89138
if T <: Vector
90-
return to_param_type(T, param, collection_format)
139+
to_param_type(T, param, collection_format)
91140
else
92-
return to_param_type(T, param)
141+
to_param_type(T, param)
93142
end
94143
end
95144

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)