Skip to content

Commit aa3934c

Browse files
committed
Use generated function to simplify conversion to struct types
1 parent e1494df commit aa3934c

File tree

4 files changed

+174
-106
lines changed

4 files changed

+174
-106
lines changed

src/AbstractDict.jl

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
Base.IteratorSize(::Type{JSON.Object{T}}) where T = Base.SizeUnknown()
44
Base.IteratorEltype(::Type{JSON.Object{T}}) where T = Base.EltypeUnknown()
55

6-
Base.convert(::Type{T}, o::JSON.Object) where T<:AbstractDict = T(o)
7-
86

97
# Access
108

@@ -46,3 +44,61 @@ function Base.next(j::JSON.Object, (i, n, c))
4644
v = getvalue(j.s, i, c)
4745
return k => v, (i, n, c)
4846
end
47+
48+
49+
# Conversion to Dict Types
50+
51+
Base.convert(::Type{Any}, o::JSON.Object) = o
52+
53+
Base.convert(::Type{JSON.Object{T}},
54+
o::JSON.Object{T}) where T <: AbstractString = o
55+
56+
Base.convert(::Type{T}, o::JSON.Object) where T<:AbstractDict = T(o)
57+
58+
59+
# Conversion to Struct Types
60+
61+
#https://github.com/JuliaLang/julia/issues/26090
62+
isreserved(s::Symbol) = s in (
63+
:while, :if, :for, :try, :return, :break, :continue,
64+
:function, :macro, :quote, :let, :local, :global, :const, :do,
65+
:struct,
66+
:type, :immutable, :importall, # to be deprecated
67+
:module, :baremodule, :using, :import, :export,
68+
:end, :else, :catch, :finally, :true, :false)
69+
70+
unmangled_fieldnames(T) = [n[1] == '_' &&
71+
isreserved(Symbol(n[2:end])) ? n[2:end] : n
72+
for n in map(Base.String, fieldnames(T))]
73+
74+
"""
75+
Convert a `JSON.Object` to a `struct T`.
76+
Optimised for the case where the order of the JSON fields matches the struct.
77+
"""
78+
@generated function Base.convert(::Type{T}, o::JSON.Object) where T
79+
80+
fn = fieldnames(T)
81+
fk = unmangled_fieldnames(T)
82+
83+
Expr(:block,
84+
:( i = o.i ),
85+
(:( (i, $n) = get_field(o, $k, i) ) for (n,k) in zip(fn, fk))...,
86+
:( $T($(fn...)) ))
87+
end
88+
89+
"""
90+
Get a `field` from a `JSON.Object` starting at `start_i`.
91+
Returns `start_i` for next field and field value.
92+
"""
93+
function get_field(o::JSON.Object, field, start_i)
94+
i, c = get_ic(o, field, (0, 0x00), start_i)
95+
if i == 0
96+
#throw(KeyError(field))
97+
v = nothing
98+
i = start_i
99+
else
100+
v = getvalue(o.s, i, c)
101+
i = lastindex_of_value(o.s, i, c)
102+
end
103+
return i, v
104+
end

src/LazyJSON.jl

Lines changed: 6 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -255,14 +255,14 @@ memcmp(a, b, l) = ccall(:memcmp, Int32, (Ptr{UInt8}, Ptr{UInt8}, UInt), a, b, l)
255255
"""
256256
Get the index `i` and first byte `c` of the field value for `key` in an Object.
257257
"""
258-
function get_ic(o::JSON.Object, key::AbstractString, default)
258+
function get_ic(o::JSON.Object, key::AbstractString, default, start::Int=o.i)
259259

260260
key1 = getc(key, 1) # Extract 1st byte of key.
261261
keyp = pointer(key, 2) # Cache pointer to remainder
262262
keyl = sizeof(key) # of key and length of key.
263263

264264
s = o.s # Skip from "begin" byte '{'
265-
i, c = skip_noise(s, o.i + 1) # to first field name byte.
265+
i, c = skip_noise(s, start + 1) # to first field name byte.
266266

267267
while c != '}'
268268
last_i, has_escape = scan_string(s, i) # Find end of field name and
@@ -286,6 +286,10 @@ function get_ic(o::JSON.Object, key::AbstractString, default)
286286
i, c = skip_noise(s, i + 1) # ',' and whitespace.
287287
end
288288

289+
if start != o.i
290+
return get_ic(o, key, default, o.i)
291+
end
292+
289293
return default
290294
end
291295

@@ -550,98 +554,6 @@ setc(s, i, c) = (unsafe_store!(pointer(s), c, i); i + 1)
550554

551555

552556

