diff --git a/Project.toml b/Project.toml index 7b29020..2d7e47b 100644 --- a/Project.toml +++ b/Project.toml @@ -4,7 +4,7 @@ keywords = ["Swagger", "OpenAPI", "REST"] license = "MIT" desc = "OpenAPI server and client helper for Julia" authors = ["JuliaHub Inc."] -version = "0.2.1" +version = "0.2.2" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" @@ -22,8 +22,8 @@ p7zip_jll = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" [compat] Downloads = "1" HTTP = "1" -JSON = "0.20, 0.21" -LibCURL = "0.6" +JSON = "0.20, 0.21, 1" +LibCURL = "0.6, 1" MIMEs = "0.1, 1" MbedTLS = "0.6.8, 0.7, 1" TimeZones = "1" diff --git a/src/OpenAPI.jl b/src/OpenAPI.jl index 50d9e56..9366a77 100644 --- a/src/OpenAPI.jl +++ b/src/OpenAPI.jl @@ -7,6 +7,15 @@ using p7zip_jll import Base: getindex, keys, length, iterate, hasproperty import JSON: lower + +const _JSON_PARSE_ISROOT_SUPPORTED = try; JSON.parse("1 "; isroot=false); true; catch; false; end + +if _JSON_PARSE_ISROOT_SUPPORTED + _json_parse(io_or_str) = JSON.parse(io_or_str; isroot=false) +else + _json_parse(io_or_str) = JSON.parse(io_or_str) +end + include("commontypes.jl") include("datetime.jl") include("val.jl") diff --git a/src/client.jl b/src/client.jl index b7e90b5..906192b 100644 --- a/src/client.jl +++ b/src/client.jl @@ -12,7 +12,7 @@ using MIMEs import Base: convert, show, summary, getproperty, setproperty!, iterate import ..OpenAPI: APIModel, UnionAPIModel, OneOfAPIModel, AnyOfAPIModel, APIClientImpl, OpenAPIException, InvocationException, to_json, from_json, validate_property, property_type -import ..OpenAPI: str2zoneddatetime, str2datetime, str2date +import ..OpenAPI: str2zoneddatetime, str2datetime, str2date, _json_parse include("client/clienttypes.jl") include("client/chunk_readers.jl") @@ -159,8 +159,8 @@ response(::Type{DateTime}, data) = str2datetime(data) response(::Type{Date}, data) = str2date(data) response(::Type{T}, data) where {T} = convert(T, data) -response(::Type{T}, data::Dict{String,Any}) where {T} = from_json(T, data)::T -response(::Type{T}, data::Dict{String,Any}) where {T<:Dict} = convert(T, data) +response(::Type{T}, data::AbstractDict{String,Any}) where {T} = from_json(T, data)::T +response(::Type{T}, data::AbstractDict{String,Any}) where {T<:Dict} = convert(T, Dict{String,Any}(data)) response(::Type{Vector{T}}, data::Vector{V}) where {T,V} = T[response(T, v) for v in data] noop_pre_request_hook(ctx::Ctx) = ctx @@ -299,14 +299,14 @@ end Base.hasproperty(o::T, name::Symbol) where {T<:APIModel} = ((name in propertynames(o)) && (getproperty(o, name) !== nothing)) -convert(::Type{T}, json::Dict{String,Any}) where {T<:APIModel} = from_json(T, json) +convert(::Type{T}, json::AbstractDict{String,Any}) where {T<:APIModel} = from_json(T, json) convert(::Type{T}, v::Nothing) where {T<:APIModel} = T() convert(::Type{T}, v::T) where {T<:OneOfAPIModel} = v -convert(::Type{T}, json::Dict{String,Any}) where {T<:OneOfAPIModel} = from_json(T, json) +convert(::Type{T}, json::AbstractDict{String,Any}) where {T<:OneOfAPIModel} = from_json(T, json) convert(::Type{T}, v) where {T<:OneOfAPIModel} = T(v) convert(::Type{T}, v::String) where {T<:OneOfAPIModel} = T(v) convert(::Type{T}, v::T) where {T<:AnyOfAPIModel} = v -convert(::Type{T}, json::Dict{String,Any}) where {T<:AnyOfAPIModel} = from_json(T, json) +convert(::Type{T}, json::AbstractDict{String,Any}) where {T<:AnyOfAPIModel} = from_json(T, json) convert(::Type{T}, v) where {T<:AnyOfAPIModel} = T(v) convert(::Type{T}, v::String) where {T<:AnyOfAPIModel} = T(v) diff --git a/src/client/chunk_readers.jl b/src/client/chunk_readers.jl index 10d9ccf..a4b6a09 100644 --- a/src/client/chunk_readers.jl +++ b/src/client/chunk_readers.jl @@ -20,6 +20,71 @@ struct JSONChunkReader <: AbstractChunkReader buffered_input::Base.BufferStream end +function _read_json_chunk(io::IO) + out = IOBuffer() + first_byte = peek(io, UInt8) + + if first_byte == UInt8('{') || first_byte == UInt8('[') + close_byte = first_byte == UInt8('{') ? UInt8('}') : UInt8(']') + depth = 0 + in_string = false + escaped = false + + while !eof(io) + byte = read(io, UInt8) + write(out, byte) + + if escaped + escaped = false + continue + end + + if in_string + if byte == UInt8('\\') + escaped = true + elseif byte == UInt8('"') + in_string = false + end + else + if byte == UInt8('"') + in_string = true + elseif byte == first_byte + depth += 1 + elseif byte == close_byte + depth -= 1 + depth == 0 && break + end + end + end + elseif first_byte == UInt8('"') + escaped = false + read(io, UInt8) # consume opening quote + write(out, UInt8('"')) + while !eof(io) + byte = read(io, UInt8) + write(out, byte) + if escaped + escaped = false + elseif byte == UInt8('\\') + escaped = true + elseif byte == UInt8('"') + break + end + end + else + # number / true / false / null: read until delimiter + while !eof(io) + byte = peek(io, UInt8) + if isspace(Char(byte)) || byte == UInt8(',') || byte == UInt8(']') || byte == UInt8('}') + break + end + write(out, read(io, UInt8)) + end + end + + take!(out) +end + function Base.iterate(iter::JSONChunkReader, _state=nothing) if eof(iter.buffered_input) return nothing @@ -34,7 +99,9 @@ function Base.iterate(iter::JSONChunkReader, _state=nothing) end end eof(iter.buffered_input) && return nothing - valid_json = JSON.parse(iter.buffered_input) + chunk_bytes = _read_json_chunk(iter.buffered_input) + isempty(chunk_bytes) && return nothing + valid_json = _json_parse(String(chunk_bytes)) bytes = convert(Vector{UInt8}, codeunits(JSON.json(valid_json))) return (bytes, iter) end diff --git a/src/json.jl b/src/json.jl index b9add9c..db8a5dd 100644 --- a/src/json.jl +++ b/src/json.jl @@ -42,7 +42,7 @@ end is_deep_explode(sctx::StyleCtx) = sctx.name == "deepObject" && sctx.is_explode -function deep_object_to_array(src::Dict) +function deep_object_to_array(src::AbstractDict) keys_are_int = all(key -> occursin(r"^\d+$", key), keys(src)) if keys_are_int sorted_keys = sort(collect(keys(src)), by=x->parse(Int, x)) @@ -58,14 +58,14 @@ end to_json(o) = JSON.json(o) -from_json(::Type{Union{Nothing,T}}, json::Dict{String,Any}; stylectx=nothing) where {T} = from_json(T, json; stylectx) -from_json(::Type{T}, json::Dict{String,Any}; stylectx=nothing) where {T} = from_json(T(), json; stylectx) -from_json(::Type{T}, json::Dict{String,Any}; stylectx=nothing) where {T <: Dict} = convert(T, json) -from_json(::Type{T}, j::Dict{String,Any}; stylectx=nothing) where {T <: String} = to_json(j) -from_json(::Type{Any}, j::Dict{String,Any}; stylectx=nothing) = j +from_json(::Type{Union{Nothing,T}}, json::AbstractDict{String,Any}; stylectx=nothing) where {T} = from_json(T, json; stylectx) +from_json(::Type{T}, json::AbstractDict{String,Any}; stylectx=nothing) where {T} = from_json(T(), json; stylectx) +from_json(::Type{T}, json::AbstractDict{String,Any}; stylectx=nothing) where {T <: Dict} = convert(T, Dict{String,Any}(json)) +from_json(::Type{T}, j::AbstractDict{String,Any}; stylectx=nothing) where {T <: String} = to_json(j) +from_json(::Type{Any}, j::AbstractDict{String,Any}; stylectx=nothing) = j from_json(::Type{Vector{T}}, j::Vector{Any}; stylectx=nothing) where {T} = j -function from_json(::Type{Vector{T}}, json::Dict{String, Any}; stylectx=nothing) where {T} +function from_json(::Type{Vector{T}}, json::AbstractDict{String, Any}; stylectx=nothing) where {T} if !isnothing(stylectx) && is_deep_explode(stylectx) cvt = deep_object_to_array(json) if isa(cvt, Vector) @@ -78,7 +78,7 @@ function from_json(::Type{Vector{T}}, json::Dict{String, Any}; stylectx=nothing) end end -function from_json(o::T, json::Dict{String,Any};stylectx=nothing) where {T <: UnionAPIModel} +function from_json(o::T, json::AbstractDict{String,Any};stylectx=nothing) where {T <: UnionAPIModel} return from_json(o, :value, json;stylectx) end @@ -88,7 +88,7 @@ function from_json(o::T, val::Union{String,Real};stylectx=nothing) where {T <: U return o end -function from_json(o::T, json::Dict{String,Any};stylectx=nothing) where {T <: APIModel} +function from_json(o::T, json::AbstractDict{String,Any};stylectx=nothing) where {T <: APIModel} jsonkeys = [Symbol(k) for k in keys(json)] for name in intersect(propertynames(o), jsonkeys) from_json(o, name, json[String(name)];stylectx) @@ -96,8 +96,8 @@ function from_json(o::T, json::Dict{String,Any};stylectx=nothing) where {T <: AP return o end -function from_json(o::T, name::Symbol, json::Dict{String,Any};stylectx=nothing) where {T <: APIModel} - ftype = (T <: UnionAPIModel) ? property_type(T, name, json) : property_type(T, name) +function from_json(o::T, name::Symbol, json::AbstractDict{String,Any};stylectx=nothing) where {T <: APIModel} + ftype = (T <: UnionAPIModel) ? property_type(T, name, Dict{String,Any}(json)) : property_type(T, name) fval = from_json(ftype, json; stylectx) setfield!(o, name, convert(ftype, fval)) return o diff --git a/src/server.jl b/src/server.jl index ba4e388..d7d1a74 100644 --- a/src/server.jl +++ b/src/server.jl @@ -89,7 +89,7 @@ to_param_type(::Type{Vector{UInt8}}, val::String; stylectx=nothing) = convert(Ve to_param_type(::Type{Vector{T}}, val::Vector{T}, _collection_format::Union{String,Nothing}; stylectx=nothing) where {T} = val to_param_type(::Type{Vector{T}}, json::Vector{Any}; stylectx=nothing) where {T} = [to_param_type(T, x; stylectx) for x in json] -function to_param_type(::Type{Vector{T}}, json::Dict{String, Any}; stylectx=nothing) where {T} +function to_param_type(::Type{Vector{T}}, json::AbstractDict{String, Any}; stylectx=nothing) where {T} if !isnothing(stylectx) && is_deep_explode(stylectx) cvt = deep_object_to_array(json) if isa(cvt, Vector) @@ -103,7 +103,7 @@ function to_param_type(::Type{T}, strval::String; stylectx=nothing) where {T <: from_json(T, JSON.parse(strval); stylectx) end -function to_param_type(::Type{T}, json::Dict{String,Any}; stylectx=nothing) where {T <: APIModel} +function to_param_type(::Type{T}, json::AbstractDict{String,Any}; stylectx=nothing) where {T <: APIModel} from_json(T, json; stylectx) end diff --git a/test/chunkreader_tests.jl b/test/chunkreader_tests.jl index 3b10d7f..bf02a83 100644 --- a/test/chunkreader_tests.jl +++ b/test/chunkreader_tests.jl @@ -2,7 +2,7 @@ module ChunkReaderTests using Test using JSON using OpenAPI -using OpenAPI.Clients: AbstractChunkReader, JSONChunkReader, LineChunkReader, RFC7464ChunkReader +using OpenAPI.Clients: AbstractChunkReader, JSONChunkReader, LineChunkReader, RFC7464ChunkReader, _read_json_chunk function linechunk1() buff = Base.BufferStream() @@ -182,6 +182,102 @@ function rfc7464chunk2() @test length(results) == 2 end +function read_json_chunk_object() + io = IOBuffer("{\"key\": \"value\"}") + @test String(_read_json_chunk(io)) == "{\"key\": \"value\"}" + @test eof(io) +end + +function read_json_chunk_nested_object() + io = IOBuffer("{\"a\": {\"b\": 1}}") + @test String(_read_json_chunk(io)) == "{\"a\": {\"b\": 1}}" + @test eof(io) +end + +function read_json_chunk_array() + io = IOBuffer("[1, 2, 3]") + @test String(_read_json_chunk(io)) == "[1, 2, 3]" + @test eof(io) +end + +function read_json_chunk_nested_array() + io = IOBuffer("[[1,2],[3,4]]") + @test String(_read_json_chunk(io)) == "[[1,2],[3,4]]" + @test eof(io) +end + +function read_json_chunk_string() + io = IOBuffer("\"hello\"") + @test String(_read_json_chunk(io)) == "\"hello\"" + @test eof(io) +end + +function read_json_chunk_string_escaped_quote() + # embedded escaped quote: "say \"hi\"" + io = IOBuffer("\"say \\\"hi\\\"\"") + @test String(_read_json_chunk(io)) == "\"say \\\"hi\\\"\"" + @test eof(io) +end + +function read_json_chunk_string_escaped_backslash() + # embedded escaped backslash: "path\\file" + io = IOBuffer("\"path\\\\file\"") + @test String(_read_json_chunk(io)) == "\"path\\\\file\"" + @test eof(io) +end + +function read_json_chunk_integer() + io = IOBuffer("42") + @test String(_read_json_chunk(io)) == "42" + @test eof(io) +end + +function read_json_chunk_float() + io = IOBuffer("3.14") + @test String(_read_json_chunk(io)) == "3.14" + @test eof(io) +end + +function read_json_chunk_true() + io = IOBuffer("true") + @test String(_read_json_chunk(io)) == "true" + @test eof(io) +end + +function read_json_chunk_false() + io = IOBuffer("false") + @test String(_read_json_chunk(io)) == "false" + @test eof(io) +end + +function read_json_chunk_null() + io = IOBuffer("null") + @test String(_read_json_chunk(io)) == "null" + @test eof(io) +end + +function read_json_chunk_stops_at_boundary() + # reads exactly one chunk and leaves the stream positioned at the next value + io = IOBuffer("{\"a\":1}{\"b\":2}") + @test String(_read_json_chunk(io)) == "{\"a\":1}" + @test String(_read_json_chunk(io)) == "{\"b\":2}" + @test eof(io) +end + +function read_json_chunk_braces_in_string() + # braces inside a string value must not affect depth tracking + io = IOBuffer("{\"key\": \"value{nested}\"}") + @test String(_read_json_chunk(io)) == "{\"key\": \"value{nested}\"}" + @test eof(io) +end + +function read_json_chunk_brackets_in_string() + # brackets inside a string value must not affect depth tracking + io = IOBuffer("{\"key\": \"[not an array]\"}") + @test String(_read_json_chunk(io)) == "{\"key\": \"[not an array]\"}" + @test eof(io) +end + function runtests() linechunk1() linechunk2() @@ -192,6 +288,21 @@ function runtests() jsonchunk4() rfc7464chunk1() rfc7464chunk2() + read_json_chunk_object() + read_json_chunk_nested_object() + read_json_chunk_array() + read_json_chunk_nested_array() + read_json_chunk_string() + read_json_chunk_string_escaped_quote() + read_json_chunk_string_escaped_backslash() + read_json_chunk_integer() + read_json_chunk_float() + read_json_chunk_true() + read_json_chunk_false() + read_json_chunk_null() + read_json_chunk_stops_at_boundary() + read_json_chunk_braces_in_string() + read_json_chunk_brackets_in_string() end end # module ChunkReaderTests \ No newline at end of file