Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation for StructuralContext to README #300

Merged
merged 2 commits into from
Jan 10, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,166 @@ JSON.lower(p::Point2D) = [p.x, p.y]

Define a custom serialization rule for a particular data type. Must return a
value that can be directly serialized; see help for more details.

### Customizing JSON

Users may find the default behaviour of JSON inappropriate for their use case. In
such cases, JSON provides two mechanisms for users to customize serialization. The
first method, `JSON.Writer.StructuralContext`, is used to customize the cosmetic
properties of the serialized JSON. (For example, the default pretty printing vs.
compact printing is supported by provided two different `StructuralContext`s.)
Examples of applications for which `StructuralContext` is appropriate include:
particular formatting demands for JSON (maybe not in compliance with the JSON
standard) or JSON-like formats with different syntax.

The second method, `JSON.Serializations.Serialization`, is used to control the
translation of Julia objects into JSON serialization instructions. In most cases,
writing a method for `JSON.lower` (as mentioned above) is sufficient to define
JSON serializations for user-defined objects. However, this is not appropriate for
overriding or deleting predefined serializations (since that would globally affect
users of the `JSON` module and is an instance of dangerous
[type piracy](https://docs.julialang.org/en/v1/manual/style-guide/index.html#Avoid-type-piracy-1)).
For these use-cases, users should define a custom instance of `Serialization`.
An example of an application for this use case includes: a commonly requested
extension to JSON which serializes float NaN and infinite values as `NaN` or `Inf`,
in contravention of the JSON standard.

Both methods are controlled by the `JSON.show_json` function, which has the following
signature:

```
JSON.show_json(io::StructuralContext, serialization::Serialization, object)
```

which is expected to write to `io` in a way appropriate based on the rules of
`Serialization`, but here `io` is usually (but not required to be) handled in a
higher-level manner than a raw `IO` object would ordinarily be.

#### StructuralContext

To define a new `StructuralContext`, the following boilerplate is recommended:

```julia
import JSON.Writer.StructuralContext
[mutable] struct MyContext <: StructuralContext
io::IO
[ ... additional state / settings for context goes here ... ]
end
```

If your structural context is going to be very similar to the existing JSON
contexts, it is also possible to instead subtype the abstract subtype
`JSONContext` of `StructuralContext`. If this is the case, an `io::IO` field (as
above) is preferred, although the default implementation will only use this
for `write`, so replacing that method is enough to avoid this requirement.

The following methods should be defined for your context, regardless of whether it
subtypes `JSONContext` or `StructuralContext` directly. If some of these methods
are omitted, then `CommonSerialization` cannot be generally used with this context.

```
# called when the next object in a vector or next pair of a dict is to be written
# (requiring a newline and indent for some contexts)
# can do nothing if the context need not support indenting
JSON.Writer.indent(io::MyContext)

# called for vectors/dicts to separate items, usually writes ","
# unless this is the first element in a JSON array
# (default implementation for JSONContext exists, but requires a mutable bool
# `first` field, and this is an implementation detail not to be relied on;
# to define own or delegate explicitly)
JSON.Writer.delimit(io::MyContext)

# called for dicts to separate key and value, usually writes ": "
JSON.Writer.separate(io::MyContext)

# called to indicate start and end of a vector
JSON.Writer.begin_array(io::MyContext)
JSON.Writer.end_array(io::MyContext)

# called to indicate start and end of a dict
JSON.Writer.begin_object(io::MyContext)
JSON.Writer.end_object(io::MyContext)
```

For the following methods, `JSONContext` provides a default implementation,
but it can be specialized. For `StructuralContext`s which are not
`JSONContext`s, the `JSONContext` defaults are not appropriate and so are
not available.

```julia
# directly write a specific byte (if supported)
# default implementation writes to underlying `.io` field
# note that this enables JSONContext to act as any `io::IO`,
# i.e. one can use `print`, `show`, etc.
Base.write(io::MyContext, byte::UInt8)

# write "null"
# default implementation writes to underlying `.io` field
JSON.Writer.show_null(io::MyContext)

# write an object or string in a manner safe for JSON string
# default implementation calls `print` but escapes each byte as appropriate
# and adds double quotes around the content of `elt`
JSON.Writer.show_string(io::MyContext, elt)

# write a new element of JSON array
# default implementation calls delimit, then indent, then show_json
JSON.Writer.show_element(io::MyContext, elt)

# write a key for a JSON object
# default implementation calls delimit, then indent, then show_string,
# then seperate
JSON.Writer.show_key(io::MyContext, elt)

# write a key-value pair for a JSON object
# default implementation calls show key, then show_json
JSON.Writer.show_pair(io::MyContext, s::Serialization, key, value)
```

What follows is an example of a `JSONContext` subtype which is very similar
to the default context, but which uses `None` instead of `null` for JSON nulls,
which is then generally compatible with Python object literal notation (PYON). It
wraps a default `JSONContext` to delegate all the required methods to. Since
the wrapped context already has a `.io`, this object does not need to include
an `.io` field, and so the `write` method must also be delegated, since the default
is not appropriate. The only other specialization needed is `show_null`.

```julia
import JSON.Writer
import JSON.Writer.JSONContext
mutable struct PYONContext <: JSONContext
underlying::JSONContext
end

for delegate in [:indent,
:delimit,
:separate,
:begin_array,
:end_array,
:begin_object,
:end_object]
@eval JSON.Writer.$delegate(io::PYONContext) = JSON.Writer.$delegate(io.underlying)
end
Base.write(io::PYONContext, byte::UInt8) = write(io.underlying, byte)

JSON.Writer.show_null(io::PYONContext) = print(io, "None")
pyonprint(io::IO, obj) = let io = PYONContext(JSON.Writer.PrettyContext(io, 4))
JSON.print(io, obj)
return
end
```

The usage of this `pyonprint` function is as any other `print` function, e.g.

```julia
julia> pyonprint(stdout, [1, 2, nothing])
[
1,
2,
None
]

julia> sprint(pyonprint, missing)
"None"
```