Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extending to pass user options through calls #178

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
134 changes: 83 additions & 51 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 All @@ -135,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"
Expand Down Expand Up @@ -168,116 +179,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 All @@ -296,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
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 @@

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
Loading