553-
# Struct Filling
554-
555-
"""
556-
Convert a `JSON.Object` to a `mutable struct T`.
557-
"""
558-
function convert_to_mutable(T::Type, o::JSON.Object)
559-
560-
r = ccall(:jl_new_struct_uninit, Any, (Any,), T)
561-
562-
for (k, v) in o
563-
Base.setproperty!(r, Symbol(k), v)
564-
end
565-
566-
return r
567-
end
568-
569-
570-
"""
571-
Convert a `JSON.Object` to a `struct T`.
572-
Optimised for the case where the order of the JSON fields matches the struct.
573-
"""
574-
function convert_to_immutable(T::Type, o::JSON.Object)
575-
576-
count = fieldcount(T)
577-
values = Vector{Any}(uninitialized, count)
578-
579-
i = 1
580-
for (k, v) in o
581-
j = i # Start at the first struct field.
582-
while true
583-
f = fieldname(T, j)
584-
if symcmp(k, f) # If we're lucky, `k` matches the `j`th
585-
values[j] = v # struct field and we save the value.
586-
i = j
587-
break
588-
end # If the JSON fields are out of order
589-
j += 1 # we have to search...
590-
if j > count
591-
j = 1 # After last field, start again at 1.
592-
end
593-
if j == i # Wrapped around to `i`!
594-
break # Give up.
595-
end
596-
end
597-
i += 1
598-
if i > count
599-
i = 1
600-
end
601-
end
602-
603-
return T(values...)
604-
end
605-
606-
607-
Base.convert(::Type{Any}, o::JSON.Object) = o
608-
609-
function Base.convert(T::Type, o::JSON.Object)
610-
if T.mutable
611-
return convert_to_mutable(T, o)
612-
else
613-
return convert_to_immutable(T, o)
614-
end
615-
end
616-
617-
618-
Base.convert(::Type{JSON.Object{T}},
619-
v::JSON.Object{T}) where T <: AbstractString = v
620-
621-
622-
"""
623-
Is `JSON.String` `a` equal to `Symbol` `b` ?
624-
Assumes that `a` does not contain escape sequences.
625-
"""
626-
function symcmp(a::JSON.String, b::Symbol)
627-
pa = pointer(a.s, a.i+1)
628-
pb = Base.unsafe_convert(Ptr{UInt8}, b)
629-
i = 1
630-
while true
631-
ca = unsafe_load(pa, i)
632-
cb = unsafe_load(pb, i)
633-
if ca == '"' && cb == 0x00
634-
return true
635-
end
636-
if ca != cb
637-
return false
638-
end
639-
i += 1
640-
end
641-
end
642-
643-
644-
645557
# Debug Display
646558

647559
function Base.show(io::IO, e::JSON.ParseError)

test/benchmark.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ n > 1 && println("Access 2 values close to end:")
5555
n > 1 && print("LazyJSON.jl:")
5656
GC.gc()
5757
@time for i in 1:n
58-
r = LazyJSON.parse(j).shapes.scope.enum
58+
if LazyJSON.enable_getproperty
59+
r = LazyJSON.parse(j).shapes.scope.enum
60+
else
61+
r = LazyJSON.parse(j, ["shapes", "scope", "enum"])
62+
end
5963
@assert SubString(r[1]) == "Availability Zone"
6064
@assert SubString(r[2]) == "Region"
6165
end

test/runtests.jl

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,35 @@ mutable struct Bar
2222
ov::Vector{Foo}
2323
end
2424

25+
struct Geometry
26+
_type
27+
coordinates
28+
end
29+
30+
struct CityLot
31+
_type
32+
properties
33+
geometry::Geometry
34+
end
35+
36+
struct Owner
37+
login
38+
end
39+
40+
struct Repo
41+
id
42+
name
43+
owner::Owner
44+
parent
45+
full_name
46+
private::Bool
47+
url
48+
language
49+
pushed_at
50+
permissions
51+
end
52+
53+
2554

2655

2756
@testset "JSON" begin
@@ -66,19 +95,23 @@ j = LazyJSON.value("""{
6695
]
6796
}""")
6897

98+
if LazyJSON.enable_getproperty
99+
69100
@test j isa JSON.Object || j isa JSON.PropertyDict
70101
@test j.f.a == 1
71102
@test j.f.b == "foo"
72103
@test j.v == [1, 2, 3, 4, 5] ;@test j.v isa JSON.Array
73104
@test j.s == "Hello" ;@test j.s isa JSON.String
74105
@test j.s2 == "World!" ;@test j.s2 isa JSON.String
75-
@test j.ov[1].a == 1 ;@test j.ov[1] isa JSON.PropertyDict
106+
@test j.ov[1].a == 1
76107
@test j.ov[1].b == true
77108
@test j.ov[2].a == 2
78109
@test j.ov[2].b == false
79110
@test j.ov[3].a == 3
80111
@test j.ov[3].b == nothing
81112

