Mercurial > lbo > hg > juliaplay
changeset 39:8a21e7a32029
Initial commit on JSONStructs
author | Lewin Bormann <lbo@spheniscida.de> |
---|---|
date | Sat, 25 Mar 2023 21:39:20 +0100 |
parents | bae384998d85 |
children | 5e3662b65004 |
files | julia/JSONStructs/Manifest.toml julia/JSONStructs/Project.toml julia/JSONStructs/src/JSONStructs.jl julia/JSONStructs/src/jsonparser.jl julia/JSONStructs/src/metaparser.jl julia/JSONStructs/test/runtests.jl |
diffstat | 6 files changed, 390 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/julia/JSONStructs/Manifest.toml Sat Mar 25 21:39:20 2023 +0100 @@ -0,0 +1,7 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.9.0-rc1" +manifest_format = "2.0" +project_hash = "da39a3ee5e6b4b0d3255bfef95601890afd80709" + +[deps]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/julia/JSONStructs/Project.toml Sat Mar 25 21:39:20 2023 +0100 @@ -0,0 +1,4 @@ +name = "JSONStructs" +uuid = "0621286d-cf8e-43d1-9911-199e53c1c825" +authors = ["Lewin Bormann <lewin@lewin-bormann.info>"] +version = "0.1.0"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/julia/JSONStructs/src/JSONStructs.jl Sat Mar 25 21:39:20 2023 +0100 @@ -0,0 +1,12 @@ +module JSONStructs + +include("metaparser.jl") +include("jsonparser.jl") + +import .Parser: parse_struct, parse_value +import .Metaparser: @json_parseable + +export @json_parseable +export parse_value, parse_struct + +end # module JSONStructs
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/julia/JSONStructs/src/jsonparser.jl Sat Mar 25 21:39:20 2023 +0100 @@ -0,0 +1,203 @@ + +module Parser + +# JSON parser struct. +mutable struct JP + s::String + pos::Int + length::Int +end + +@inline function JP(s::AbstractString)::JP + JP(string(s), 1, length(s)) +end + +@inline function current(jp::JP)::Char + jp.s[jp.pos] +end + +@inline function next!(jp::JP) + jp.pos += 1 +end + +@inline function isend(jp::JP) + jp.pos > jp.length +end + +@inline function reset!(jp::JP) + jp.pos = 1 +end + +const FLOAT_CHARS = ['e', '.', '-'] + +function take_num!(jp::JP)::Union{Nothing,Float64,Int} + isfloatstr(c) = c in FLOAT_CHARS + pred(c) = isdigit(c) || isfloatstr(c) + span = takewhile!(jp, pred) + if isnothing(span) + nothing + else + a, b = span + s = (@view jp.s[a:b]) + parse(Float64, s) + end +end + +function take_bool!(jp::JP)::Union{Nothing,Bool} + if expect_prefix!(jp, "true") + true + elseif expect_prefix!(jp, "false") + false + else + nothing + end +end + +function take_object!(jp::JP)::Union{Nothing,Dict{String,Any}} + expect!(jp, '{') || return nothing + + d = Dict{String,Any}() + while true + key = take_str!(jp) + if isnothing(key) + # Empty object + break + end + + expect!(jp, ':') || error("malformatted object - expecting ':'") + + val = take_val!(jp) + + d[key] = val + if expect!(jp, ',') + continue + else + # End of object + break + end + end + + expect!(jp, '}') || error("Unclosed object - '}' missing") + d +end + +function take_str!(jp::JP)::Union{Nothing,String} + expect!(jp, '"') || return nothing + + span = takewhile!(jp, (!=)('"'), false) + if isnothing(span) + error("unclosed string at $(jp.pos)") + end + + expect!(jp, '"') || error("unclosed string at $(jp.pos)") + a, b = span + jp.s[a:b] +end + +function take_list!(jp::JP)::Union{Nothing,Vector{Any}} + expect!(jp, '[') || return nothing + + l = Any[] + while true + o = take_val!(jp) + if isnothing(o) + break + else + push!(l, o) + end + + if expect!(jp, ',') + continue + else + break + end + end + + expect!(jp, ']') || error("Missing closing ']' at $(jp.pos)") + l +end + +"""value is anything - object/list/number/boolean/string""" +function take_val!(jp::JP)::Union{Nothing,Any} + n = take_num!(jp) + if !isnothing(n) + return n + end + s = take_str!(jp) + if !isnothing(s) + return s + end + l = take_list!(jp) + if !isnothing(l) + return l + end + d = take_object!(jp) + if !isnothing(d) + return d + end + b = take_bool!(jp) + if !isnothing(b) + return b + end + nothing +end + +function take_struct!(t::Type{T}, ::JP)::Union{Nothing,T} where {T} + error("JSON Parsing not implemented for type $t") +end + +function strip_ws!(jp::JP) + while !isend(jp) && isspace(jp.s[jp.pos]) + jp.pos += 1 + end +end + +function takewhile!(jp::JP, pred::Function, stripws = true)::Union{Nothing,Tuple{Int,Int}} + if stripws + strip_ws!(jp) + end + if !isend(jp) && pred(current(jp)) + a = jp.pos + while !isend(jp) && pred(current(jp)) + next!(jp) + end + (a, jp.pos - 1) + else + nothing + end +end + +@inline function expect!(jp::JP, c::Char)::Bool + strip_ws!(jp) + if current(jp) == c + next!(jp) + true + else + false + end +end + +function expect_prefix!(jp::JP, pref::AbstractString)::Bool + strip_ws!(jp) + pl = length(pref) + + if (@view jp.s[jp.pos:min(jp.length, jp.pos + pl - 1)]) == pref + jp.pos += pl + true + else + false + end +end + +"""Parse a json value..""" +function parse_value(s::AbstractString) + jp = JP(s) + take_val!(jp) +end + +"""Parse a struct that is marked with @json_parseable.""" +function parse_struct(t::Type{T}, s::AbstractString)::T where {T} + take_struct!(t, JP(s)) +end + +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/julia/JSONStructs/src/metaparser.jl Sat Mar 25 21:39:20 2023 +0100 @@ -0,0 +1,145 @@ +module Metaparser + +function parse_struct_expr(s::Expr) + s.head == :struct || error("Expr must be struct but is $s") + args = s.args[2:end] + typedef = args[1] + + fields = args[2] + fields.head == :block || error("Expr must contain a block but has $fields") + fields_exprs = Pair[] + for f in fields.args + if typeof(f) == LineNumberNode + continue + end + typeof(f) == Expr || error("Field $f in $typedef must have type!") + f.head == :(::) || error( + "Field $f in $typedef must be type! (we don't support constructors etc. - struct must be plain)", + ) + name, typ = f.args + + is_type = (typeof(typ) == Symbol || (typeof(typ) == Expr && typ.head == :curly)) + is_type || + error("Type of $f should be symbol (simple type) but is $(typeof(typ)): $typ") + + typ = eval(typ) + isconcretetype(typ) || + error("Type of field $name must be concrete, but is not (is $typ)!") + + push!(fields_exprs, name => eval(typ)) + end + + fields_exprs +end + +function get_type_of_struct(s::Expr) + s.head == :struct || error("Expr must be struct, is $s") + typedef = s.args[2] + + if typeof(typedef) == Symbol + typedef + elseif typeof(typedef) == Expr + error("We don't support generic structs yet :(") + end +end + +function map_type_to_parse_method(::Type{<:Number})::Symbol + :take_num! +end +function map_type_to_parse_method(::Type{<:AbstractString})::Symbol + :take_str! +end +function map_type_to_parse_method(::Type{Bool})::Symbol + :take_bool! +end +function map_type_to_parse_method(::Type{<:AbstractDict})::Symbol + :take_object! +end +function map_type_to_parse_method(::Type{<:AbstractVector})::Symbol + :take_list! +end +function map_type_to_parse_method(::Type{T})::Symbol where {T} + error("unexpected type") + if isstructtype(T) + :take_struct! + else + error("Unknown type $T for JSON parsing!") + end +end + +function check_variables_filled_expr(varnames::AbstractVector{Symbol})::Vector{Expr} + check_var_cond(sym) = begin + syms = string(sym) + quote + syms = $syms + if isnothing($sym) + error("Field $syms not given in JSON object!") + end + end + end + [check_var_cond(s) for s in varnames] +end + +function json_parseable(strct) + typs::Vector{Pair{Symbol,Type}} = parse_struct_expr(strct) + typ = get_type_of_struct(strct) + + fieldvars = [:($(name)::Union{$typ,Nothing} = nothing) for (name, typ) in typs] + fieldnames = [name for (name, _) in typs] + fields_filled_cond = check_variables_filled_expr(fieldnames) + Mod = :(JSONStructs.Parser) + + methods = [(name, map_type_to_parse_method(typ)) for (name, typ) in typs] + field_dispatch = [ + quote + if !matched && key == $(string(name)) + $name = $Mod.$(method)(jp) + matched = true + end + end for (name, method) in methods + ] + + quote + $strct + + function $Mod.take_struct!(::Type{$typ}, jp::$(Mod).JP)::Union{Nothing,$typ} + $Mod.expect!(jp, '{') || return nothing + + $(fieldvars...) + + while true + key = $Mod.take_str!(jp) + if isnothing(key) + break + end + + $Mod.expect!(jp, ':') || error("malformed object - expected ':'") + + matched = false + $(field_dispatch...) + + if !matched + $Mod.take_val!(jp) + end + + if $Mod.expect!(jp, ',') + continue + else + break + end + end + + $(fields_filled_cond...) + + $Mod.expect!(jp, '}') || error("unclosed object") + + $(typ)($(fieldnames...)) + end + end |> esc +end + +macro json_parseable(strct) + json_parseable(strct) +end + +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/julia/JSONStructs/test/runtests.jl Sat Mar 25 21:39:20 2023 +0100 @@ -0,0 +1,19 @@ +# Some basic tests. + +using JSONStructs + +@json_parseable struct TestStruct1 + a::Int + b::Float64 + c::Vector{String} +end + +function test_parse_1() + json = "{\"a\": 33, \"b\": 55.55, \"c\": [\"xyz\", \"abc\"]}" + have = parse_struct(TestStruct1, json) + want = TestStruct1(33, 55.55, ["xyz", "abc"]) + @assert string(have) == string(want) "$have == $want" +end + + +test_parse_1()