Current section
Files
Jump to
Current section
Files
lib/johnson.ex
defmodule Johnson do
@moduledoc """
Module to encode and decode JSON while preserving the key orders.
"""
@count_key :kc
defstruct data: []
@type t :: %__MODULE__{data: keyword()}
@doc """
Decode a JSON string, while preserving the original key order.
## Examples
iex> Johnson.decode!(~s({"a":"a","b":"b"}))
%Johnson{data: [{"a", "a"}, {"b", "b"}]}
"""
@spec decode!(binary()) :: t()
def decode!(string) when is_bitstring(string) do
Process.put(@count_key, 0)
string
|> Jason.decode!(
keys: fn key ->
@count_key
|> Process.get()
|> (& Process.put(@count_key, &1 + 1)).()
|> (& {&1, key}).()
end
)
|> new()
end
@doc """
Transform a struct back to a JSON string.
## Examples
iex> Johnson.encode!(%Johnson{data: [{"a", "a"}, {"b", "b"}]})
~s({"a":"a","b":"b"})
"""
@spec encode!(t()) :: binary()
def encode!(%__MODULE__{} = j), do: Jason.encode!(j)
@doc """
Get field.
## Examples
iex> Johnson.get(%Johnson{data: [{"a", "a"}, {"b", "b"}]}, "a")
"a"
"""
@spec get(t(), binary()) :: any()
def get(%__MODULE__{data: data}, key) when is_binary(key) do
data
|> List.keyfind(key, 0)
|> case do
{_, value} -> value
x -> x
end
end
defp new(map) when is_map(map) do
data =
map
|> Map.to_list()
|> Enum.sort_by(fn {{idx, _}, _} -> idx end)
|> Enum.map(fn {{_, k}, v} -> {k, new(v)} end)
%__MODULE__{data: data}
end
defp new(list) when is_list(list), do: Enum.map(list, &new/1)
defp new(value), do: value
end
defimpl Jason.Encoder, for: Johnson do
def encode(%{data: value}, opts), do: Jason.Encode.keyword(value, opts)
end
defimpl String.Chars, for: Johnson do
def to_string(struct) do
Jason.encode!(struct)
end
end