Skip to content

Commit 3ce50d3

Browse files
committed
add support for JSON.jl 1.x
This adds support for JSON.jl 1.x. The package will now be compatible with both 0.x and 1.x versions of JSON.
1 parent f553a42 commit 3ce50d3

File tree

7 files changed

+211
-24
lines changed

7 files changed

+211
-24
lines changed

Project.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ keywords = ["Swagger", "OpenAPI", "REST"]
44
license = "MIT"
55
desc = "OpenAPI server and client helper for Julia"
66
authors = ["JuliaHub Inc."]
7-
version = "0.2.1"
7+
version = "0.2.2"
88

99
[deps]
1010
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
@@ -22,8 +22,8 @@ p7zip_jll = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
2222
[compat]
2323
Downloads = "1"
2424
HTTP = "1"
25-
JSON = "0.20, 0.21"
26-
LibCURL = "0.6"
25+
JSON = "0.20, 0.21, 1"
26+
LibCURL = "0.6, 1"
2727
MIMEs = "0.1, 1"
2828
MbedTLS = "0.6.8, 0.7, 1"
2929
TimeZones = "1"

src/OpenAPI.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ using p7zip_jll
77
import Base: getindex, keys, length, iterate, hasproperty
88
import JSON: lower
99

10+
11+
const _JSON_PARSE_ISROOT_SUPPORTED = try; JSON.parse("1 "; isroot=false); true; catch; false; end
12+
13+
if _JSON_PARSE_ISROOT_SUPPORTED
14+
_json_parse(io_or_str) = JSON.parse(io_or_str; isroot=false)
15+
else
16+
_json_parse(io_or_str) = JSON.parse(io_or_str)
17+
end
18+
1019
include("commontypes.jl")
1120
include("datetime.jl")
1221
include("val.jl")

src/client.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ using MIMEs
1212

1313
import Base: convert, show, summary, getproperty, setproperty!, iterate
1414
import ..OpenAPI: APIModel, UnionAPIModel, OneOfAPIModel, AnyOfAPIModel, APIClientImpl, OpenAPIException, InvocationException, to_json, from_json, validate_property, property_type
15-
import ..OpenAPI: str2zoneddatetime, str2datetime, str2date
15+
import ..OpenAPI: str2zoneddatetime, str2datetime, str2date, _json_parse
1616

1717
include("client/clienttypes.jl")
1818
include("client/chunk_readers.jl")
@@ -159,8 +159,8 @@ response(::Type{DateTime}, data) = str2datetime(data)
159159
response(::Type{Date}, data) = str2date(data)
160160

161161
response(::Type{T}, data) where {T} = convert(T, data)
162-
response(::Type{T}, data::Dict{String,Any}) where {T} = from_json(T, data)::T
163-
response(::Type{T}, data::Dict{String,Any}) where {T<:Dict} = convert(T, data)
162+
response(::Type{T}, data::AbstractDict{String,Any}) where {T} = from_json(T, data)::T
163+
response(::Type{T}, data::AbstractDict{String,Any}) where {T<:Dict} = convert(T, Dict{String,Any}(data))
164164
response(::Type{Vector{T}}, data::Vector{V}) where {T,V} = T[response(T, v) for v in data]
165165

166166
noop_pre_request_hook(ctx::Ctx) = ctx
@@ -299,14 +299,14 @@ end
299299

300300
Base.hasproperty(o::T, name::Symbol) where {T<:APIModel} = ((name in propertynames(o)) && (getproperty(o, name) !== nothing))
301301

302-
convert(::Type{T}, json::Dict{String,Any}) where {T<:APIModel} = from_json(T, json)
302+
convert(::Type{T}, json::AbstractDict{String,Any}) where {T<:APIModel} = from_json(T, json)
303303
convert(::Type{T}, v::Nothing) where {T<:APIModel} = T()
304304
convert(::Type{T}, v::T) where {T<:OneOfAPIModel} = v
305-
convert(::Type{T}, json::Dict{String,Any}) where {T<:OneOfAPIModel} = from_json(T, json)
305+
convert(::Type{T}, json::AbstractDict{String,Any}) where {T<:OneOfAPIModel} = from_json(T, json)
306306
convert(::Type{T}, v) where {T<:OneOfAPIModel} = T(v)
307307
convert(::Type{T}, v::String) where {T<:OneOfAPIModel} = T(v)
308308
convert(::Type{T}, v::T) where {T<:AnyOfAPIModel} = v
309-
convert(::Type{T}, json::Dict{String,Any}) where {T<:AnyOfAPIModel} = from_json(T, json)
309+
convert(::Type{T}, json::AbstractDict{String,Any}) where {T<:AnyOfAPIModel} = from_json(T, json)
310310
convert(::Type{T}, v) where {T<:AnyOfAPIModel} = T(v)
311311
convert(::Type{T}, v::String) where {T<:AnyOfAPIModel} = T(v)
312312

