237 lines
6.4 KiB
Elixir
237 lines
6.4 KiB
Elixir
|
defprotocol Jason.Encoder do
|
||
|
@moduledoc """
|
||
|
Protocol controlling how a value is encoded to JSON.
|
||
|
|
||
|
## Deriving
|
||
|
|
||
|
The protocol allows leveraging the Elixir's `@derive` feature
|
||
|
to simplify protocol implementation in trivial cases. Accepted
|
||
|
options are:
|
||
|
|
||
|
* `:only` - encodes only values of specified keys.
|
||
|
* `:except` - encodes all struct fields except specified keys.
|
||
|
|
||
|
By default all keys except the `:__struct__` key are encoded.
|
||
|
|
||
|
## Example
|
||
|
|
||
|
Let's assume a presence of the following struct:
|
||
|
|
||
|
defmodule Test do
|
||
|
defstruct [:foo, :bar, :baz]
|
||
|
end
|
||
|
|
||
|
If we were to call `@derive Jason.Encoder` just before `defstruct`,
|
||
|
an implementation similar to the following implementation would be generated:
|
||
|
|
||
|
defimpl Jason.Encoder, for: Test do
|
||
|
def encode(value, opts) do
|
||
|
Jason.Encode.map(Map.take(value, [:foo, :bar, :baz]), opts)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
If we called `@derive {Jason.Encoder, only: [:foo]}`, an implementation
|
||
|
similar to the following implementation would be generated:
|
||
|
|
||
|
defimpl Jason.Encoder, for: Test do
|
||
|
def encode(value, opts) do
|
||
|
Jason.Encode.map(Map.take(value, [:foo]), opts)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
If we called `@derive {Jason.Encoder, except: [:foo]}`, an implementation
|
||
|
similar to the following implementation would be generated:
|
||
|
|
||
|
defimpl Jason.Encoder, for: Test do
|
||
|
def encode(value, opts) do
|
||
|
Jason.Encode.map(Map.take(value, [:bar, :baz]), opts)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
The actually generated implementations are more efficient computing some data
|
||
|
during compilation similar to the macros from the `Jason.Helpers` module.
|
||
|
|
||
|
## Explicit implementation
|
||
|
|
||
|
If you wish to implement the protocol fully yourself, it is advised to
|
||
|
use functions from the `Jason.Encode` module to do the actual iodata
|
||
|
generation - they are highly optimized and verified to always produce
|
||
|
valid JSON.
|
||
|
"""
|
||
|
|
||
|
@type t :: term
|
||
|
@type opts :: Jason.Encode.opts()
|
||
|
|
||
|
@fallback_to_any true
|
||
|
|
||
|
@doc """
|
||
|
Encodes `value` to JSON.
|
||
|
|
||
|
The argument `opts` is opaque - it can be passed to various functions in
|
||
|
`Jason.Encode` (or to the protocol function itself) for encoding values to JSON.
|
||
|
"""
|
||
|
@spec encode(t, opts) :: iodata
|
||
|
def encode(value, opts)
|
||
|
end
|
||
|
|
||
|
defimpl Jason.Encoder, for: Any do
|
||
|
defmacro __deriving__(module, struct, opts) do
|
||
|
fields = fields_to_encode(struct, opts)
|
||
|
kv = Enum.map(fields, &{&1, generated_var(&1, __MODULE__)})
|
||
|
escape = quote(do: escape)
|
||
|
encode_map = quote(do: encode_map)
|
||
|
encode_args = [escape, encode_map]
|
||
|
kv_iodata = Jason.Codegen.build_kv_iodata(kv, encode_args)
|
||
|
|
||
|
quote do
|
||
|
defimpl Jason.Encoder, for: unquote(module) do
|
||
|
require Jason.Helpers
|
||
|
|
||
|
def encode(%{unquote_splicing(kv)}, {unquote(escape), unquote(encode_map)}) do
|
||
|
unquote(kv_iodata)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# The same as Macro.var/2 except it sets generated: true
|
||
|
defp generated_var(name, context) do
|
||
|
{name, [generated: true], context}
|
||
|
end
|
||
|
|
||
|
def encode(%_{} = struct, _opts) do
|
||
|
raise Protocol.UndefinedError,
|
||
|
protocol: @protocol,
|
||
|
value: struct,
|
||
|
description: """
|
||
|
Jason.Encoder protocol must always be explicitly implemented.
|
||
|
|
||
|
If you own the struct, you can derive the implementation specifying \
|
||
|
which fields should be encoded to JSON:
|
||
|
|
||
|
@derive {Jason.Encoder, only: [....]}
|
||
|
defstruct ...
|
||
|
|
||
|
It is also possible to encode all fields, although this should be \
|
||
|
used carefully to avoid accidentally leaking private information \
|
||
|
when new fields are added:
|
||
|
|
||
|
@derive Jason.Encoder
|
||
|
defstruct ...
|
||
|
|
||
|
Finally, if you don't own the struct you want to encode to JSON, \
|
||
|
you may use Protocol.derive/3 placed outside of any module:
|
||
|
|
||
|
Protocol.derive(Jason.Encoder, NameOfTheStruct, only: [...])
|
||
|
Protocol.derive(Jason.Encoder, NameOfTheStruct)
|
||
|
"""
|
||
|
end
|
||
|
|
||
|
def encode(value, _opts) do
|
||
|
raise Protocol.UndefinedError,
|
||
|
protocol: @protocol,
|
||
|
value: value,
|
||
|
description: "Jason.Encoder protocol must always be explicitly implemented"
|
||
|
end
|
||
|
|
||
|
defp fields_to_encode(struct, opts) do
|
||
|
fields = Map.keys(struct)
|
||
|
|
||
|
cond do
|
||
|
only = Keyword.get(opts, :only) ->
|
||
|
case only -- fields do
|
||
|
[] ->
|
||
|
only
|
||
|
|
||
|
error_keys ->
|
||
|
raise ArgumentError,
|
||
|
"`:only` specified keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
|
||
|
"#{inspect(fields -- [:__struct__])}"
|
||
|
|
||
|
end
|
||
|
|
||
|
except = Keyword.get(opts, :except) ->
|
||
|
case except -- fields do
|
||
|
[] ->
|
||
|
fields -- [:__struct__ | except]
|
||
|
|
||
|
error_keys ->
|
||
|
raise ArgumentError,
|
||
|
"`:except` specified keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
|
||
|
"#{inspect(fields -- [:__struct__])}"
|
||
|
|
||
|
end
|
||
|
|
||
|
true ->
|
||
|
fields -- [:__struct__]
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# The following implementations are formality - they are already covered
|
||
|
# by the main encoding mechanism in Jason.Encode, but exist mostly for
|
||
|
# documentation purposes and if anybody had the idea to call the protocol directly.
|
||
|
|
||
|
defimpl Jason.Encoder, for: Atom do
|
||
|
def encode(atom, opts) do
|
||
|
Jason.Encode.atom(atom, opts)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defimpl Jason.Encoder, for: Integer do
|
||
|
def encode(integer, _opts) do
|
||
|
Jason.Encode.integer(integer)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defimpl Jason.Encoder, for: Float do
|
||
|
def encode(float, _opts) do
|
||
|
Jason.Encode.float(float)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defimpl Jason.Encoder, for: List do
|
||
|
def encode(list, opts) do
|
||
|
Jason.Encode.list(list, opts)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defimpl Jason.Encoder, for: Map do
|
||
|
def encode(map, opts) do
|
||
|
Jason.Encode.map(map, opts)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defimpl Jason.Encoder, for: BitString do
|
||
|
def encode(binary, opts) when is_binary(binary) do
|
||
|
Jason.Encode.string(binary, opts)
|
||
|
end
|
||
|
|
||
|
def encode(bitstring, _opts) do
|
||
|
raise Protocol.UndefinedError,
|
||
|
protocol: @protocol,
|
||
|
value: bitstring,
|
||
|
description: "cannot encode a bitstring to JSON"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defimpl Jason.Encoder, for: [Date, Time, NaiveDateTime, DateTime] do
|
||
|
def encode(value, _opts) do
|
||
|
[?", @for.to_iso8601(value), ?"]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defimpl Jason.Encoder, for: Decimal do
|
||
|
def encode(value, _opts) do
|
||
|
# silence the xref warning
|
||
|
decimal = Decimal
|
||
|
[?", decimal.to_string(value), ?"]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defimpl Jason.Encoder, for: Jason.Fragment do
|
||
|
def encode(%{encode: encode}, opts) do
|
||
|
encode.(opts)
|
||
|
end
|
||
|
end
|