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