Current section
Files
Jump to
Current section
Files
lib/encoder.ex
defprotocol RustyJson.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 struct that represents a person:
defmodule Person do
@derive {RustyJson.Encoder, only: [:name, :age]}
defstruct [:name, :age, :private_data]
end
The `@derive` will generate an implementation like:
defimpl RustyJson.Encoder, for: Person do
def encode(value) do
Map.take(value, [:name, :age])
end
end
## Explicit Implementation
If you need full control, implement the protocol directly:
defimpl RustyJson.Encoder, for: Money do
def encode(%Money{amount: amount, currency: currency}) do
%{amount: amount, currency: to_string(currency)}
end
end
The returned value must be a JSON-encodable term (map, list, string,
number, boolean, nil, or another struct implementing this protocol).
"""
@fallback_to_any true
@doc """
Converts `value` to a JSON-encodable type.
"""
@spec encode(t) :: term
def encode(value)
end
# For maps, lists, and tuples - only recurse if values might need encoding.
# Primitives (strings, numbers, atoms, booleans) pass through unchanged.
defimpl RustyJson.Encoder, for: Map do
def encode(map) do
# Check if any value needs encoding (is a struct, map, list, or tuple)
if Enum.any?(map, fn {_k, v} -> needs_encoding?(v) end) do
:maps.map(fn _k, v -> RustyJson.Encoder.encode(v) end, map)
else
map
end
end
defp needs_encoding?(v) when is_map(v), do: true
defp needs_encoding?(v) when is_list(v) and v != [], do: true
defp needs_encoding?(v) when is_tuple(v), do: true
defp needs_encoding?(_), do: false
end
defimpl RustyJson.Encoder, for: List do
def encode([]), do: []
def encode(list) do
if Enum.any?(list, &needs_encoding?/1) do
Enum.map(list, &RustyJson.Encoder.encode/1)
else
list
end
end
defp needs_encoding?(v) when is_map(v), do: true
defp needs_encoding?(v) when is_list(v) and v != [], do: true
defp needs_encoding?(v) when is_tuple(v), do: true
defp needs_encoding?(_), do: false
end
defimpl RustyJson.Encoder, for: Tuple do
def encode(tuple) do
tuple |> Tuple.to_list() |> RustyJson.Encoder.encode()
end
end
# Built-in types are passed through to Rust which handles them natively.
# This avoids double-processing while maintaining protocol compatibility.
defimpl RustyJson.Encoder, for: [Date, Time, NaiveDateTime, DateTime, MapSet, Range] do
def encode(value), do: value
end
defimpl RustyJson.Encoder, for: URI do
def encode(uri), do: uri
end
if Code.ensure_loaded?(Decimal) do
defimpl RustyJson.Encoder, for: Decimal do
def encode(decimal), do: decimal
end
end
defimpl RustyJson.Encoder, for: Any do
@moduledoc false
defmacro __deriving__(module, _struct, opts) do
fields = fields_to_encode(opts)
quote do
defimpl RustyJson.Encoder, for: unquote(module) do
def encode(struct) do
Map.take(struct, unquote(fields))
|> RustyJson.Encoder.encode()
end
end
end
end
defp fields_to_encode(opts) do
cond do
only = Keyword.get(opts, :only) ->
only
except = Keyword.get(opts, :except) ->
quote do
Map.keys(%unquote(opts[:struct]){}) -- [:__struct__ | unquote(except)]
end
true ->
quote do
Map.keys(%unquote(opts[:struct]){}) -- [:__struct__]
end
end
end
def encode(%{__struct__: _module} = struct) do
struct
|> Map.from_struct()
|> RustyJson.Encoder.encode()
end
def encode(value), do: value
end