-
Notifications
You must be signed in to change notification settings - Fork 170
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add an option to decode JSON objects preserving order
This adds a new Jason.OrderedObject struct that can be used to store a keyword (with non-atom keys) and properly implement protocols on it. It's further used when the option `objects: :ordered_objects` is provided for the decoder to replace native maps. No performance impact when the option is not used was measured. Co-authored-by: Alex Lopez <[email protected]>
- Loading branch information
1 parent
1ffe009
commit 7e25ebb
Showing
7 changed files
with
185 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
defmodule Jason.OrderedObject do | ||
@doc """ | ||
Struct implementing a JSON object retaining order of properties. | ||
A wrapper around a keyword (that supports non-atom keys) allowing for | ||
proper protocol implementations. | ||
Implements the `Access` behaviour and `Enumerable` protocol with | ||
complexity similar to keywords/lists. | ||
""" | ||
|
||
@behaviour Access | ||
|
||
@type t :: %__MODULE__{values: [{String.Chars.t(), term()}]} | ||
|
||
defstruct values: [] | ||
|
||
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: Jason.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 Jason.Encoder, for: Jason.OrderedObject do | ||
def encode(%{values: values}, opts) do | ||
Jason.Encode.keyword(values, opts) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
defmodule Jason.OrderedObjectTest do | ||
use ExUnit.Case, async: true | ||
|
||
alias Jason.OrderedObject | ||
|
||
test "Access behavior" do | ||
obj = OrderedObject.new([{:foo, 1}, {"bar", 2}]) | ||
|
||
assert obj[:foo] == 1 | ||
assert obj["bar"] == 2 | ||
|
||
assert Access.pop(obj, :foo) == {1, OrderedObject.new([{"bar", 2}])} | ||
|
||
obj = OrderedObject.new(foo: OrderedObject.new(bar: 1)) | ||
assert obj[:foo][:bar] == 1 | ||
modified_obj = put_in(obj[:foo][:bar], 2) | ||
assert %OrderedObject{} = modified_obj[:foo] | ||
assert modified_obj[:foo][:bar] == 2 | ||
end | ||
|
||
test "Enumerable protocol" do | ||
obj = OrderedObject.new(foo: 1, bar: 2, quux: 42) | ||
|
||
assert Enum.count(obj) == 3 | ||
assert Enum.member?(obj, {:foo, 1}) | ||
|
||
assert Enum.into(obj, %{}) == %{foo: 1, bar: 2, quux: 42} | ||
assert Enum.into(obj, []) == [foo: 1, bar: 2, quux: 42] | ||
end | ||
end |