From 27080e287fb10db6940c5229fba018eb58af770a Mon Sep 17 00:00:00 2001 From: Keith Brings Date: Mon, 27 Nov 2023 11:25:46 +0700 Subject: [PATCH 1/2] Extending to pass user options through calls --- .gitignore | 4 ++ README.md | 27 +++++++++ lib/encode.ex | 127 ++++++++++++++++++++++++++----------------- lib/encoder.ex | 5 +- test/encode_test.exs | 27 +++++++++ 5 files changed, 138 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index 5f72a4a..0a8ed5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +.idea/ +bench/ +jason.iml + # The directory Mix will write compiled artifacts to. /_build diff --git a/README.md b/README.md index 0cbd316..7edbd40 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,33 @@ iex(2)> Jason.decode!(~s({"age":44,"name":"Steve Irwin","nationality":"Australia %{"age" => 44, "name" => "Steve Irwin", "nationality" => "Australian"} ``` +## User Options +In some cases you may want to change encoding based on options such as Permissions, Output Format, etc. +You may use user settings to support this use case + +``` elixir + defmodule Custom do + defstruct [ + foo: nil, + bar: nil + ] + + defimpl Jason.Encoder do + def encode(s, {_,_,user_opts} = opts) do + unless user_opts[:compact] do + %{foo: s.foo, bar: s.bar} |> Jason.Encode.map(opts) + else + %{foo: s.foo} |> Jason.Encode.map(opts) + end + end + end + end + +iex(1)> Jason.encode!(%Custom{foo: "abba", bar: "babba"}, user: [compact: true]) +"{\"foo\":\"abba\"}" + +``` + Full documentation can be found at [https://hexdocs.pm/jason](https://hexdocs.pm/jason). ## Use with other libraries diff --git a/lib/encode.ex b/lib/encode.ex index 032952e..9d70267 100644 --- a/lib/encode.ex +++ b/lib/encode.ex @@ -31,10 +31,11 @@ defmodule Jason.Encode do @doc false @spec encode(any, map) :: {:ok, iodata} | {:error, EncodeError.t | Exception.t} def encode(value, opts) do + user_opts = user_options(opts) escape = escape_function(opts) encode_map = encode_map_function(opts) try do - {:ok, value(value, escape, encode_map)} + {:ok, value(value, escape, encode_map, user_opts)} catch :throw, %EncodeError{} = e -> {:error, e} @@ -47,11 +48,15 @@ defmodule Jason.Encode do defp encode_map_function(%{maps: maps}) do case maps do - :naive -> &map_naive/3 - :strict -> &map_strict/3 + :naive -> &map_naive/4 + :strict -> &map_strict/4 end end + defp user_options(%{user: nil}), do: nil + defp user_options(%{user: user}), do: user + defp user_options(_), do: nil + if Code.ensure_loaded?(Jason.Native) do defp escape_function(%{escape: escape}) do case escape do @@ -91,42 +96,45 @@ defmodule Jason.Encode do def value(value, {escape, encode_map}) do value(value, escape, encode_map) end + def value(value, {escape, encode_map, user_opts}) do + value(value, escape, encode_map, user_opts) + end @doc false # We use this directly in the helpers and deriving for extra speed - def value(value, escape, _encode_map) when is_atom(value) do + def value(value, escape, _encode_map, user_opts \\ nil) + def value(value, escape, _encode_map, _user_opts) when is_atom(value) do encode_atom(value, escape) end - def value(value, escape, _encode_map) when is_binary(value) do + def value(value, escape, _encode_map, _user_opts) when is_binary(value) do encode_string(value, escape) end - def value(value, _escape, _encode_map) when is_integer(value) do + def value(value, _escape, _encode_map, _user_opts) when is_integer(value) do integer(value) end - def value(value, _escape, _encode_map) when is_float(value) do + def value(value, _escape, _encode_map, _user_opts) when is_float(value) do float(value) end - def value(value, escape, encode_map) when is_list(value) do - list(value, escape, encode_map) + def value(value, escape, encode_map, user_opts) when is_list(value) do + list(value, escape, encode_map, user_opts) end - def value(%{__struct__: module} = value, escape, encode_map) do - struct(value, escape, encode_map, module) + def value(%{__struct__: module} = value, escape, encode_map, user_opts) do + struct(value, escape, encode_map, module, user_opts) end - def value(value, escape, encode_map) when is_map(value) do + def value(value, escape, encode_map, user_opts) when is_map(value) do case Map.to_list(value) do [] -> "{}" - keyword -> encode_map.(keyword, escape, encode_map) + keyword -> encode_map.(keyword, escape, encode_map, user_opts) end end - - def value(value, escape, encode_map) do - Encoder.encode(value, {escape, encode_map}) + def value(value, escape, encode_map, user_opts) do + Encoder.encode(value, {escape, encode_map, user_opts}) end @compile {:inline, integer: 1, float: 1} @@ -168,70 +176,82 @@ defmodule Jason.Encode do @spec list(list, opts) :: iodata def list(list, {escape, encode_map}) do - list(list, escape, encode_map) + list(list, escape, encode_map, nil) + end + def list(list, {escape, encode_map, user_opts}) do + list(list, escape, encode_map, user_opts) end - defp list([], _escape, _encode_map) do + defp list([], _escape, _encode_map, _user_opts) do "[]" end - defp list([head | tail], escape, encode_map) do - [?[, value(head, escape, encode_map) - | list_loop(tail, escape, encode_map)] + defp list([head | tail], escape, encode_map, user_opts) do + [?[, value(head, escape, encode_map, user_opts) + | list_loop(tail, escape, encode_map, user_opts)] end - defp list_loop([], _escape, _encode_map) do + defp list_loop([], _escape, _encode_map, _user_opts) do ']' end - defp list_loop([head | tail], escape, encode_map) do - [?,, value(head, escape, encode_map) - | list_loop(tail, escape, encode_map)] + defp list_loop([head | tail], escape, encode_map, user_opts) do + [?,, value(head, escape, encode_map, user_opts) + | list_loop(tail, escape, encode_map, user_opts)] end @spec keyword(keyword, opts) :: iodata def keyword(list, _) when list == [], do: "{}" def keyword(list, {escape, encode_map}) when is_list(list) do - encode_map.(list, escape, encode_map) + encode_map.(list, escape, encode_map, nil) + end + def keyword(list, {escape, encode_map, user_opts}) when is_list(list) do + encode_map.(list, escape, encode_map, user_opts) end @spec map(map, opts) :: iodata def map(value, {escape, encode_map}) do case Map.to_list(value) do [] -> "{}" - keyword -> encode_map.(keyword, escape, encode_map) + keyword -> encode_map.(keyword, escape, encode_map, nil) + end + end + def map(value, {escape, encode_map, user_opts}) do + case Map.to_list(value) do + [] -> "{}" + keyword -> encode_map.(keyword, escape, encode_map, user_opts) end end - defp map_naive([{key, value} | tail], escape, encode_map) do + defp map_naive([{key, value} | tail], escape, encode_map, user_opts) do ["{\"", key(key, escape), "\":", - value(value, escape, encode_map) - | map_naive_loop(tail, escape, encode_map)] + value(value, escape, encode_map, user_opts) + | map_naive_loop(tail, escape, encode_map, user_opts)] end - defp map_naive_loop([], _escape, _encode_map) do + defp map_naive_loop([], _escape, _encode_map, _user_opts) do '}' end - defp map_naive_loop([{key, value} | tail], escape, encode_map) do + defp map_naive_loop([{key, value} | tail], escape, encode_map, user_opts) do [",\"", key(key, escape), "\":", - value(value, escape, encode_map) - | map_naive_loop(tail, escape, encode_map)] + value(value, escape, encode_map, user_opts) + | map_naive_loop(tail, escape, encode_map, user_opts)] end - defp map_strict([{key, value} | tail], escape, encode_map) do + defp map_strict([{key, value} | tail], escape, encode_map, user_opts) do key = IO.iodata_to_binary(key(key, escape)) visited = %{key => []} ["{\"", key, "\":", - value(value, escape, encode_map) - | map_strict_loop(tail, escape, encode_map, visited)] + value(value, escape, encode_map, user_opts) + | map_strict_loop(tail, escape, encode_map, visited, user_opts)] end - defp map_strict_loop([], _encode_map, _escape, _visited) do + defp map_strict_loop([], _encode_map, _escape, _visited, _user_opts) do '}' end - defp map_strict_loop([{key, value} | tail], escape, encode_map, visited) do + defp map_strict_loop([{key, value} | tail], escape, encode_map, visited, user_opts) do key = IO.iodata_to_binary(key(key, escape)) case visited do %{^key => _} -> @@ -239,45 +259,52 @@ defmodule Jason.Encode do _ -> visited = Map.put(visited, key, []) [",\"", key, "\":", - value(value, escape, encode_map) - | map_strict_loop(tail, escape, encode_map, visited)] + value(value, escape, encode_map, user_opts) + | map_strict_loop(tail, escape, encode_map, visited, user_opts)] end end @spec struct(struct, opts) :: iodata def struct(%module{} = value, {escape, encode_map}) do - struct(value, escape, encode_map, module) + struct(value, escape, encode_map, module, nil) + end + def struct(%module{} = value, {escape, encode_map, user_opts}) do + struct(value, escape, encode_map, module, user_opts) end # TODO: benchmark the effect of inlining the to_iso8601 functions for module <- [Date, Time, NaiveDateTime, DateTime] do - defp struct(value, _escape, _encode_map, unquote(module)) do + defp struct(value, _escape, _encode_map, unquote(module), _user_opts) do [?", unquote(module).to_iso8601(value), ?"] end end - defp struct(value, _escape, _encode_map, Decimal) do + defp struct(value, _escape, _encode_map, Decimal, _user_opts) do # silence the xref warning decimal = Decimal [?", decimal.to_string(value, :normal), ?"] end - defp struct(value, escape, encode_map, Fragment) do + defp struct(value, escape, encode_map, Fragment, nil) do %{encode: encode} = value encode.({escape, encode_map}) end + defp struct(value, escape, encode_map, Fragment, user_opts) do + %{encode: encode} = value + encode.({escape, encode_map, user_opts}) + end - defp struct(value, escape, encode_map, OrderedObject) do + defp struct(value, escape, encode_map, OrderedObject, user_opts) do case value do %{values: []} -> "{}" - %{values: values} -> encode_map.(values, escape, encode_map) + %{values: values} -> encode_map.(values, escape, encode_map, user_opts) end end - - defp struct(value, escape, encode_map, _module) do - Encoder.encode(value, {escape, encode_map}) + defp struct(value, escape, encode_map, _module, user_opts) do + Encoder.encode(value, {escape, encode_map, user_opts}) end + @doc false # This is used in the helpers and deriving implementation def key(string, escape) when is_binary(string) do diff --git a/lib/encoder.ex b/lib/encoder.ex index d693399..bad3de5 100644 --- a/lib/encoder.ex +++ b/lib/encoder.ex @@ -80,14 +80,15 @@ defimpl Jason.Encoder, for: Any do kv = Enum.map(fields, &{&1, generated_var(&1, __MODULE__)}) escape = quote(do: escape) encode_map = quote(do: encode_map) - encode_args = [escape, encode_map] + user_opts = quote(do: user_opts) + encode_args = [escape, encode_map, user_opts] kv_iodata = Jason.Codegen.build_kv_iodata(kv, encode_args) quote do defimpl Jason.Encoder, for: unquote(module) do require Jason.Helpers - def encode(%{unquote_splicing(kv)}, {unquote(escape), unquote(encode_map)}) do + def encode(%{unquote_splicing(kv)}, {unquote(escape), unquote(encode_map), user_opts}) do unquote(kv_iodata) end end diff --git a/test/encode_test.exs b/test/encode_test.exs index 8ed77cd..5d53444 100644 --- a/test/encode_test.exs +++ b/test/encode_test.exs @@ -3,6 +3,33 @@ defmodule Jason.EncoderTest do alias Jason.{EncodeError, Encoder} + defmodule Custom do + defstruct [ + foo: nil, + bar: nil + ] + + defimpl Jason.Encoder do + def encode(s, {_,_,user_opts} = opts) do + unless user_opts[:compact] do + %{foo: s.foo, bar: s.bar} |> Jason.Encode.map(opts) + else + %{foo: s.foo} |> Jason.Encode.map(opts) + end + end + end + end + + test "user opts" do + sut = %Custom{foo: "abba", bar: "bobba"} + s = Jason.encode!(sut) + assert s == "{\"foo\":\"abba\",\"bar\":\"bobba\"}" + s = Jason.encode!(sut, user: [compact: true]) + assert s == "{\"foo\":\"abba\"}" + s = Jason.encode!(%{entity: sut}, user: [compact: true]) + assert s == "{\"entity\":{\"foo\":\"abba\"}}" + end + test "atom" do assert to_json(nil) == "null" assert to_json(true) == "true" From 63b789d06269c7b3fac9b58e8ce05794ba14523e Mon Sep 17 00:00:00 2001 From: Keith Brings Date: Mon, 27 Nov 2023 21:18:36 +0700 Subject: [PATCH 2/2] string,atom encode methods with user_options --- lib/encode.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/encode.ex b/lib/encode.ex index 9d70267..a350f2f 100644 --- a/lib/encode.ex +++ b/lib/encode.ex @@ -143,6 +143,9 @@ defmodule Jason.Encode do def atom(atom, {escape, _encode_map}) do encode_atom(atom, escape) end + def atom(atom, {escape, _encode_map, _user_opts}) do + encode_atom(atom, escape) + end defp encode_atom(nil, _escape), do: "null" defp encode_atom(true, _escape), do: "true" @@ -323,7 +326,9 @@ defmodule Jason.Encode do def string(string, {escape, _encode_map}) do encode_string(string, escape) end - + def string(string, {escape, _encode_map,_user_opts}) do + encode_string(string, escape) + end defp encode_string(string, escape) do [?", escape.(string), ?"] end