Skip to content

Commit 53ce83a

Browse files
committed
feat: JSON handling with JSON.jl v1.0 and StructTypes
1 parent 0c95ebb commit 53ce83a

5 files changed

Lines changed: 146 additions & 80 deletions

File tree

Project.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ uuid = "d5e62ea6-ddf3-4d43-8e4c-ad5e6c8bfd7d"
33
keywords = ["Swagger", "OpenAPI", "REST"]
44
license = "MIT"
55
desc = "OpenAPI server and client helper for Julia"
6+
version = "0.2.1"
67
authors = ["JuliaHub Inc."]
7-
version = "0.2.0"
88

99
[deps]
1010
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
@@ -15,17 +15,19 @@ JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
1515
LibCURL = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
1616
MIMEs = "6c6e2e6c-3030-632d-7369-2d6c69616d65"
1717
MbedTLS = "739be429-bea8-5141-9913-cc70e7f3736d"
18+
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
1819
TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"
1920
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
2021
p7zip_jll = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
2122

2223
[compat]
2324
Downloads = "1"
2425
HTTP = "1"
25-
JSON = "0.20, 0.21"
26+
JSON = "1.1.0"
2627
LibCURL = "0.6"
2728
MIMEs = "0.1, 1"
2829
MbedTLS = "0.6.8, 0.7, 1"
30+
StructTypes = "1.11.0"
2931
TimeZones = "1"
3032
URIs = "1.3"
3133
julia = "1.6"

src/OpenAPI.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module OpenAPI
33
using HTTP, JSON, URIs, Dates, TimeZones, Base64
44
using Downloads
55
using p7zip_jll
6-
6+
using StructTypes
77
import Base: getindex, keys, length, iterate, hasproperty
88
import JSON: lower
99

src/client.jl

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ using TimeZones
99
using LibCURL
1010
using HTTP
1111
using MIMEs
12+
using StructTypes
1213

