diff --git a/src/JSON.jl b/src/JSON.jl index 86cbabc..4f47863 100644 --- a/src/JSON.jl +++ b/src/JSON.jl @@ -7,27 +7,32 @@ using Compat export json # returns a compact (or indented) JSON representation as a string include("Common.jl") + +# Parser modules include("Parser.jl") +# Writer modules +include("Serializations.jl") + using .Common import .Parser.parse +import .Serializations.StandardSerialization + +""" +Internal JSON.jl implementation detail; do not depend on this type. -# These are temporary ways to bypass excess memory allocation -# They can be removed once types define their own serialization behaviour again -"Internal JSON.jl implementation detail; do not depend on this type." -immutable AssociativeWrapper{T} <: Associative{Symbol, Any} +A JSON primitive that wraps around any composite type to enable `Dict`-like +serialization. +""" +immutable CompositeTypeWrapper{T} wrapped::T fns::Vector{Symbol} end -AssociativeWrapper(x) = AssociativeWrapper(x, fieldnames(x)) +CompositeTypeWrapper(x) = CompositeTypeWrapper(x, fieldnames(x)) const JSONPrimitive = Union{ Associative, Tuple, AbstractArray, AbstractString, Integer, - AbstractFloat, Bool, Void} - -Base.getindex(w::AssociativeWrapper, s::Symbol) = getfield(w.wrapped, s) -Base.keys(w::AssociativeWrapper) = w.fns -Base.length(w::AssociativeWrapper) = length(w.fns) + AbstractFloat, Void, CompositeTypeWrapper} """ Return a value of a JSON-encodable primitive type that `x` should be lowered @@ -44,7 +49,14 @@ Note that the return value need not be *recursively* lowered—this function may for instance return an `AbstractArray{Any, 1}` whose elements are not JSON primitives. """ -lower(a) = AssociativeWrapper(a) +function lower(a) + if nfields(typeof(a)) > 0 + CompositeTypeWrapper(a) + else + error("Cannot serialize type $(typeof(a))") + end +end + lower(a::JSONPrimitive) = a if isdefined(Base, :Dates) @@ -62,206 +74,253 @@ lower(d::Type) = string(d) lower(m::Module) = throw(ArgumentError("cannot serialize Module $m as JSON")) lower(x::Real) = Float64(x) -const INDENT=true -const NOINDENT=false - -@compat type State{I} - indentstep::Int - indentlen::Int - prefix::AbstractString - otype::Vector{Bool} - (::Type{State{I}}){I}(indentstep::Int) = new{I}(indentstep, - 0, - "", - Bool[]) +""" +Abstract supertype of all JSON and JSON-like structural writer contexts. +""" +@compat abstract type StructuralContext <: IO end + +""" +Internal implementation detail. + +A JSON structural context around an `IO` object. Structural writer contexts +define the behaviour of serializing JSON structural objects, such as objects, +arrays, and strings to JSON. The translation of Julia types to JSON structural +objects is not handled by a `JSONContext`, but by a `Serialization` wrapper +around it. Abstract supertype of `PrettyContext` and `CompactContext`. Data can +be written to a JSON context in the usual way, but often higher-level operations +such as `begin_array` or `begin_object` are preferred to directly writing bytes +to the stream. +""" +@compat abstract type JSONContext <: StructuralContext end + +""" +Internal implementation detail. + +Keeps track of the current location in the array or object, which winds and +unwinds during serialization. +""" +type PrettyContext{T<:IO} <: JSONContext + io::T + step::Int # number of spaces to step + state::Int # number of steps at present + first::Bool # whether an object/array was just started end -State(indentstep::Int=0) = State{indentstep>0}(indentstep) +PrettyContext(io::IO, step) = PrettyContext(io, step, 0, false) + +""" +Internal implementation detail. -function set_state(state::State{INDENT}, operate::Int) - state.indentlen += state.indentstep * operate - state.prefix = " "^state.indentlen +For compact printing, which in JSON is fully recursive. +""" +type CompactContext{T<:IO} <: JSONContext + io::T + first::Bool end +CompactContext(io::IO) = CompactContext(io, false) -set_state(state::State{NOINDENT}, operate::Int) = nothing +""" +Internal implementation detail. -suffix(::State{INDENT}) = "\n" -suffix(::State{NOINDENT}) = "" +Implements an IO context safe for printing into JSON strings. +""" +immutable StringContext{T<:IO} <: IO + io::T +end -prefix(s::State{INDENT}) = s.prefix -prefix(::State{NOINDENT}) = "" +# These make defining additional methods on `show_json` easier. +const CS = Serializations.CommonSerialization +const SC = StructuralContext -separator(::State{INDENT}) = ": " -separator(::State{NOINDENT}) = ":" +# Low-level direct access +Base.write(io::JSONContext, byte::UInt8) = write(io.io, byte) +Base.write(io::StringContext, byte::UInt8) = + write(io.io, ESCAPED_ARRAY[byte + 0x01]) +#= turn on if there's a performance benefit +write(io::StringContext, char::Char) = + char <= '\x7f' ? write(io, ESCAPED_ARRAY[@compat UInt8(c) + 0x01]) : + Base.print(io, c) +=# -# short hand for printing suffix then prefix -printsp(io::IO, state::State{INDENT}) = Base.print(io, suffix(state), prefix(state)) -printsp(io::IO, state::State{NOINDENT}) = nothing +""" +Internal implementation detail. -function start_object(io::IO, state::State{INDENT}, is_dict::Bool) - push!(state.otype, is_dict) - Base.print(io, is_dict ? "{": "[", suffix(state)) - set_state(state, 1) +If appropriate, write a newline, then indent the IO by the appropriate number of +spaces. Otherwise, do nothing. +""" +@inline function indent(io::PrettyContext) + write(io, NEWLINE) + for _ in 1:io.state + write(io, SPACE) + end end +@inline indent(io::CompactContext) = nothing -function start_object(io::IO, state::State{NOINDENT}, is_dict::Bool) - Base.print(io, is_dict ? "{": "[") -end +""" +Internal implementation detail. -function end_object(io::IO, state::State{INDENT}, is_dict::Bool) - set_state(state, -1) - pop!(state.otype) - printsp(io, state) - Base.print(io, is_dict ? "}": "]") -end +Write a colon, followed by a space if appropriate. +""" +@inline separate(io::PrettyContext) = write(io, SEPARATOR, SPACE) +@inline separate(io::CompactContext) = write(io, SEPARATOR) -function end_object(io::IO, state::State{NOINDENT}, is_dict::Bool) - Base.print(io, is_dict ? "}": "]") -end +""" +Internal implementation detail. -function print_escaped(io::IO, s::AbstractString) - @inbounds for c in s - c <= '\x7f' ? Base.write(io, ESCAPED_ARRAY[@compat UInt8(c) + 0x01]) : - Base.print(io, c) #JSON is UTF8 encoded +If this is not the first item written in a collection, write a comma in the IO. +Otherwise, do not write a comma, but set a flag that the first element has been +written already. +""" +@inline function delimit(io::JSONContext) + if !io.first + write(io, DELIMITER) end + io.first = false end -function print_escaped(io::IO, s::Compat.UTF8String) - @inbounds for c in Vector{UInt8}(s) - Base.write(io, ESCAPED_ARRAY[c + 0x01]) +for kind in ("object", "array") + beginfn = Symbol("begin_", kind) + beginsym = Symbol(uppercase(kind), "_BEGIN") + endfn = Symbol("end_", kind) + endsym = Symbol(uppercase(kind), "_END") + ctxfn = Symbol("json_", kind) + # Begin and end objects + @eval function $beginfn(io::PrettyContext) + write(io, $beginsym) + io.state += io.step + io.first = true end + @eval $beginfn(io::CompactContext) = (write(io, $beginsym); io.first = true) + @eval function $endfn(io::PrettyContext) + io.state -= io.step + if !io.first + indent(io) + end + write(io, $endsym) + io.first = false + end + @eval $endfn(io::CompactContext) = (write(io, $endsym); io.first = false) + @eval $ctxfn(f, io::JSONContext) = ($beginfn(io); f(io); $endfn(io)) end -function _writejson(io::IO, state::State, s::AbstractString) - Base.print(io, '"') - JSON.print_escaped(io, s) - Base.print(io, '"') +function show_string(io::IO, x) + write(io, STRING_DELIM) + Base.print(StringContext(io), x) + write(io, STRING_DELIM) end -# workaround for issue in Julia 0.5.x where Float32 values are printed as -# 3.4f-5 instead of 3.4e-5 -if v"0.5-" <= VERSION < v"0.6.0-dev.788" - _writejson(io::IO, state::State, s::Float32) = _writejson(io, state, Float64(s)) +show_null(io::IO) = Base.print(io, "null") + +function show_element(io::JSONContext, s, x) + delimit(io) + indent(io) + show_json(io, s, x) end -function _writejson(io::IO, state::State, s::Union{Integer, AbstractFloat}) - if isnan(s) || isinf(s) - Base.print(io, "null") +function show_key(io::JSONContext, k) + delimit(io) + indent(io) + show_string(io, k) + separate(io) +end + +function show_pair(io::JSONContext, s, k, v) + show_key(io, k) + show_json(io, s, v) +end +show_pair(io::JSONContext, s, kv) = show_pair(io, s, first(kv), last(kv)) + +# Default serialization rules for CommonSerialization (CS) +show_json(io::SC, ::CS, x::Union{AbstractString, Symbol}) = show_string(io, x) + +function show_json(io::SC, s::CS, x::Union{Integer, AbstractFloat}) + # workaround for issue in Julia 0.5.x where Float32 values are printed as + # 3.4f-5 instead of 3.4e-5 + @static if v"0.5-" <= VERSION < v"0.6.0-dev.788" + if isa(x, Float32) + return show_json(io, s, Float64(x)) + end + end + if isfinite(x) + Base.print(io, x) else - Base.print(io, s) + show_null(io) end end -function _writejson(io::IO, state::State, n::Void) - Base.print(io, "null") -end +show_json(io::SC, ::CS, ::Void) = show_null(io) -function _writejson(io::IO, state::State, a::Nullable) +function show_json(io::SC, s::CS, a::Nullable) if isnull(a) Base.print(io, "null") else - _writejson(io, state, get(a)) + show_json(io, s, get(a)) end end -function _writejson(io::IO, state::State, a::Associative) - if length(a) == 0 - Base.print(io, "{}") - return +function show_json(io::SC, s::CS, a::Associative) + begin_object(io) + for kv in a + show_pair(io, s, kv) end - start_object(io, state, true) - first = true - for key in keys(a) - first ? (first = false) : Base.print(io, ",", suffix(state)) - Base.print(io, prefix(state)) - _writejson(io, state, string(key)) - Base.print(io, separator(state)) - _writejson(io, state, a[key]) - end - end_object(io, state, true) + end_object(io) end -function _writejson(io::IO, state::State, a::Union{AbstractVector,Tuple}) - if length(a) == 0 - Base.print(io, "[]") - return - end - start_object(io, state, false) - Base.print(io, prefix(state)) - i = start(a) - !done(a,i) && ((x, i) = next(a, i); _writejson(io, state, x); ) - - while !done(a,i) - (x, i) = next(a, i) - Base.print(io, ",") - printsp(io, state) - _writejson(io, state, x) +function show_json(io::SC, s::CS, x::CompositeTypeWrapper) + begin_object(io) + fns = x.fns + for k in 1:length(fns) + show_pair(io, s, fns[k], getfield(x.wrapped, k)) end - end_object(io, state, false) + end_object(io) end -function _writejson(io::IO, state::State, a) - # FIXME: This fallback is harming performance substantially. - # Remove this fallback when _print removed. - if applicable(_print, io, state, a) - Base.depwarn( - "Overloads to `_print` are deprecated; extend `lower` instead.", - :_print) - _print(io, state, a) - else - _writejson(io, state, lower(a)) +function show_json(io::SC, s::CS, x::Union{AbstractVector, Tuple}) + begin_array(io) + for elt in x + show_element(io, s, elt) end + end_array(io) end -# Note: Arrays are printed in COLUMN MAJOR format. -# i.e. json([1 2 3; 4 5 6]) == "[[1,4],[2,5],[3,6]]" -function _writejson{T, N}(io::IO, state::State, a::AbstractArray{T, N}) - lengthN = size(a, N) - if lengthN > 0 - start_object(io, state, false) - if VERSION <= v"0.3" - newdims = ntuple(N - 1, i -> 1:size(a, i)) - else - newdims = ntuple(i -> 1:size(a, i), N - 1) - end - Base.print(io, prefix(state)) - _writejson(io, state, Compat.view(a, newdims..., 1)) - - for j in 2:lengthN - Base.print(io, ",") - printsp(io, state) - _writejson(io, state, Compat.view(a, newdims..., j)) - end - end_object(io, state, false) - else - Base.print(io, "[]") +""" +Serialize a multidimensional array to JSON in column-major format. That is, +`json([1 2 3; 4 5 6]) == "[[1,4],[2,5],[3,6]]"`. +""" +function show_json{T,n}(io::SC, s::CS, A::AbstractArray{T,n}) + begin_array(io) + newdims = ntuple(_ -> :, Val{n - 1}) + for j in 1:size(A, n) + show_element(io, s, Compat.view(A, newdims..., j)) end + end_array(io) end -# this is _print() instead of _print because we need to support v0.3 -# FIXME: drop the parentheses when v0.3 support dropped -"Deprecated way to overload JSON printing behaviour. Use `lower` instead." -function _print(io::IO, s::State, a::JSONPrimitive) - Base.depwarn( - "Do not call internal function `JSON._print`; use `JSON.print`", - :_print) - _writejson(io, s, a) -end +show_json(io::SC, s::CS, a) = show_json(io, s, lower(a)) -function print(io::IO, a, indent=0) - _writejson(io, State(indent), a) - if indent > 0 - Base.print(io, "\n") +# Fallback show_json for non-SC types +""" +Serialize Julia object `obj` to IO `io` using the behaviour described by `s`. If +`indent` is provided, then the JSON will be pretty-printed; otherwise it will be +printed on one line. If pretty-printing is enabled, then a trailing newline will +be printed; otherwise there will be no trailing newline. +""" +function show_json(io::IO, s::Serializations.Serialization, obj; indent=nothing) + ctx = indent === nothing ? CompactContext(io) : PrettyContext(io, indent) + show_json(ctx, s, obj) + if indent !== nothing + println(io) end end -function print(a, indent=0) - _writejson(STDOUT, State(indent), a) - if indent > 0 - println() - end -end +print(io::IO, obj, indent) = + show_json(io, StandardSerialization(), obj; indent=indent) +print(io::IO, obj) = show_json(io, StandardSerialization(), obj) + +print(a, indent) = print(STDOUT, a, indent) +print(a) = print(STDOUT, a) -json(a, indent=0) = sprint(JSON.print, a, indent) +json(a) = sprint(JSON.print, a) +json(a, indent) = sprint(JSON.print, a, indent) function parsefile{T<:Associative}(filename::AbstractString; dicttype::Type{T}=Dict{Compat.UTF8String, Any}, use_mmap=true) sz = filesize(filename) diff --git a/src/Serializations.jl b/src/Serializations.jl new file mode 100644 index 0000000..11709e0 --- /dev/null +++ b/src/Serializations.jl @@ -0,0 +1,40 @@ +""" +JSON writer serialization contexts. + +This module defines the `Serialization` abstract type and several concrete +implementations, as they relate to JSON. +""" + +module Serializations + +using Compat + +""" +A `Serialization` defines how objects are lowered to JSON format. +""" +@compat abstract type Serialization end + +""" +The `CommonSerialization` comes with a default set of rules for serializing +Julia types to their JSON equivalents. Additional rules are provided either by +packages explicitly defining `JSON.show_json` for this serialization, or by the +`JSON.lower` method. Most concrete implementations of serializers should subtype +`CommonSerialization`, unless it is desirable to bypass the `lower` system, in +which case `Serialization` should be subtyped. +""" +@compat abstract type CommonSerialization <: Serialization end + +""" +The `StandardSerialization` defines a common, standard JSON serialization format +that is optimized to: + +- strictly follow the JSON standard +- be useful in the greatest number of situations + +All serializations defined for `CommonSerialization` are inherited by +`StandardSerialization`. It is therefore generally advised to add new +serialization behaviour to `CommonSerialization`. +""" +immutable StandardSerialization <: CommonSerialization end + +end diff --git a/test/runtests.jl b/test/runtests.jl index e56ae36..14a17fb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -308,5 +308,8 @@ end # Lowering tests include("lowering.jl") +# Custom serializing tests +include("serializer.jl") + # Check that printing to the default STDOUT doesn't fail JSON.print(["JSON.jl tests pass!"],1) diff --git a/test/serializer.jl b/test/serializer.jl new file mode 100644 index 0000000..98fd2e9 --- /dev/null +++ b/test/serializer.jl @@ -0,0 +1,91 @@ +module TestSerializer + +using JSON +using Base.Test +using Compat +import Compat: String + +# to define a new serialization behaviour, import these first +import JSON.Serializations: CommonSerialization, StandardSerialization +import JSON: StructuralContext + +# those names are long so we can define some type aliases +const CS = CommonSerialization +const SC = StructuralContext + +# for test harness purposes +function sprint_kwarg(f, args...; kwargs...) + b = IOBuffer() + f(b, args...; kwargs...) + String(take!(b)) +end + +# issue #168: Print NaN and Inf as Julia would +immutable NaNSerialization <: CS end +JSON.show_json(io::SC, ::NaNSerialization, f::AbstractFloat) = + Base.print(io, f) + +@test sprint(JSON.show_json, NaNSerialization(), [NaN, Inf, -Inf, 0.0]) == + "[NaN,Inf,-Inf,0.0]" + +@test sprint_kwarg( + JSON.show_json, + NaNSerialization(), + [NaN, Inf, -Inf, 0.0]; + indent=4 +) == """ +[ + NaN, + Inf, + -Inf, + 0.0 +] +""" + +# issue #170: Print JavaScript functions directly +immutable JSSerialization <: CS end +immutable JSFunction + data::Compat.UTF8String +end + +function JSON.show_json(io::SC, ::JSSerialization, f::JSFunction) + first = true + for line in split(f.data, '\n') + if !first + JSON.indent(io) + end + first = false + Base.print(io, line) + end +end + +@test sprint_kwarg(JSON.show_json, JSSerialization(), Any[ + 1, + 2, + JSFunction("function test() {\n return 1;\n}") +]; indent=2) == """ +[ + 1, + 2, + function test() { + return 1; + } +] +""" + +# test serializing a type without any fields +immutable SingletonType end +@test_throws ErrorException json(SingletonType()) + +# test printing to STDOUT +let filename = tempname() + open(filename, "w") do f + redirect_stdout(f) do + JSON.print(Any[1, 2, 3.0]) + end + end + @test readstring(filename) == "[1,2,3.0]" + rm(filename) +end + +end