src/client/chunk_readers.jl

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,71 @@ struct JSONChunkReader <: AbstractChunkReader
2020
buffered_input::Base.BufferStream
2121
end
2222

23+
function _read_json_chunk(io::IO)
24+
out = IOBuffer()
25+
first_byte = peek(io, UInt8)
26+
27+
if first_byte == UInt8('{') || first_byte == UInt8('[')
28+
close_byte = first_byte == UInt8('{') ? UInt8('}') : UInt8(']')
29+
depth = 0
30+
in_string = false
31+
escaped = false
32+
33+
while !eof(io)
34+
byte = read(io, UInt8)
35+
write(out, byte)
36+
37+
if escaped
38+
escaped = false
39+
continue
40+
end
41+
42+
if in_string
43+
if byte == UInt8('\\')
44+
escaped = true
45+
elseif byte == UInt8('"')
46+
in_string = false
47+
end
48+
else
49+
if byte == UInt8('"')
50+
in_string = true
51+
elseif byte == first_byte
52+
depth += 1
53+
elseif byte == close_byte
54+
depth -= 1
55+
depth == 0 && break
56+
end
57+
end
58+
end
59+
elseif first_byte == UInt8('"')
60+
escaped = false
61+
read(io, UInt8) # consume opening quote
62+
write(out, UInt8('"'))
63+
while !eof(io)
64+
byte = read(io, UInt8)
65+
write(out, byte)
66+
if escaped
67+
escaped = false
68+
elseif byte == UInt8('\\')
69+
escaped = true
70+
elseif byte == UInt8('"')
71+
break
72+
end
73+
end
74+
else
75+
# number / true / false / null: read until delimiter
76+
while !eof(io)
77+
byte = peek(io, UInt8)
78+
if isspace(Char(byte)) || byte == UInt8(',') || byte == UInt8(']') || byte == UInt8('}')
79+
break
80+
end
81+
write(out, read(io, UInt8))
82+
end
83+
end
84+
85+
take!(out)
86+
end
87+
2388
function Base.iterate(iter::JSONChunkReader, _state=nothing)
2489
if eof(iter.buffered_input)
2590
return nothing
@@ -34,7 +99,9 @@ function Base.iterate(iter::JSONChunkReader, _state=nothing)
3499
end
35100
end
36101
eof(iter.buffered_input) && return nothing
37-
valid_json = JSON.parse(iter.buffered_input)
102+
chunk_bytes = _read_json_chunk(iter.buffered_input)
103+
isempty(chunk_bytes) && return nothing
104+
valid_json = _json_parse(String(chunk_bytes))
38105
bytes = convert(Vector{UInt8}, codeunits(JSON.json(valid_json)))
39106
return (bytes, iter)
40107
end

src/json.jl

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ end
4242

4343
is_deep_explode(sctx::StyleCtx) = sctx.name == "deepObject" && sctx.is_explode
4444

45-
function deep_object_to_array(src::Dict)
45+
function deep_object_to_array(src::AbstractDict)
4646
keys_are_int = all(key -> occursin(r"^\d+$", key), keys(src))
4747
if keys_are_int
4848
sorted_keys = sort(collect(keys(src)), by=x->parse(Int, x))
@@ -58,14 +58,14 @@ end
5858

5959
to_json(o) = JSON.json(o)
6060

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
61+
from_json(::Type{Union{Nothing,T}}, json::AbstractDict{String,Any}; stylectx=nothing) where {T} = from_json(T, json; stylectx)
62+
from_json(::Type{T}, json::AbstractDict{String,Any}; stylectx=nothing) where {T} = from_json(T(), json; stylectx)
63+
from_json(::Type{T}, json::AbstractDict{String,Any}; stylectx=nothing) where {T <: Dict} = convert(T, Dict{String,Any}(json))
64+
from_json(::Type{T}, j::AbstractDict{String,Any}; stylectx=nothing) where {T <: String} = to_json(j)
65+
from_json(::Type{Any}, j::AbstractDict{String,Any}; stylectx=nothing) = j
6666
from_json(::Type{Vector{T}}, j::Vector{Any}; stylectx=nothing) where {T} = j
6767

