Packages

Ultra-fast JSON encoding/decoding for Elixir. A drop-in Jason replacement that's 2-3x faster with 2-4x less memory, plus key interning for up to 2x faster bulk decoding. Full RFC 8259 compliance and memory safety. Purpose-built Rust NIFs, no serde.

Current section

Files

Jump to
rustyjson lib ordered_object.ex
Raw

lib/ordered_object.ex

defmodule RustyJson.OrderedObject do
@moduledoc """
An ordered JSON object that preserves key insertion order.
A wrapper around a keyword list (that supports non-atom keys) allowing for
proper protocol implementations.
This struct is used when decoding JSON with `objects: :ordered_objects` option.
It preserves the original order of keys as they appeared in the JSON input.
Implements the `Access` behaviour and `Enumerable` protocol with
complexity similar to keywords/lists.
## Decoding
iex> RustyJson.decode!(~s({"b":2,"a":1}), objects: :ordered_objects)
%RustyJson.OrderedObject{values: [{"b", 2}, {"a", 1}]}
## Encoding
When encoded back to JSON, the original key order is preserved:
iex> obj = %RustyJson.OrderedObject{values: [{"b", 2}, {"a", 1}]}
iex> RustyJson.encode!(obj)
~s({"b":2,"a":1})
## Access
iex> obj = RustyJson.OrderedObject.new([{"x", 1}, {"y", 2}])
iex> obj["x"]
1
"""
@behaviour Access
@type t :: %__MODULE__{
values: [{String.Chars.t(), term()}]
}
defstruct values: []
@doc """
Creates a new OrderedObject from a list of key-value tuples.
"""
@spec new(list({String.Chars.t(), term()})) :: t()
def new(values) when is_list(values) do
%__MODULE__{values: values}
end
@impl Access
def fetch(%__MODULE__{values: values}, key) do
case :lists.keyfind(key, 1, values) do
{_, value} -> {:ok, value}
false -> :error
end
end
@impl Access
def get_and_update(%__MODULE__{values: values} = obj, key, function) do
{result, new_values} = get_and_update(values, [], key, function)
{result, %{obj | values: new_values}}
end
@impl Access
def pop(%__MODULE__{values: values} = obj, key, default \\ nil) do
case :lists.keyfind(key, 1, values) do
{_, value} -> {value, %{obj | values: delete_key(values, key)}}
false -> {default, obj}
end
end
defp get_and_update([{key, current} | t], acc, key, fun) do
case fun.(current) do
{get, value} ->
{get, :lists.reverse(acc, [{key, value} | t])}
:pop ->
{current, :lists.reverse(acc, t)}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
end
end
defp get_and_update([{_, _} = h | t], acc, key, fun), do: get_and_update(t, [h | acc], key, fun)
defp get_and_update([], acc, key, fun) do
case fun.(nil) do
{get, update} ->
{get, [{key, update} | :lists.reverse(acc)]}
:pop ->
{nil, :lists.reverse(acc)}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
end
end
defp delete_key([{key, _} | tail], key), do: delete_key(tail, key)
defp delete_key([{_, _} = pair | tail], key), do: [pair | delete_key(tail, key)]
defp delete_key([], _key), do: []
end
defimpl Enumerable, for: RustyJson.OrderedObject do
def count(%{values: []}), do: {:ok, 0}
def count(_obj), do: {:error, __MODULE__}
def member?(%{values: []}, _value), do: {:ok, false}
def member?(_obj, _value), do: {:error, __MODULE__}
def slice(%{values: []}), do: {:ok, 0, fn _, _ -> [] end}
def slice(_obj), do: {:error, __MODULE__}
def reduce(%{values: values}, acc, fun), do: Enumerable.List.reduce(values, acc, fun)
end
defimpl RustyJson.Encoder, for: RustyJson.OrderedObject do
# Pass through to Rust for native formatting with proper pretty-printing.
# Only pre-process values when they contain structs/tuples that need
# Encoder protocol dispatch; otherwise return as-is (zero allocation).
def encode(%RustyJson.OrderedObject{values: values} = obj, opts) do
if needs_encoding?(values) do
%RustyJson.OrderedObject{values: preprocess_values(values, opts)}
else
obj
end
end
defp needs_encoding?([]), do: false
defp needs_encoding?([{_k, %{__struct__: _}} | _rest]), do: true
defp needs_encoding?([{_k, v} | _rest]) when is_tuple(v), do: true
defp needs_encoding?([{_k, v} | rest]) when is_map(v) do
has_struct_key?(v) or needs_encoding?(rest)
end
defp needs_encoding?([{_k, v} | rest]) when is_list(v) do
needs_encoding_list?(v) or needs_encoding?(rest)
end
defp needs_encoding?([{_k, _v} | rest]), do: needs_encoding?(rest)
defp has_struct_key?(map), do: :maps.is_key(:__struct__, map)
defp needs_encoding_list?([]), do: false
defp needs_encoding_list?([%{__struct__: _} | _]), do: true
defp needs_encoding_list?([v | _]) when is_tuple(v), do: true
defp needs_encoding_list?([v | rest]) when is_map(v) do
has_struct_key?(v) or needs_encoding_list?(rest)
end
defp needs_encoding_list?([v | rest]) when is_list(v) do
needs_encoding_list?(v) or needs_encoding_list?(rest)
end
defp needs_encoding_list?([_ | rest]), do: needs_encoding_list?(rest)
defp preprocess_values([], _opts), do: []
defp preprocess_values([{k, v} | rest], opts) do
[{k, RustyJson.Encoder.encode(v, opts)} | preprocess_values(rest, opts)]
end
end