I would like to have a quick and dirty way to load/save custom julia structs to a human readable format, say JSON.
Goals
- Human readable
- Works out of the box for user defined structs
- Should be simple to implement
- Should not require passing type information
Non Goals
- Performance
- Security
Here is what I tried so far:
module H
using ArgCheck
using ConstructionBase: constructorof
KEY_META = "__meta__"
KEY_CONSTRUCTOR = "constructor"
KEY_PROPERTYNAMES = "propertynames"
function marshal_struct(o)
d = Dict{String, Any}()
meta = Dict(
KEY_CONSTRUCTOR => string(constructorof(typeof(o))),
KEY_PROPERTYNAMES => collect(map(string, propertynames(o))),
)
d[KEY_META] = meta
for prop in propertynames(o)
key = string(prop)
d[key] = marshal(getproperty(o, prop))
end
d
end
marshal(o) = marshal_struct(o)
marshal(o::String) = o
marshal(o::Number) = o
unmarshal(o::String, ctx) = o
unmarshal(o::Number, ctx) = o
function unmarshal(d::Dict, eval)
@argcheck haskey(d, KEY_META)
meta = d[KEY_META]
@check haskey(meta, KEY_CONSTRUCTOR)
@check haskey(meta, KEY_PROPERTYNAMES)
ctor_str = meta[KEY_CONSTRUCTOR]
@check ctor_str isa String
ctor_expr = nothing
try
ctor_expr = Meta.parse(ctor_str)
catch err
msg = """Error parsing constructor:
string: $ctor_str
error: $err
"""
error(msg)
end
ctor = nothing
try
ctor = eval(ctor_expr)
catch err
msg = """Error evaluating constructor expression:
string: $ctor_str
expr: $ctor_expr
error: $err
"""
error(msg)
end
propnames = meta[KEY_PROPERTYNAMES]
args = map(propnames) do key
unmarshal(d[key], eval)
end
ctor(args...)
end
end#module
struct S{A,B}
a::A
b::B
end
s = S(1, 2.0)
d = H.marshal(s)
s2 = H.unmarshal(d, eval)
@show s
@show d
@show s2
s = S{Int64,Float64}(1, 2.0)
d = Dict{String,Any}("b" => 2.0,"__meta__" => Dict{String,Any}("propertynames" => ["a", "b"],"constructor" => "S"),"a" => 1)
s2 = S{Int64,Float64}(1, 2.0)
In order to load user defined structs, one needs to call eval
at some point. My problem is, that this eval
must be the eval
of the calling module, not the eval
of the module that defines the marshaling functionality. That is why the user needs to pass his eval
…
How to avoid passing the user module eval
?
JLD.jl
must have solved this problem, but I cannot understand the code.
Or is there another approach?