Skip to content

Commit

Permalink
Extending to pass user options through calls
Browse files Browse the repository at this point in the history
  • Loading branch information
noizu committed Nov 27, 2023
1 parent 866227f commit 27080e2
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 52 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.idea/
bench/
jason.iml

# The directory Mix will write compiled artifacts to.
/_build

Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
127 changes: 77 additions & 50 deletions lib/encode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -168,116 +176,135 @@ 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 => _} ->
error({:duplicate_key, key})
_ ->
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
Expand Down
5 changes: 3 additions & 2 deletions lib/encoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions test/encode_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check failure on line 23 in test/encode_test.exs

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.4.x | OTP 20)

test user opts (Jason.EncoderTest)

Check failure on line 23 in test/encode_test.exs

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.5.x | OTP 20)

test user opts (Jason.EncoderTest)

Check failure on line 23 in test/encode_test.exs

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.6.x | OTP 20)

test user opts (Jason.EncoderTest)

Check failure on line 23 in test/encode_test.exs

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.7.x | OTP 20)

test user opts (Jason.EncoderTest)

Check failure on line 23 in test/encode_test.exs

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.8.x | OTP 20)

test user opts (Jason.EncoderTest)

Check failure on line 23 in test/encode_test.exs

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.9.x | OTP 20)

test user opts (Jason.EncoderTest)

Check failure on line 23 in test/encode_test.exs

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.10.x | OTP 21)

test user opts (Jason.EncoderTest)

Check failure on line 23 in test/encode_test.exs

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.11.x | OTP 22)

test user opts (Jason.EncoderTest)

Check failure on line 23 in test/encode_test.exs

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.11.x | OTP 23)

test user opts (Jason.EncoderTest)

Check failure on line 23 in test/encode_test.exs

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.12.x | OTP 23)

test user opts (Jason.EncoderTest)

Check failure on line 23 in test/encode_test.exs

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | OTP 24)

test user opts (Jason.EncoderTest)

Check failure on line 23 in test/encode_test.exs

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.14.x | OTP 25)

test user opts (Jason.EncoderTest)
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"
Expand Down

0 comments on commit 27080e2

Please sign in to comment.