68-
function from_json(::Type{Vector{T}}, json::Dict{String, Any}; stylectx=nothing) where {T}
68+
function from_json(::Type{Vector{T}}, json::AbstractDict{String, Any}; stylectx=nothing) where {T}
6969
if !isnothing(stylectx) && is_deep_explode(stylectx)
7070
cvt = deep_object_to_array(json)
7171
if isa(cvt, Vector)
@@ -78,7 +78,7 @@ function from_json(::Type{Vector{T}}, json::Dict{String, Any}; stylectx=nothing)
7878
end
7979
end
8080

81-
function from_json(o::T, json::Dict{String,Any};stylectx=nothing) where {T <: UnionAPIModel}
81+
function from_json(o::T, json::AbstractDict{String,Any};stylectx=nothing) where {T <: UnionAPIModel}
8282
return from_json(o, :value, json;stylectx)
8383
end
8484

@@ -88,16 +88,16 @@ function from_json(o::T, val::Union{String,Real};stylectx=nothing) where {T <: U
8888
return o
8989
end
9090

91-
function from_json(o::T, json::Dict{String,Any};stylectx=nothing) where {T <: APIModel}
91+
function from_json(o::T, json::AbstractDict{String,Any};stylectx=nothing) where {T <: APIModel}
9292
jsonkeys = [Symbol(k) for k in keys(json)]
9393
for name in intersect(propertynames(o), jsonkeys)
9494
from_json(o, name, json[String(name)];stylectx)
9595
end
9696
return o
9797
end
9898

99-
function from_json(o::T, name::Symbol, json::Dict{String,Any};stylectx=nothing) where {T <: APIModel}
100-
ftype = (T <: UnionAPIModel) ? property_type(T, name, json) : property_type(T, name)
99+
function from_json(o::T, name::Symbol, json::AbstractDict{String,Any};stylectx=nothing) where {T <: APIModel}
100+
ftype = (T <: UnionAPIModel) ? property_type(T, name, Dict{String,Any}(json)) : property_type(T, name)
101101
fval = from_json(ftype, json; stylectx)
102102
setfield!(o, name, convert(ftype, fval))
103103
return o

src/server.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ to_param_type(::Type{Vector{UInt8}}, val::String; stylectx=nothing) = convert(Ve
8989
to_param_type(::Type{Vector{T}}, val::Vector{T}, _collection_format::Union{String,Nothing}; stylectx=nothing) where {T} = val
9090
to_param_type(::Type{Vector{T}}, json::Vector{Any}; stylectx=nothing) where {T} = [to_param_type(T, x; stylectx) for x in json]
9191

92-
function to_param_type(::Type{Vector{T}}, json::Dict{String, Any}; stylectx=nothing) where {T}
92+
function to_param_type(::Type{Vector{T}}, json::AbstractDict{String, Any}; stylectx=nothing) where {T}
9393
if !isnothing(stylectx) && is_deep_explode(stylectx)
9494
cvt = deep_object_to_array(json)
9595
if isa(cvt, Vector)
@@ -103,7 +103,7 @@ function to_param_type(::Type{T}, strval::String; stylectx=nothing) where {T <:
103103
from_json(T, JSON.parse(strval); stylectx)
104104
end
105105

106-
function to_param_type(::Type{T}, json::Dict{String,Any}; stylectx=nothing) where {T <: APIModel}
106+
function to_param_type(::Type{T}, json::AbstractDict{String,Any}; stylectx=nothing) where {T <: APIModel}
107107
from_json(T, json; stylectx)
108108
end
109109

test/chunkreader_tests.jl

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module ChunkReaderTests
22
using Test
33
using JSON
44
using OpenAPI
5-
using OpenAPI.Clients: AbstractChunkReader, JSONChunkReader, LineChunkReader, RFC7464ChunkReader
5+
using OpenAPI.Clients: AbstractChunkReader, JSONChunkReader, LineChunkReader, RFC7464ChunkReader, _read_json_chunk
66

77
function linechunk1()
88
buff = Base.BufferStream()
@@ -182,6 +182,102 @@ function rfc7464chunk2()
182182
@test length(results) == 2
183183
end
184184

185+
function read_json_chunk_object()
186+
io = IOBuffer("{\"key\": \"value\"}")
187+
@test String(_read_json_chunk(io)) == "{\"key\": \"value\"}"
188+
@test eof(io)
189+
end
190+
191+
function read_json_chunk_nested_object()
192+
io = IOBuffer("{\"a\": {\"b\": 1}}")
193+
@test String(_read_json_chunk(io)) == "{\"a\": {\"b\": 1}}"
194+
@test eof(io)
195+
end
196+
197+
function read_json_chunk_array()
198+
io = IOBuffer("[1, 2, 3]")
199+
@test String(_read_json_chunk(io)) == "[1, 2, 3]"
200+
@test eof(io)
201+
end
202+
203+
function read_json_chunk_nested_array()
204+
io = IOBuffer("[[1,2],[3,4]]")
205+
@test String(_read_json_chunk(io)) == "[[1,2],[3,4]]"
206+
@test eof(io)
207+
end
208+
209+
function read_json_chunk_string()
210+
io = IOBuffer("\"hello\"")
211+
@test String(_read_json_chunk(io)) == "\"hello\""
212+
@test eof(io)
213+
end
214+
215+
function read_json_chunk_string_escaped_quote()
216+
# embedded escaped quote: "say \"hi\""
217+
io = IOBuffer("\"say \\\"hi\\\"\"")
218+
@test String(_read_json_chunk(io)) == "\"say \\\"hi\\\"\""
219+
@test eof(io)
220+
end
221+
222+
function read_json_chunk_string_escaped_backslash()
223+
# embedded escaped backslash: "path\\file"
224+
io = IOBuffer("\"path\\\\file\"")
225+
@test String(_read_json_chunk(io)) == "\"path\\\\file\""
226+
@test eof(io)
227+
end
228+
229+
function read_json_chunk_integer()
230+
io = IOBuffer("42")
231+
@test String(_read_json_chunk(io)) == "42"
232+
@test eof(io)
233+
end
234+
235+
function read_json_chunk_float()
236+
io = IOBuffer("3.14")
237+
@test String(_read_json_chunk(io)) == "3.14"
238+
@test eof(io)
239+
end
240+
241+
function read_json_chunk_true()
242+
io = IOBuffer("true")
243+
@test String(_read_json_chunk(io)) == "true"
244+
@test eof(io)
245+
end
246+
247+
function read_json_chunk_false()
248+
io = IOBuffer("false")
249+
@test String(_read_json_chunk(io)) == "false"
250+
@test eof(io)
251+
end
252+
253+
function read_json_chunk_null()
254+
io = IOBuffer("null")
255+
@test String(_read_json_chunk(io)) == "null"
256+
@test eof(io)
257+
end
258+
259+
function read_json_chunk_stops_at_boundary()
260+
# reads exactly one chunk and leaves the stream positioned at the next value
261+
io = IOBuffer("{\"a\":1}{\"b\":2}")
262+
@test String(_read_json_chunk(io)) == "{\"a\":1}"
263+
@test String(_read_json_chunk(io)) == "{\"b\":2}"
264+
@test eof(io)
265+
end
266+
267+
function read_json_chunk_braces_in_string()
268+
# braces inside a string value must not affect depth tracking
269+
io = IOBuffer("{\"key\": \"value{nested}\"}")
270+
@test String(_read_json_chunk(io)) == "{\"key\": \"value{nested}\"}"
271+
@test eof(io)
272+
end
273+
274+
function read_json_chunk_brackets_in_string()
275+
# brackets inside a string value must not affect depth tracking
276+
io = IOBuffer("{\"key\": \"[not an array]\"}")
277+
@test String(_read_json_chunk(io)) == "{\"key\": \"[not an array]\"}"
278+
@test eof(io)
279+
end
280+
185281
function runtests()
186282
linechunk1()
187283
linechunk2()
@@ -192,6 +288,21 @@ function runtests()
192288
jsonchunk4()
193289
rfc7464chunk1()
194290
rfc7464chunk2()
291+
read_json_chunk_object()
292+
read_json_chunk_nested_object()
293+
read_json_chunk_array()
294+
read_json_chunk_nested_array()
295+
read_json_chunk_string()
296+
read_json_chunk_string_escaped_quote()
297+
read_json_chunk_string_escaped_backslash()
298+
read_json_chunk_integer()
299+
read_json_chunk_float()
300+
read_json_chunk_true()
301+
read_json_chunk_false()
302+
read_json_chunk_null()
303+
read_json_chunk_stops_at_boundary()
304+
read_json_chunk_braces_in_string()
305+
read_json_chunk_brackets_in_string()
195306
end
196307

197308
end # module ChunkReaderTests

0 commit comments

Comments
 (0)