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 helpers.ex
Raw

lib/helpers.ex

defmodule RustyJson.Helpers do
@moduledoc """
Compile-time helpers for JSON encoding, compatible with Jason's `Helpers` module.
Provides macros that pre-encode JSON object keys at compile time
for faster runtime encoding.
## Examples
import RustyJson.Helpers
def render(user) do
json_map(name: user.name, email: user.email)
end
def render_partial(user) do
json_map_take(user, [:name, :email])
end
"""
@doc """
Encodes a keyword list as a JSON object with keys pre-encoded at compile time.
Values are encoded at runtime. Returns a `%RustyJson.Fragment{}`.
Keys must be atoms with ASCII printable characters only (no `\\`, `/`, `"`).
## Examples
iex> import RustyJson.Helpers
iex> fragment = json_map(name: "Alice", age: 30)
iex> RustyJson.encode!(fragment, protocol: true)
~s({"name":"Alice","age":30})
"""
defmacro json_map(kv) do
# Jason behavior: preserve order (no sorting)
# kv = Enum.sort_by(kv, &elem(&1, 0))
encoded_keys =
Enum.map(kv, fn {key, _} ->
key_str = Atom.to_string(key)
validate_json_key!(key_str)
RustyJson.encode!(key_str)
end)
value_asts = Enum.map(kv, fn {_, v} -> v end)
quote do
RustyJson.Helpers.__json_map__(unquote(encoded_keys), unquote(value_asts))
end
end
@doc """
Encodes selected keys from a map as a JSON object with keys pre-encoded at compile time.
Takes a map and a compile-time list of atom keys. Values are looked up and
encoded at runtime. Returns a `%RustyJson.Fragment{}`.
Raises `ArgumentError` at runtime if the map is missing any of the specified keys.
## Examples
iex> import RustyJson.Helpers
iex> user = %{name: "Alice", age: 30, email: "alice@example.com"}
iex> fragment = json_map_take(user, [:name, :age])
iex> RustyJson.encode!(fragment, protocol: true)
~s({"name":"Alice","age":30})
"""
defmacro json_map_take(map, take) do
# Jason behavior: preserve order of requested keys (no sorting)
# take = Enum.sort(take)
encoded_key_pairs =
Enum.map(take, fn key ->
key_str = Atom.to_string(key)
validate_json_key!(key_str)
{RustyJson.encode!(key_str), key}
end)
quote do
RustyJson.Helpers.__json_map_take__(
unquote(map),
unquote(Macro.escape(encoded_key_pairs)),
unquote(take)
)
end
end
@doc false
def __json_map__(encoded_keys, values) do
%RustyJson.Fragment{
encode: fn opts ->
[
"{"
| encoded_keys
|> Enum.zip(values)
|> Enum.with_index()
|> Enum.flat_map(fn {{key_json, value}, idx} ->
prefix = if idx > 0, do: [","], else: []
prefix ++ [key_json, ":", RustyJson.Encode.value(value, opts)]
end)
] ++ ["}"]
end
}
end
@doc false
def __json_map_take__(map, encoded_key_pairs, requested_keys) do
values =
Enum.map(encoded_key_pairs, fn {_, key_atom} ->
case Map.fetch(map, key_atom) do
{:ok, val} ->
val
:error ->
raise ArgumentError,
"expected a map with keys #{inspect(requested_keys)}, got: #{inspect(map)}"
end
end)
%RustyJson.Fragment{
encode: fn opts ->
[
"{"
| encoded_key_pairs
|> Enum.zip(values)
|> Enum.with_index()
|> Enum.flat_map(fn {{{key_json, _}, value}, idx} ->
prefix = if idx > 0, do: [","], else: []
prefix ++ [key_json, ":", RustyJson.Encode.value(value, opts)]
end)
] ++ ["}"]
end
}
end
defp validate_json_key!(key_str) do
if not String.match?(key_str, ~r/^[\x20-\x21\x23-\x2E\x30-\x5B\x5D-\x7E]*$/) do
raise ArgumentError,
"json_map/json_map_take keys must be ASCII printable " <>
"(excluding \", \\, /), got: #{inspect(key_str)}"
end
end
end