1314
import Base: convert, show, summary, getproperty, setproperty!, iterate
1415
import ..OpenAPI: APIModel, UnionAPIModel, OneOfAPIModel, AnyOfAPIModel, APIClientImpl, OpenAPIException, InvocationException, to_json, from_json, validate_property, property_type
@@ -424,30 +425,21 @@ function header(resp::Downloads.Response, name::AbstractString, defaultval::Abst
424425
return defaultval
425426
end
426427

428+
427429
response(::Type{Nothing}, resp::Downloads.Response, body) = nothing::Nothing
428-
response(::Type{T}, resp::Downloads.Response, body) where {T <: Real} = response(T, body)::T
429-
response(::Type{T}, resp::Downloads.Response, body) where {T <: String} = response(T, body)::T
430+
response(::Type{T}, resp::Downloads.Response, body) where {T <: Real} = parse(T, String(body))::T
431+
response(::Type{T}, resp::Downloads.Response, body) where {T <: String} = String(body)::T
430432
function response(::Type{T}, resp::Downloads.Response, body) where {T}
431433
ctype = header(resp, "Content-Type", "application/json")
432-
response(T, is_json_mime(ctype), body)::T
433-
end
434-
response(::Type{T}, ::Nothing, body) where {T} = response(T, true, body)
435-
function response(::Type{T}, is_json::Bool, body) where {T}
436-
(length(body) == 0) && return T()
437-
response(T, is_json ? JSON.parse(String(body)) : body)::T
434+
if is_json_mime(ctype)
435+
(length(body) == 0) && return T() # Handle empty body for model types
436+
# Use JSON.read for direct deserialization
437+
return JSON.read(body, T)::T
438+
else
439+
# Fallback for non-JSON content
440+
return convert(T, body)
441+
end
438442
end
439-
response(::Type{String}, data::Vector{UInt8}) = String(data)
440-
response(::Type{T}, data::Vector{UInt8}) where {T<:Real} = parse(T, String(data))
441-
response(::Type{T}, data::T) where {T} = data
442-
443-
response(::Type{ZonedDateTime}, data) = str2zoneddatetime(data)
444-
response(::Type{DateTime}, data) = str2datetime(data)
445-
response(::Type{Date}, data) = str2date(data)
446-
447-
response(::Type{T}, data) where {T} = convert(T, data)
448-
response(::Type{T}, data::Dict{String,Any}) where {T} = from_json(T, data)::T
449-
response(::Type{T}, data::Dict{String,Any}) where {T<:Dict} = convert(T, data)
450-
response(::Type{Vector{T}}, data::Vector{V}) where {T,V} = T[response(T, v) for v in data]
451443

452444
struct LineChunkReader <: AbstractChunkReader
453445
buffered_input::Base.BufferStream
@@ -471,23 +463,72 @@ struct JSONChunkReader <: AbstractChunkReader
471463
buffered_input::Base.BufferStream
472464
end
473465

474-
function Base.iterate(iter::JSONChunkReader, _state=nothing)
475-
if eof(iter.buffered_input)
476-
return nothing
477-
else
478-
# read all whitespaces
479-
while !eof(iter.buffered_input)
480-
byte = peek(iter.buffered_input, UInt8)
481-
if isspace(Char(byte))
482-
read(iter.buffered_input, UInt8)
466+
function Base.iterate(iter::JSONChunkReader, buffer::IOBuffer=IOBuffer())
467+
# This function now uses an IOBuffer as its state to hold
468+
# data that has been read from the stream but not yet parsed.
469+
470+
while true
471+
# First, try to parse the data we already have in our buffer.
472+
seekstart(buffer)
473+
data = read(buffer)
474+
475+
if !isempty(data)
476+
# Skip leading whitespace in our buffer
477+
# This is important if the previous chunk had trailing spaces.
478+
start_pos = 1
479+
while start_pos <= length(data) && isspace(Char(data[start_pos]))
480+
start_pos += 1
481+
end
482+
483+
if start_pos > length(data)
484+
# Buffer only contained whitespace, clear it and read more.
485+
take!(buffer)
483486
else
484-
break
487+
local lazy_val
488+
local end_pos
489+
try
490+
# Check if the buffer contains at least one complete JSON object.
491+
# We parse from the first non-whitespace character.
492+
lazy_val = JSON.lazy(view(data, start_pos:length(data)))
493+
494+
# If successful, find where this object ends.
495+
end_pos = JSON.skip(lazy_val)
496+
497+
# The bytes for the complete JSON chunk.
498+
json_chunk = data[start_pos:(start_pos + end_pos - 2)]
499+
500+
# The rest of the data is carried over to the next iteration.
501+
remaining_data = data[(start_pos + end_pos - 1):end]
502+
next_buffer = IOBuffer(remaining_data)
503+
504+
return (json_chunk, next_buffer)
505+
catch e
506+
# An UnexpectedEOF error means our buffer contains an incomplete object.
507+
# We'll loop again to read more data from the main stream.
508+
# Any other error indicates a real JSON syntax problem.
509+
if !(e isa ArgumentError && occursin("UnexpectedEOF", e.msg))
510+
rethrow()
511+
end
512+
end
485513
end
486514
end
487-
eof(iter.buffered_input) && return nothing
488-
valid_json = JSON.parse(iter.buffered_input)
489-
bytes = convert(Vector{UInt8}, codeunits(JSON.json(valid_json)))
490-
return (bytes, iter)
515+
516+
# If we are here, our buffer is empty or has an incomplete object.
517+
# We need to read more data from the source input stream.
518+
if eof(iter.buffered_input)
519+
# The source stream is finished. If the buffer still contains non-whitespace
520+
# data, it means the stream ended with an incomplete JSON object.
521+
if bytesavailable(buffer) > 0
522+
seekstart(buffer)
523+
if !eof(skipchars(isspace, buffer))
524+
error("Incomplete JSON data at end of stream.")
525+
end
526+
end
527+
return nothing # Correctly end the iteration.
528+
end
529+
530+
# Block and read new data from the input, then loop to retry parsing.
531+
write(buffer, readavailable(iter.buffered_input))
491532
end
492533
end
493534

src/json.jl

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,24 @@
1-
# JSONWrapper for OpenAPI models handles
2-
# - null fields
3-
# - field names that are Julia keywords
4-
struct JSONWrapper{T<:APIModel} <: AbstractDict{Symbol, Any}
5-
wrapped::T
6-
flds::Tuple
7-
end
1+
# Declare how StructTypes should handle APIModels
2+
StructTypes.StructType(::Type{<:APIModel}) = StructTypes.Struct()
83

9-
JSONWrapper(o::T) where {T<:APIModel} = JSONWrapper(o, filter(n->hasproperty(o,n) && (getproperty(o,n) !== nothing), propertynames(o)))
4+
# This single line replaces the entire JSONWrapper implementation.
5+
# It tells JSON.jl to automatically omit fields whose values are `nothing`.
6+
StructTypes.omitempties(::Type{<:APIModel}) = true
107

11-
getindex(w::JSONWrapper, s::Symbol) = getproperty(w.wrapped, s)
12-
keys(w::JSONWrapper) = w.flds
13-
length(w::JSONWrapper) = length(w.flds)
8+
# This hook tells JSON.read to use our custom `from_json` logic
9+
# for constructing APIModel types. This preserves our handling of dates,
10+
# discriminated unions, and other special cases.
11+
StructTypes.construct(::Type{T}, dict::Dict) where {T <: APIModel} = from_json(T, dict)
1412

15-
function iterate(w::JSONWrapper, state...)
16-
result = iterate(w.flds, state...)
17-
if result === nothing
18-
return result
19-
else
20-
name,nextstate = result
21-
val = getproperty(w.wrapped, name)
22-
return (name=>val, nextstate)
23-
end
24-
end
2513

26-
lower(o::T) where {T<:APIModel} = JSONWrapper(o)
14+
# The `lower` method for UnionAPIModel is still useful because it allows us to
15+
# serialize the inner .value of a oneOf/anyOf type, not the wrapper itself.
16+
# JSON.jl v1.0 still respects `lower`.
17+
2718
function lower(o::T) where {T<:UnionAPIModel}
2819
if typeof(o.value) <: APIModel
29-
return JSONWrapper(o.value)
20+
# Use JSON.lower on the wrapped value to apply its own rules
21+
return JSON.lower(o.value)
3022
elseif typeof(o.value) <: Union{String,Real}
3123
return o.value
3224
else
@@ -59,10 +51,20 @@ end
5951
to_json(o) = JSON.json(o)
6052

6153
from_json(::Type{Union{Nothing,T}}, json::Dict{String,Any}; stylectx=nothing) where {T} = from_json(T, json; stylectx)
54+
from_json(::Type{Union{Nothing,T}}, json::JSON.Object{String, Any}; stylectx=nothing) where {T} = from_json(T, json; stylectx)
55+
6256
from_json(::Type{T}, json::Dict{String,Any}; stylectx=nothing) where {T} = from_json(T(), json; stylectx)
57+
from_json(::Type{T}, json::JSON.Object{String, Any}; stylectx=nothing) where {T} = from_json(T(), json; stylectx)
58+
6359
from_json(::Type{T}, json::Dict{String,Any}; stylectx=nothing) where {T <: Dict} = convert(T, json)
60+
from_json(::Type{T}, json::JSON.Object{String, Any}; stylectx=nothing) where {T <: Dict} = convert(T, json)
61+
6462
from_json(::Type{T}, j::Dict{String,Any}; stylectx=nothing) where {T <: String} = to_json(j)
63+
from_json(::Type{T}, j::JSON.Object{String, Any}; stylectx=nothing) where {T <: String} = to_json(j)
64+
6565
from_json(::Type{Any}, j::Dict{String,Any}; stylectx=nothing) = j
66+
from_json(::Type{Any}, j::JSON.Object{String, Any}; stylectx=nothing) = j
67+
6668
from_json(::Type{Vector{T}}, j::Vector{Any}; stylectx=nothing) where {T} = j
6769

6870
function from_json(::Type{Vector{T}}, json::Dict{String, Any}; stylectx=nothing) where {T}
@@ -78,10 +80,27 @@ function from_json(::Type{Vector{T}}, json::Dict{String, Any}; stylectx=nothing)
7880
end
7981
end
8082

83+
function from_json(::Type{Vector{T}}, json::JSON.Object{String, Any}; stylectx=nothing) where {T}
84+
if !isnothing(stylectx) && is_deep_explode(stylectx)
85+
cvt = deep_object_to_array(json)
86+
if isa(cvt, Vector)
87+
return from_json(Vector{T}, cvt; stylectx)
88+
else
89+
return from_json(T, json; stylectx)
90+
end
91+
else
92+
return from_json(T, json; stylectx)
93+
end
94+
end
95+
8196
function from_json(o::T, json::Dict{String,Any};stylectx=nothing) where {T <: UnionAPIModel}
8297
return from_json(o, :value, json;stylectx)
8398
end
8499

100+
function from_json(o::T, json::JSON.Object{String, Any};stylectx=nothing) where {T <: UnionAPIModel}
101+
return from_json(o, :value, json;stylectx)
102+
end
103+
85104
from_json(::Type{T}, val::Union{String,Real};stylectx=nothing) where {T <: UnionAPIModel} = T(val)
86105
function from_json(o::T, val::Union{String,Real};stylectx=nothing) where {T <: UnionAPIModel}
87106
o.value = val
@@ -96,13 +115,28 @@ function from_json(o::T, json::Dict{String,Any};stylectx=nothing) where {T <: AP
96115
return o
97116
end
98117

118+
function from_json(o::T, json::JSON.Object{String, Any};stylectx=nothing) where {T <: APIModel}
119+
jsonkeys = [Symbol(k) for k in collect(keys(json))]
120+
for name in intersect(propertynames(o), jsonkeys)
121+
from_json(o, name, json[String(name)];stylectx)
122+
end
123+
return o
124+
end
125+
99126
function from_json(o::T, name::Symbol, json::Dict{String,Any};stylectx=nothing) where {T <: APIModel}
100127
ftype = (T <: UnionAPIModel) ? property_type(T, name, json) : property_type(T, name)
101128
fval = from_json(ftype, json; stylectx)
102129
setfield!(o, name, convert(ftype, fval))
103130
return o
104131
end
105132

133+
function from_json(o::T, name::Symbol, json::JSON.Object{String, Any};stylectx=nothing) where {T <: APIModel}
134+
ftype = (T <: UnionAPIModel) ? property_type(T, name, json) : property_type(T, name)
135+
fval = from_json(ftype, json; stylectx)
136+
setfield!(o, name, convert(ftype, fval))
137+
return o
138+
end
139+
106140
function from_json(o::T, name::Symbol, v; stylectx=nothing) where {T <: APIModel}
107141
ftype = (T <: UnionAPIModel) ? property_type(T, name, Dict{String,Any}()) : property_type(T, name)
108142
atype = isa(ftype, Union) ? ((ftype.a === Nothing) ? ftype.b : ftype.a) : ftype

src/server.jl

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module Servers
22

33
using JSON
44
using HTTP
5+
using StructTypes
56

67
import ..OpenAPI: APIModel, ValidationException, from_json, to_json, deep_object_to_array, StyleCtx, is_deep_explode
78

@@ -82,28 +83,16 @@ function to_param_type(::Type{T}, strval::String; stylectx=nothing) where {T <:
8283
parse(T, strval)
8384
end
8485

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})")
86+
# UPDATED: Use JSON.read for direct deserialization from a string.
87+
function to_param_type(::Type{T}, strval::String; stylectx=nothing) where {T <: APIModel}
88+
return JSON.read(strval, T)
10089
end
10190

102-
function to_param_type(::Type{T}, strval::String; stylectx=nothing) where {T <: APIModel}
103-
from_json(T, JSON.parse(strval); stylectx)
91+
function to_param_type(::Type{T}, json::Dict{String,Any}; stylectx=nothing) where {T <: APIModel}
92+
from_json(T, json; stylectx)
10493
end
10594

106-
function to_param_type(::Type{T}, json::Dict{String,Any}; stylectx=nothing) where {T <: APIModel}
95+
function to_param_type(::Type{T}, json::JSON.Object{String, Any}; stylectx=nothing) where {T <: APIModel}
10796
from_json(T, json; stylectx)
10897
end
10998

@@ -112,9 +101,9 @@ function to_param_type(::Type{Vector{T}}, strval::String, delim::String; stylect
112101
return map(x->to_param_type(T, x; stylectx), elems)
113102
end
114103

104+
# Use JSON.read for direct deserialization from a string.
115105
function to_param_type(::Type{Vector{T}}, strval::String; stylectx=nothing) where {T}
116-
elems = JSON.parse(strval)
117-
return map(x->to_param_type(T, x; stylectx), elems)
106+
return JSON.read(strval, Vector{T})
118107
end
119108

120109
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)

0 commit comments

Comments
 (0)