113+
end
114+
82115

83116
v = convert(Bar, j)
84117

@@ -96,17 +129,17 @@ v = convert(Bar, j)
96129
d = convert(Dict, j)
97130

98131
@test d isa Dict{AbstractString,Any}
99-
@test d["f"].a == 1
100-
@test d["f"].b == "foo"
132+
@test d["f"]["a"] == 1
133+
@test d["f"]["b"] == "foo"
101134
@test d["v"] == [1, 2, 3, 4, 5]
102135
@test d["s"] == "Hello" ;@test d["s"] isa JSON.String
103136
@test d["s2"] == "World!" ;@test d["s2"] isa JSON.String
104-
@test d["ov"][1].a == 1 ;@test d["ov"][1] isa JSON.PropertyDict
105-
@test d["ov"][1].b == true
106-
@test d["ov"][2].a == 2
107-
@test d["ov"][2].b == false
108-
@test d["ov"][3].a == 3
109-
@test d["ov"][3].b == nothing
137+
@test d["ov"][1]["a"] == 1
138+
@test d["ov"][1]["b"] == true
139+
@test d["ov"][2]["a"] == 2
140+
@test d["ov"][2]["b"] == false
141+
@test d["ov"][3]["a"] == 3
142+
@test d["ov"][3]["b"] == nothing
110143

111144

112145
end #testset
@@ -445,6 +478,69 @@ end # testset
445478
end # testset
446479

447480

481+
#-------------------------------------------------------------------------------
482+
@testset "citylots" begin
483+
#-------------------------------------------------------------------------------
484+
485+
json = """{
486+
"type": "FeatureCollection",
487+
"features": [
488+
{ "type": "Feature", "properties": { "MAPBLKLOT": "0001001", "BLKLOT": "0001001", "BLOCK_NUM": "0001", "LOT_NUM": "001", "FROM_ST": "0", "TO_ST": "0", "STREET": "UNKN
489+
OWN", "ST_TYPE": null, "ODD_EVEN": "E" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -122.422003528252475, 37.808480096967251, 0.0 ], [ -122.42207601332528
490+
1, 37.808835019815085, 0.0 ], [ -122.421102174348633, 37.808803534992904, 0.0 ], [ -122.421062569067274, 37.808601056818148, 0.0 ], [ -122.422003528252475, 37.8084800
491+
96967251, 0.0 ] ] ] } }
492+
,
493+
{ "type": "Feature", "properties": { "MAPBLKLOT": "0002001", "BLKLOT": "0002001", "BLOCK_NUM": "0002", "LOT_NUM": "001", "FROM_ST": "0", "TO_ST": "0", "STREET": "UNKN
494+
OWN", "ST_TYPE": null, "ODD_EVEN": "E" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -122.42082593937107, 37.808631474146033, 0.0 ], [ -122.420858049679694
495+
, 37.808795641369592, 0.0 ], [ -122.419811958704301, 37.808761809714007, 0.0 ], [ -122.42082593937107, 37.808631474146033, 0.0 ] ] ] } }
496+
497+
]}"""
498+
499+
500+
v = convert(Vector{CityLot}, LazyJSON.value(json, ["features"]))
501+
502+
@test v[1]._type == "Feature"
503+
@test v[1].properties["MAPBLKLOT"] == "0001001"
504+
505+
@test v[2]._type == "Feature"
506+
@test v[2].properties["MAPBLKLOT"] == "0002001"
507+
@test v[2].geometry.coordinates[1][1][1] == -122.42082593937107
508+
509+
end # testset
510+
511+
512+
#-------------------------------------------------------------------------------
513+
@testset "GitHub.jl" begin
514+
#-------------------------------------------------------------------------------
515+
516+
gist_json = """{
517+
"id": 1296269,
518+
"owner": {
519+
"login": "octocat"
520+
},
521+
"parent": {
522+
"name": "test-parent"
523+
},
524+
"full_name": "octocat/Hello-World",
525+
"private": false,
526+
"url": "https://api.github.com/repos/octocat/Hello-World",
527+
"language": null,
528+
"pushed_at": "2011-01-26T19:06:43Z",
529+
"permissions": {
530+
"admin": false,
531+
"push": false,
532+
"pull": true
533+
}
534+
}"""
535+
536+
v = convert(Repo, LazyJSON.value(gist_json))
537+
538+
@test v.owner.login == "octocat"
539+
540+
541+
end #testset
542+
543+
448544
#-------------------------------------------------------------------------------
449545
end # top level testset JSON
450546
#-------------------------------------------------------------------------------

0 commit comments

Comments
 (0)