diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2e1cbc..f228b10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,9 +15,6 @@ jobs: fail-fast: false matrix: include: - - elixir: 1.13.x - otp: 24 - os: ubuntu-20.04 - elixir: 1.14.x otp: 25 os: ubuntu-22.04 @@ -28,6 +25,14 @@ jobs: otp: 26 os: ubuntu-latest warnings_as_errors: true + - elixir: 1.17.x + otp: 27 + os: ubuntu-latest + warnings_as_errors: true + - elixir: 1.18.x + otp: 27 + os: ubuntu-latest + warnings_as_errors: true env: MIX_ENV: test steps: diff --git a/README.md b/README.md index ffbecc9..9dcb530 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,13 @@ It works similar like [OJSON](https://hex.pm/packages/ojson) but can output a pr StableJason.encode(%{c: 3, b: 2, a: 1}) {:ok, ~S|{"a":1,"b":2,"c":3}|} -StableJason.encode(%{c: 3, b: 2, a: 1}, pretty: true) +StableJason.encode(%{c: 3, b: 2, a: 1}, sorter: :asc, pretty: true) {:ok, "{\n \"a\": 1,\n \"b\": 2,\n \"c\": 3\n}"} StableJason.encode!(%{c: 3, b: 2, a: 1}) "{\"a\":1,\"b\":2,\"c\":3}" -StableJason.encode!(%{c: 3, b: 2, a: 1}, pretty: true) +StableJason.encode!(%{c: 3, b: 2, a: 1}, sorter: :asc, pretty: true) "{\n \"a\": 1,\n \"b\": 2,\n \"c\": 3\n}" ``` diff --git a/lib/stable_jason.ex b/lib/stable_jason.ex index c8c4193..7ba996f 100644 --- a/lib/stable_jason.ex +++ b/lib/stable_jason.ex @@ -15,6 +15,9 @@ defmodule StableJason do iex> StableJason.encode(%{c: 3, b: 2, a: 1}) {:ok, ~S|{"a":1,"b":2,"c":3}|} + iex> StableJason.encode(%{c: 3, b: 2, a: 1}, sorter: :desc) + {:ok, ~S|{"c":3,"b":2,"a":1}|} + iex> StableJason.encode(<<0::1>>) {:error, %Protocol.UndefinedError{ @@ -24,7 +27,9 @@ defmodule StableJason do }} """ def encode(input, opts \\ []) do - case Encoder.encode(input) do + {sorter, opts} = Keyword.pop(opts, :sorter) + + case Encoder.encode(input, sorter || :asc) do {:ok, result} -> Jason.encode(result, opts) {:error, error} -> {:error, error} end @@ -41,12 +46,17 @@ defmodule StableJason do iex> StableJason.encode!(%{a: 1}) ~S|{"a":1}| + iex> StableJason.encode!(%{a: 1, b: 3}, sorter: :desc) + ~S|{"b":3,"a":1}| + iex> StableJason.encode!("\\xFF") ** (Jason.EncodeError) invalid byte 0xFF in <<255>> """ def encode!(input, opts \\ []) do - case Encoder.encode(input) do + {sorter, opts} = Keyword.pop(opts, :sorter) + + case Encoder.encode(input, sorter || :asc) do {:ok, result} -> Jason.encode!(result, opts) {:error, error} -> raise error end diff --git a/lib/stable_jason/encoder.ex b/lib/stable_jason/encoder.ex index bd7af62..ad12ce6 100644 --- a/lib/stable_jason/encoder.ex +++ b/lib/stable_jason/encoder.ex @@ -19,32 +19,32 @@ defmodule StableJason.Encoder do description: "cannot encode a bitstring to JSON" }} """ - def encode(input) do + def encode(input, sorter \\ :asc) do case Jason.encode(input) do - {:ok, result} -> {:ok, encode_stable(result)} + {:ok, result} -> {:ok, encode_stable(result, sorter)} {:error, error} -> {:error, error} end end - defp encode_stable(input) when is_binary(input) do + defp encode_stable(input, sorter) when is_binary(input) do input |> Jason.decode!(%{objects: :ordered_objects}) - |> do_encode_stable() + |> do_encode_stable(sorter) end - defp do_encode_stable(%Jason.OrderedObject{} = ordered_object) do + defp do_encode_stable(%Jason.OrderedObject{} = ordered_object, sorter) do stable_values = - for {k, v} <- List.keysort(ordered_object.values, 0) do - {k, do_encode_stable(v)} + for {k, v} <- List.keysort(ordered_object.values, 0, sorter) do + {k, do_encode_stable(v, sorter)} end %Jason.OrderedObject{values: stable_values} end - defp do_encode_stable(input) when is_list(input) do + defp do_encode_stable(input, sorter) when is_list(input) do input - |> Enum.map(&do_encode_stable/1) + |> Enum.map(fn i -> do_encode_stable(i, sorter) end) end - defp do_encode_stable(input), do: input + defp do_encode_stable(input, _sorter), do: input end diff --git a/mix.exs b/mix.exs index 65aeb4c..b83e0c8 100644 --- a/mix.exs +++ b/mix.exs @@ -1,14 +1,14 @@ defmodule StableJason.MixProject do use Mix.Project - @version "1.0.0" + @version "2.0.0" @source_url "https://github.com/egze/stable_jason" def project do [ app: :stable_jason, version: @version, - elixir: "~> 1.13", + elixir: "~> 1.14", start_permanent: Mix.env() == :prod, deps: deps(), description: description(), diff --git a/test/stable_jason/encoder_test.exs b/test/stable_jason/encoder_test.exs index ef8ed4c..48d69a8 100644 --- a/test/stable_jason/encoder_test.exs +++ b/test/stable_jason/encoder_test.exs @@ -28,7 +28,7 @@ defmodule StableJason.EncoderTest do end test "nested map with more complex data types" do - input = %{c: 3, b: %{d: Date.utc_today(), a: 1.5}, a: 1} + input = %{c: 3, b: %{d: ~D[2024-01-18], a: 1.5}, a: 1} assert Encoder.encode(input) == {:ok, @@ -60,5 +60,25 @@ defmodule StableJason.EncoderTest do 1 ]} end + + test "using a sorter atom" do + input = %{a: 5, aa: 4, b: 9, A: 12} + + assert Encoder.encode(input, :desc) == + {:ok, %Jason.OrderedObject{values: [{"b", 9}, {"aa", 4}, {"a", 5}, {"A", 12}]}} + end + + test "using a sorter function" do + input = %{a: 5, aa: 4, b: 9, A: 12} + + sorter = fn a, b -> + if String.length(inspect(a)) == String.length(inspect(b)), + do: a < b, + else: String.length(inspect(a)) < String.length(inspect(b)) + end + + assert Encoder.encode(input, sorter) == + {:ok, %Jason.OrderedObject{values: [{"A", 12}, {"a", 5}, {"b", 9}, {"aa", 4}]}} + end end end