Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions src/OpenAPI.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
12 changes: 6 additions & 6 deletions src/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
69 changes: 68 additions & 1 deletion src/client/chunk_readers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
22 changes: 11 additions & 11 deletions src/json.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
Expand All @@ -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

Expand All @@ -88,16 +88,16 @@ 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)
end
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
Expand Down
4 changes: 2 additions & 2 deletions src/server.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down
113 changes: 112 additions & 1 deletion test/chunkreader_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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
Loading