diff --git a/README.md b/README.md index c87046b..e32607c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Jason +# JasonV A blazing fast JSON parser and generator in pure Elixir. The parser and generator are at least twice as fast as other Elixir/Erlang libraries (most notably `Poison`). The performance is comparable to `jiffy`, which is implemented in C as a NIF. -Jason is usually only twice as slow. +JasonV is usually only twice as slow. Both parser and generator fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and @@ -26,10 +26,10 @@ end ## Basic Usage ``` elixir -iex(1)> Jason.encode!(%{"age" => 44, "name" => "Steve Irwin", "nationality" => "Australian"}) +iex(1)> JasonV.encode!(%{"age" => 44, "name" => "Steve Irwin", "nationality" => "Australian"}) "{\"age\":44,\"name\":\"Steve Irwin\",\"nationality\":\"Australian\"}" -iex(2)> Jason.decode!(~s({"age":44,"name":"Steve Irwin","nationality":"Australian"})) +iex(2)> JasonV.decode!(~s({"age":44,"name":"Steve Irwin","nationality":"Australian"})) %{"age" => 44, "name" => "Steve Irwin", "nationality" => "Australian"} ``` @@ -39,17 +39,17 @@ Full documentation can be found at [https://hexdocs.pm/jason](https://hexdocs.pm ### Postgrex -Versions starting at 0.14.0 use `Jason` by default. For earlier versions, please refer to +Versions starting at 0.14.0 use `JasonV` by default. For earlier versions, please refer to [previous versions of this document](https://github.com/michalmuskala/jason/tree/v1.1.2#postgrex). ### Ecto -Versions starting at 3.0.0 use `Jason` by default. For earlier versions, please refer to +Versions starting at 3.0.0 use `JasonV` by default. For earlier versions, please refer to [previous versions of this document](https://github.com/michalmuskala/jason/tree/v1.1.2#ecto). ### Plug (and Phoenix) -Phoenix starting at 1.4.0 uses `Jason` by default. For earlier versions, please refer to +Phoenix starting at 1.4.0 uses `JasonV` by default. For earlier versions, please refer to [previous versions of this document](https://github.com/michalmuskala/jason/tree/v1.1.2#plug-and-phoenix). ### Absinthe @@ -60,12 +60,12 @@ You need to pass the `:json_codec` option to `Absinthe.Plug` # When called directly: plug Absinthe.Plug, schema: MyApp.Schema, - json_codec: Jason + json_codec: JasonV # When used in phoenix router: forward "/api", to: Absinthe.Plug, - init_opts: [schema: MyApp.Schema, json_codec: Jason] + init_opts: [schema: MyApp.Schema, json_codec: JasonV] ``` ## Benchmarks @@ -85,24 +85,24 @@ A HTML report of the benchmarks (after their execution) can be found in ## Differences to Poison -Jason has a couple feature differences compared to Poison. +JasonV has a couple feature differences compared to Poison. - * Jason follows the JSON spec more strictly, for example it does not allow + * JasonV follows the JSON spec more strictly, for example it does not allow unescaped newline characters in JSON strings - e.g. `"\"\n\""` will produce a decoding error. * no support for decoding into data structures (the `as:` option). * no built-in encoders for `MapSet`, `Range` and `Stream`. * no support for encoding arbitrary structs - explicit implementation - of the `Jason.Encoder` protocol is always required. + of the `JasonV.Encoder` protocol is always required. * different pretty-printing customisation options (default `pretty: true` works the same) If you require encoders for any of the unsupported collection types, I suggest adding the needed implementations directly to your project: ```elixir -defimpl Jason.Encoder, for: [MapSet, Range, Stream] do +defimpl JasonV.Encoder, for: [MapSet, Range, Stream] do def encode(struct, opts) do - Jason.Encode.list(Enum.to_list(struct), opts) + JasonV.Encode.list(Enum.to_list(struct), opts) end end ``` @@ -112,7 +112,7 @@ if you own the struct, you can derive the implementation specifying which fields should be encoded to JSON: ```elixir -@derive {Jason.Encoder, only: [....]} +@derive {JasonV.Encoder, only: [....]} defstruct # ... ``` @@ -121,7 +121,7 @@ used carefully to avoid accidentally leaking private information when new fields are added: ```elixir -@derive Jason.Encoder +@derive JasonV.Encoder defstruct # ... ``` @@ -129,13 +129,13 @@ Finally, if you don't own the struct you want to encode to JSON, you may use `Protocol.derive/3` placed outside of any module: ```elixir -Protocol.derive(Jason.Encoder, NameOfTheStruct, only: [...]) -Protocol.derive(Jason.Encoder, NameOfTheStruct) +Protocol.derive(JasonV.Encoder, NameOfTheStruct, only: [...]) +Protocol.derive(JasonV.Encoder, NameOfTheStruct) ``` ## License -Jason is released under the Apache License 2.0 - see the [LICENSE](LICENSE) file. +JasonV is released under the Apache License 2.0 - see the [LICENSE](LICENSE) file. Some elements of tests and benchmarks have their origins in the [Poison library](https://github.com/devinus/poison) and were initially licensed under [CC0-1.0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/bench/decode.exs b/bench/decode.exs index 553e1cb..bf39367 100644 --- a/bench/decode.exs +++ b/bench/decode.exs @@ -1,11 +1,11 @@ decode_jobs = %{ - "Jason" => fn {json, _} -> Jason.decode!(json) end, + "JasonV" => fn {json, _} -> JasonV.decode!(json) end, "Poison" => fn {json, _} -> Poison.decode!(json) end, - "JSX" => fn {json, _} -> JSX.decode!(json, [:strict]) end, - "Tiny" => fn {json, _} -> Tiny.decode!(json) end, - "jsone" => fn {json, _} -> :jsone.decode(json) end, - "jiffy" => fn {json, _} -> :jiffy.decode(json, [:return_maps, :use_nil]) end, - "JSON" => fn {json, _} -> JSON.decode!(json) end, + "JSX" => fn {json, _} -> JSX.decode!(json, [:strict]) end, + "Tiny" => fn {json, _} -> Tiny.decode!(json) end, + "jsone" => fn {json, _} -> :jsone.decode(json) end, + "jiffy" => fn {json, _} -> :jiffy.decode(json, [:return_maps, :use_nil]) end, + "JSON" => fn {json, _} -> JSON.decode!(json) end # "binary_to_term/1" => fn {_, etf} -> :erlang.binary_to_term(etf) end, } @@ -19,18 +19,18 @@ decode_inputs = [ "JSON Generator (Pretty)", "UTF-8 escaped", "UTF-8 unescaped", - "Issue 90", + "Issue 90" ] -read_data = fn (name) -> +read_data = fn name -> file = name - |> String.downcase + |> String.downcase() |> String.replace(~r/([^\w]|-|_)+/, "-") |> String.trim("-") json = File.read!(Path.expand("data/#{file}.json", __DIR__)) - etf = :erlang.term_to_binary(Jason.decode!(json)) + etf = :erlang.term_to_binary(JasonV.decode!(json)) {json, etf} end @@ -45,7 +45,7 @@ end IO.puts("\n") Benchee.run(decode_jobs, -# parallel: 4, + # parallel: 4, warmup: 5, time: 30, memory_time: 1, @@ -54,6 +54,6 @@ Benchee.run(decode_jobs, load: "output/runs/*.benchee", formatters: [ {Benchee.Formatters.HTML, file: Path.expand("output/decode.html", __DIR__)}, - Benchee.Formatters.Console, + Benchee.Formatters.Console ] ) diff --git a/bench/encode.exs b/bench/encode.exs index 3ec2797..310aa6a 100644 --- a/bench/encode.exs +++ b/bench/encode.exs @@ -1,12 +1,12 @@ encode_jobs = %{ - "Jason" => &Jason.encode_to_iodata!/1, - "Jason strict" => &Jason.encode_to_iodata!(&1, maps: :strict), - "Poison" => &Poison.encode_to_iodata!/1, - "JSX" => &JSX.encode!/1, - "Tiny" => &Tiny.encode!/1, - "jsone" => &:jsone.encode/1, - "jiffy" => &:jiffy.encode/1, - "JSON" => &JSON.encode!/1, + "JasonV" => &JasonV.encode_to_iodata!/1, + "JasonV strict" => &JasonV.encode_to_iodata!(&1, maps: :strict), + "Poison" => &Poison.encode_to_iodata!/1, + "JSX" => &JSX.encode!/1, + "Tiny" => &Tiny.encode!/1, + "jsone" => &:jsone.encode/1, + "jiffy" => &:jiffy.encode/1, + "JSON" => &JSON.encode!/1 # "term_to_binary" => &:erlang.term_to_binary/1, } @@ -22,28 +22,28 @@ encode_inputs = [ "Canada", ] -read_data = fn (name) -> +read_data = fn name -> name - |> String.downcase + |> String.downcase() |> String.replace(~r/([^\w]|-|_)+/, "-") |> String.trim("-") |> (&"data/#{&1}.json").() |> Path.expand(__DIR__) - |> File.read! + |> File.read!() end - Benchee.run(encode_jobs, -# parallel: 4, + # parallel: 4, warmup: 5, time: 30, memory_time: 1, - inputs: for name <- encode_inputs, into: %{} do - name - |> read_data.() - |> Jason.decode!() - |> (&{name, &1}).() - end, + inputs: + for name <- encode_inputs, into: %{} do + name + |> read_data.() + |> JasonV.decode!() + |> (&{name, &1}).() + end, formatters: [ {Benchee.Formatters.HTML, file: Path.expand("output/encode.html", __DIR__)}, Benchee.Formatters.Console diff --git a/bench/mix.exs b/bench/mix.exs index 649fe7c..28724a1 100644 --- a/bench/mix.exs +++ b/bench/mix.exs @@ -1,4 +1,4 @@ -defmodule JasonBench.MixProject do +defmodule JasonVBench.MixProject do use Mix.Project def project do diff --git a/dialyzer.ignore b/dialyzer.ignore index 8eaa21b..0f2eba2 100644 --- a/dialyzer.ignore +++ b/dialyzer.ignore @@ -1,5 +1,5 @@ -Unknown function 'Elixir.Jason.Encoder.Function':'__impl__'/1 -Unknown function 'Elixir.Jason.Encoder.PID':'__impl__'/1 -Unknown function 'Elixir.Jason.Encoder.Port':'__impl__'/1 -Unknown function 'Elixir.Jason.Encoder.Reference':'__impl__'/1 -Unknown function 'Elixir.Jason.Encoder.Tuple':'__impl__'/1 +Unknown function 'Elixir.JasonV.Encoder.Function':'__impl__'/1 +Unknown function 'Elixir.JasonV.Encoder.PID':'__impl__'/1 +Unknown function 'Elixir.JasonV.Encoder.Port':'__impl__'/1 +Unknown function 'Elixir.JasonV.Encoder.Reference':'__impl__'/1 +Unknown function 'Elixir.JasonV.Encoder.Tuple':'__impl__'/1 diff --git a/lib/codegen.ex b/lib/codegen.ex index 9679c8b..7e3e6cc 100644 --- a/lib/codegen.ex +++ b/lib/codegen.ex @@ -1,7 +1,7 @@ -defmodule Jason.Codegen do +defmodule JasonV.Codegen do @moduledoc false - alias Jason.{Encode, EncodeError} + alias JasonV.{Encode, EncodeError} def jump_table(ranges, default) do ranges @@ -93,12 +93,12 @@ defmodule Jason.Codegen do defp ranges_to_orddict(ranges) do ranges |> Enum.flat_map(fn - {int, value} when is_integer(int) -> - [{int, value}] + {int, value} when is_integer(int) -> + [{int, value}] - {enum, value} -> - Enum.map(enum, &{&1, value}) - end) + {enum, value} -> + Enum.map(enum, &{&1, value}) + end) |> :orddict.from_list() end diff --git a/lib/decoder.ex b/lib/decoder.ex index 8ffe178..7003e00 100644 --- a/lib/decoder.ex +++ b/lib/decoder.ex @@ -1,33 +1,36 @@ -defmodule Jason.DecodeError do - @type t :: %__MODULE__{position: integer, data: String.t} +defmodule JasonV.DecodeError do + @type t :: %__MODULE__{position: integer, data: String.t()} defexception [:position, :token, :data] def message(%{position: position, token: token}) when is_binary(token) do - "unexpected sequence at position #{position}: #{inspect token}" + "unexpected sequence at position #{position}: #{inspect(token)}" end + def message(%{position: position, data: data}) when position == byte_size(data) do "unexpected end of input at position #{position}" end + def message(%{position: position, data: data}) do byte = :binary.at(data, position) str = <> + if String.printable?(str) do "unexpected byte at position #{position}: " <> "#{inspect byte, base: :hex} (#{inspect str})" else "unexpected byte at position #{position}: " <> - "#{inspect byte, base: :hex}" + "#{inspect(byte, base: :hex)}" end end end -defmodule Jason.Decoder do +defmodule JasonV.Decoder do @moduledoc false import Bitwise - alias Jason.{DecodeError, Codegen} + alias JasonV.{DecodeError, Codegen} import Codegen, only: [bytecase: 2, bytecase: 3] import Record @@ -39,9 +42,9 @@ defmodule Jason.Decoder do # We use integers instead of atoms to take advantage of the jump table # optimization @terminate 0 - @array 1 - @key 2 - @object 3 + @array 1 + @key 2 + @object 3 defrecordp :decode, [keys: nil, strings: nil, objects: nil, floats: nil] @@ -56,6 +59,7 @@ defmodule Jason.Decoder do catch {:position, position} -> {:error, %DecodeError{position: position, data: data}} + {:token, token, position} -> {:error, %DecodeError{token: token, position: position, data: data}} else @@ -66,14 +70,14 @@ defmodule Jason.Decoder do defp key_decode_function(%{keys: :atoms}), do: &String.to_atom/1 defp key_decode_function(%{keys: :atoms!}), do: &String.to_existing_atom/1 - defp key_decode_function(%{keys: :strings}), do: &(&1) + defp key_decode_function(%{keys: :strings}), do: & &1 defp key_decode_function(%{keys: fun}) when is_function(fun, 1), do: fun defp string_decode_function(%{strings: :copy}), do: &:binary.copy/1 - defp string_decode_function(%{strings: :reference}), do: &(&1) + defp string_decode_function(%{strings: :reference}), do: & &1 defp object_decode_function(%{objects: :maps}), do: &:maps.from_list/1 - defp object_decode_function(%{objects: :ordered_objects}), do: &Jason.OrderedObject.new(:lists.reverse(&1)) + defp object_decode_function(%{objects: :ordered_objects}), do: &JasonV.OrderedObject.new(:lists.reverse(&1)) defp float_decode_function(%{floats: :native}) do fn string, token, skip -> @@ -124,6 +128,7 @@ defmodule Jason.Decoder do <<_::bits>> -> error(original, skip) end + _ in 'f', rest -> case rest do <<"alse", rest::bits>> -> @@ -131,6 +136,7 @@ defmodule Jason.Decoder do <<_::bits>> -> error(original, skip) end + _ in 'n', rest -> case rest do <<"ull", rest::bits>> -> @@ -138,6 +144,7 @@ defmodule Jason.Decoder do <<_::bits>> -> error(original, skip) end + _, rest -> error(rest, original, skip + 1, stack, decode) <<_::bits>> -> @@ -300,6 +307,7 @@ defmodule Jason.Decoder do value(rest, original, skip + 1, [@array, [value | acc] | stack], decode) _, _rest -> error(original, skip) + <<_::bits>> -> empty_error(original, skip) end @@ -330,6 +338,7 @@ defmodule Jason.Decoder do key(rest, original, skip, [acc | stack], decode) _, _rest -> error(original, skip) + <<_::bits>> -> empty_error(original, skip) end @@ -347,10 +356,12 @@ defmodule Jason.Decoder do _ -> error(original, skip) end + _ in '"', rest -> string(rest, original, skip + 1, [@key | stack], decode, 0) _, _rest -> error(original, skip) + <<_::bits>> -> empty_error(original, skip) end @@ -364,6 +375,7 @@ defmodule Jason.Decoder do value(rest, original, skip + 1, [@object, value | stack], decode) _, _rest -> error(original, skip) + <<_::bits>> -> empty_error(original, skip) end @@ -442,6 +454,7 @@ defmodule Jason.Decoder do escapeu(rest, original, skip, stack, decode, acc) _, _rest -> error(original, skip + 1) + <<_::bits>> -> empty_error(original, skip) end @@ -502,12 +515,14 @@ defmodule Jason.Decoder do quote location: :keep do unquote(int) -> escape_surrogate(unquote_splicing(args)) end + clause end defp escapeu_first_clause(int, first, last, rest, original, skip, stack, decode, acc) when first <= 0x00 do - skip = quote do: (unquote(skip) + 6) + skip = quote do: unquote(skip) + 6 + acc = quote bind_quoted: [acc: acc, first: first, last: last] do if last <= 0x7F do @@ -515,7 +530,7 @@ defmodule Jason.Decoder do [acc, last] else # 110xxxx?? 10????? - byte1 = ((0b110 <<< 5) + (first <<< 2)) + (last >>> 6) + byte1 = (0b110 <<< 5) + (first <<< 2) + (last >>> 6) byte2 = (0b10 <<< 6) + (last &&& 0b111111) [acc, byte1, byte2] end @@ -525,16 +540,18 @@ defmodule Jason.Decoder do quote location: :keep do unquote(int) -> string(unquote_splicing(args)) end + clause end defp escapeu_first_clause(int, first, last, rest, original, skip, stack, decode, acc) when first <= 0x07 do - skip = quote do: (unquote(skip) + 6) + skip = quote do: unquote(skip) + 6 + acc = quote bind_quoted: [acc: acc, first: first, last: last] do # 110xxx?? 10?????? - byte1 = ((0b110 <<< 5) + (first <<< 2)) + (last >>> 6) + byte1 = (0b110 <<< 5) + (first <<< 2) + (last >>> 6) byte2 = (0b10 <<< 6) + (last &&& 0b111111) [acc, byte1, byte2] end @@ -543,17 +560,19 @@ defmodule Jason.Decoder do quote location: :keep do unquote(int) -> string(unquote_splicing(args)) end + clause end defp escapeu_first_clause(int, first, last, rest, original, skip, stack, decode, acc) when first <= 0xFF do - skip = quote do: (unquote(skip) + 6) + skip = quote do: unquote(skip) + 6 + acc = quote bind_quoted: [acc: acc, first: first, last: last] do # 1110xxxx 10xxxx?? 10?????? byte1 = (0b1110 <<< 4) + (first >>> 4) - byte2 = ((0b10 <<< 6) + ((first &&& 0b1111) <<< 2)) + (last >>> 6) + byte2 = (0b10 <<< 6) + ((first &&& 0b1111) <<< 2) + (last >>> 6) byte3 = (0b10 <<< 6) + (last &&& 0b111111) [acc, byte1, byte2, byte3] end @@ -562,11 +581,13 @@ defmodule Jason.Decoder do quote location: :keep do unquote(int) -> string(unquote_splicing(args)) end + clause end defmacro escapeu_last(int, original, skip) do clauses = escapeu_last_clauses() + quote location: :keep do case unquote(int) do unquote(clauses ++ token_error_clause(original, skip, 6)) @@ -580,6 +601,7 @@ defmodule Jason.Decoder do quote do unquote(int) -> unquote(last) end + clause end end @@ -597,6 +619,7 @@ defmodule Jason.Decoder do defp escapeu_surrogate_clauses(last, rest, original, skip, stack, decode, acc, hi) do digits1 = 'Dd' digits2 = Stream.concat([?C..?F, ?c..?f]) + for {int, first} <- unicode_escapes(digits1, digits2) do escapeu_surrogate_clause(int, first, last, rest, original, skip, stack, decode, acc, hi) end @@ -604,10 +627,11 @@ defmodule Jason.Decoder do defp escapeu_surrogate_clause(int, first, last, rest, original, skip, stack, decode, acc, hi) do skip = quote do: unquote(skip) + 12 + acc = quote bind_quoted: [acc: acc, first: first, last: last, hi: hi] do lo = ((first &&& 0x03) <<< 8) + last - [acc | <<(hi + lo)::utf8>>] + [acc | <>] end args = [rest, original, skip, stack, decode, acc, 0] [clause] = @@ -615,6 +639,7 @@ defmodule Jason.Decoder do unquote(int) -> string(unquote_splicing(args)) end + clause end end @@ -650,20 +675,20 @@ defmodule Jason.Decoder do end defp empty_error(_original, skip) do - throw {:position, skip} + throw({:position, skip}) end @compile {:inline, error: 2, token_error: 2, token_error: 3} defp error(_original, skip) do - throw {:position, skip} + throw({:position, skip}) end defp token_error(token, position) do - throw {:token, token, position} + throw({:token, token, position}) end defp token_error(token, position, len) do - throw {:token, binary_part(token, position, len), position} + throw({:token, binary_part(token, position, len), position}) end @compile {:inline, continue: 6} diff --git a/lib/encode.ex b/lib/encode.ex index 3c42dcd..8b636f5 100644 --- a/lib/encode.ex +++ b/lib/encode.ex @@ -1,26 +1,27 @@ -defmodule Jason.EncodeError do +defmodule JasonV.EncodeError do defexception [:message] - @type t :: %__MODULE__{message: String.t} + @type t :: %__MODULE__{message: String.t()} def new({:duplicate_key, key}) do %__MODULE__{message: "duplicate key: #{key}"} end + def new({:invalid_byte, byte, original}) do - %__MODULE__{message: "invalid byte #{inspect byte, base: :hex} in #{inspect original}"} + %__MODULE__{message: "invalid byte #{inspect(byte, base: :hex)} in #{inspect(original)}"} end end -defmodule Jason.Encode do +defmodule JasonV.Encode do @moduledoc """ Utilities for encoding elixir values to JSON. """ import Bitwise - alias Jason.{Codegen, EncodeError, Encoder, Fragment, OrderedObject} + alias JasonV.{Codegen, EncodeError, Encoder, Fragment, OrderedObject} - @typep escape :: (String.t, String.t, integer -> iodata) + @typep escape :: (String.t(), String.t(), integer -> iodata) @typep encode_map :: (map, escape, encode_map -> iodata) @opaque opts :: {escape, encode_map} @@ -29,16 +30,18 @@ defmodule Jason.Encode do # @compile :native @doc false - @spec encode(any, map) :: {:ok, iodata} | {:error, EncodeError.t | Exception.t} + @spec encode(any, map) :: {:ok, iodata} | {:error, EncodeError.t() | Exception.t()} def encode(value, opts) do escape = escape_function(opts) encode_map = encode_map_function(opts) + try do {:ok, value(value, escape, encode_map)} catch :throw, %EncodeError{} = e -> {:error, e} - :error, %Protocol.UndefinedError{protocol: Jason.Encoder} = e -> + + :error, %Protocol.UndefinedError{protocol: JasonV.Encoder} = e -> {:error, e} end end @@ -63,7 +66,7 @@ defmodule Jason.Encode do end @doc """ - Equivalent to calling the `Jason.Encoder.encode/2` protocol function. + Equivalent to calling the `JasonV.Encoder.encode/2` protocol function. Slightly more efficient for built-in types because of the internal dispatching. """ @@ -119,6 +122,7 @@ defmodule Jason.Encode do defp encode_atom(nil, _escape), do: "null" defp encode_atom(true, _escape), do: "true" defp encode_atom(false, _escape), do: "false" + defp encode_atom(atom, escape), do: encode_string(Atom.to_string(atom), escape) @@ -156,8 +160,11 @@ defmodule Jason.Encode do end defp list([head | tail], escape, encode_map) do - [?[, value(head, escape, encode_map) - | list_loop(tail, escape, encode_map)] + [ + ?[, + value(head, escape, encode_map) + | list_loop(tail, escape, encode_map) + ] end defp list_loop([], _escape, _encode_map) do @@ -165,12 +172,16 @@ defmodule Jason.Encode do end defp list_loop([head | tail], escape, encode_map) do - [?,, value(head, escape, encode_map) - | list_loop(tail, escape, encode_map)] + [ + ?,, + value(head, escape, encode_map) + | list_loop(tail, escape, encode_map) + ] 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) end @@ -184,9 +195,13 @@ defmodule Jason.Encode do end defp map_naive([{key, value} | tail], escape, encode_map) do - ["{\"", key(key, escape), "\":", - value(value, escape, encode_map) - | map_naive_loop(tail, escape, encode_map)] + [ + "{\"", + key(key, escape), + "\":", + value(value, escape, encode_map) + | map_naive_loop(tail, escape, encode_map) + ] end defp map_naive_loop([], _escape, _encode_map) do @@ -194,17 +209,26 @@ defmodule Jason.Encode do end defp map_naive_loop([{key, value} | tail], escape, encode_map) do - [",\"", key(key, escape), "\":", - value(value, escape, encode_map) - | map_naive_loop(tail, escape, encode_map)] + [ + ",\"", + key(key, escape), + "\":", + value(value, escape, encode_map) + | map_naive_loop(tail, escape, encode_map) + ] end defp map_strict([{key, value} | tail], escape, encode_map) 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)] + + [ + "{\"", + key, + "\":", + value(value, escape, encode_map) + | map_strict_loop(tail, escape, encode_map, visited) + ] end defp map_strict_loop([], _encode_map, _escape, _visited) do @@ -213,14 +237,21 @@ defmodule Jason.Encode do defp map_strict_loop([{key, value} | tail], escape, encode_map, visited) 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)] + + [ + ",\"", + key, + "\":", + value(value, escape, encode_map) + | map_strict_loop(tail, escape, encode_map, visited) + ] end end @@ -263,16 +294,18 @@ defmodule Jason.Encode do def key(string, escape) when is_binary(string) do escape.(string, string, 0) end + def key(atom, escape) when is_atom(atom) do string = Atom.to_string(atom) escape.(string, string, 0) end + def key(other, escape) do string = String.Chars.to_string(other) escape.(string, string, 0) end - @spec string(String.t, opts) :: iodata + @spec string(String.t(), opts) :: iodata def string(string, {escape, _encode_map}) do encode_string(string, escape) end @@ -291,8 +324,10 @@ defmodule Jason.Encode do {byte, :unicode} -> sequence = List.to_string(:io_lib.format("\\u~4.16.0B", [byte])) defp escape(unquote(byte)), do: unquote(sequence) + {byte, char} when is_integer(char) -> defp escape(unquote(byte)), do: unquote(<>) + {byte, :error} -> defp escape(unquote(byte)), do: throw(:error) end) @@ -311,6 +346,7 @@ defmodule Jason.Encode do when byte === unquote(byte) do escape_json_chunk(rest, acc, original, skip, 1) end + {byte, _escape} -> defp escape_json(<>, acc, original, skip) when byte === unquote(byte) do @@ -318,20 +354,25 @@ defmodule Jason.Encode do escape_json(rest, acc, original, skip + 1) end end) + defp escape_json(<>, acc, original, skip) when char <= 0x7FF do escape_json_chunk(rest, acc, original, skip, 2) end + defp escape_json(<>, acc, original, skip) when char <= 0xFFFF do escape_json_chunk(rest, acc, original, skip, 3) end + defp escape_json(<<_char::utf8, rest::bits>>, acc, original, skip) do escape_json_chunk(rest, acc, original, skip, 4) end + defp escape_json(<<>>, acc, _original, _skip) do acc end + defp escape_json(<>, _acc, original, _skip) do error({:invalid_byte, byte, original}) end @@ -342,6 +383,7 @@ defmodule Jason.Encode do when byte === unquote(byte) do escape_json_chunk(rest, acc, original, skip, len + 1) end + {byte, _escape} -> defp escape_json_chunk(<>, acc, original, skip, len) when byte === unquote(byte) do @@ -350,21 +392,26 @@ defmodule Jason.Encode do escape_json(rest, acc, original, skip + len + 1) end end) + defp escape_json_chunk(<>, acc, original, skip, len) when char <= 0x7FF do escape_json_chunk(rest, acc, original, skip, len + 2) end + defp escape_json_chunk(<>, acc, original, skip, len) when char <= 0xFFFF do escape_json_chunk(rest, acc, original, skip, len + 3) end + defp escape_json_chunk(<<_char::utf8, rest::bits>>, acc, original, skip, len) do escape_json_chunk(rest, acc, original, skip, len + 4) end + defp escape_json_chunk(<<>>, acc, original, skip, len) do part = binary_part(original, skip, len) [acc | part] end + defp escape_json_chunk(<>, _acc, original, _skip, _len) do error({:invalid_byte, byte, original}) end @@ -378,36 +425,43 @@ defmodule Jason.Encode do Enum.map(json_jt, fn {byte, :chunk} -> defp escape_javascript(<>, acc, original, skip) - when byte === unquote(byte) do + when byte === unquote(byte) do escape_javascript_chunk(rest, acc, original, skip, 1) end + {byte, _escape} -> defp escape_javascript(<>, acc, original, skip) - when byte === unquote(byte) do + when byte === unquote(byte) do acc = [acc | escape(byte)] escape_javascript(rest, acc, original, skip + 1) end end) + defp escape_javascript(<>, acc, original, skip) - when char <= 0x7FF do + when char <= 0x7FF do escape_javascript_chunk(rest, acc, original, skip, 2) end + Enum.map(surogate_escapes, fn {byte, escape} -> defp escape_javascript(<>, acc, original, skip) do acc = [acc | unquote(escape)] escape_javascript(rest, acc, original, skip + 3) end end) + defp escape_javascript(<>, acc, original, skip) - when char <= 0xFFFF do + when char <= 0xFFFF do escape_javascript_chunk(rest, acc, original, skip, 3) end + defp escape_javascript(<<_char::utf8, rest::bits>>, acc, original, skip) do escape_javascript_chunk(rest, acc, original, skip, 4) end + defp escape_javascript(<<>>, acc, _original, _skip) do acc end + defp escape_javascript(<>, _acc, original, _skip) do error({:invalid_byte, byte, original}) end @@ -415,21 +469,24 @@ defmodule Jason.Encode do Enum.map(json_jt, fn {byte, :chunk} -> defp escape_javascript_chunk(<>, acc, original, skip, len) - when byte === unquote(byte) do + when byte === unquote(byte) do escape_javascript_chunk(rest, acc, original, skip, len + 1) end + {byte, _escape} -> defp escape_javascript_chunk(<>, acc, original, skip, len) - when byte === unquote(byte) do + when byte === unquote(byte) do part = binary_part(original, skip, len) acc = [acc, part | escape(byte)] escape_javascript(rest, acc, original, skip + len + 1) end end) + defp escape_javascript_chunk(<>, acc, original, skip, len) - when char <= 0x7FF do + when char <= 0x7FF do escape_javascript_chunk(rest, acc, original, skip, len + 2) end + Enum.map(surogate_escapes, fn {byte, escape} -> defp escape_javascript_chunk(<>, acc, original, skip, len) do part = binary_part(original, skip, len) @@ -437,17 +494,21 @@ defmodule Jason.Encode do escape_javascript(rest, acc, original, skip + len + 3) end end) + defp escape_javascript_chunk(<>, acc, original, skip, len) - when char <= 0xFFFF do + when char <= 0xFFFF do escape_javascript_chunk(rest, acc, original, skip, len + 3) end + defp escape_javascript_chunk(<<_char::utf8, rest::bits>>, acc, original, skip, len) do escape_javascript_chunk(rest, acc, original, skip, len + 4) end + defp escape_javascript_chunk(<<>>, acc, original, skip, len) do part = binary_part(original, skip, len) [acc | part] end + defp escape_javascript_chunk(<>, _acc, original, _skip, _len) do error({:invalid_byte, byte, original}) end @@ -463,36 +524,43 @@ defmodule Jason.Encode do Enum.map(html_jt, fn {byte, :chunk} -> defp escape_html(<>, acc, original, skip) - when byte === unquote(byte) do + when byte === unquote(byte) do escape_html_chunk(rest, acc, original, skip, 1) end + {byte, _escape} -> defp escape_html(<>, acc, original, skip) - when byte === unquote(byte) do + when byte === unquote(byte) do acc = [acc | escape(byte)] escape_html(rest, acc, original, skip + 1) end end) + defp escape_html(<>, acc, original, skip) - when char <= 0x7FF do + when char <= 0x7FF do escape_html_chunk(rest, acc, original, skip, 2) end + Enum.map(surogate_escapes, fn {byte, escape} -> defp escape_html(<>, acc, original, skip) do acc = [acc | unquote(escape)] escape_html(rest, acc, original, skip + 3) end end) + defp escape_html(<>, acc, original, skip) - when char <= 0xFFFF do + when char <= 0xFFFF do escape_html_chunk(rest, acc, original, skip, 3) end + defp escape_html(<<_char::utf8, rest::bits>>, acc, original, skip) do escape_html_chunk(rest, acc, original, skip, 4) end + defp escape_html(<<>>, acc, _original, _skip) do acc end + defp escape_html(<>, _acc, original, _skip) do error({:invalid_byte, byte, original}) end @@ -500,21 +568,24 @@ defmodule Jason.Encode do Enum.map(html_jt, fn {byte, :chunk} -> defp escape_html_chunk(<>, acc, original, skip, len) - when byte === unquote(byte) do + when byte === unquote(byte) do escape_html_chunk(rest, acc, original, skip, len + 1) end + {byte, _escape} -> defp escape_html_chunk(<>, acc, original, skip, len) - when byte === unquote(byte) do + when byte === unquote(byte) do part = binary_part(original, skip, len) acc = [acc, part | escape(byte)] escape_html(rest, acc, original, skip + len + 1) end end) + defp escape_html_chunk(<>, acc, original, skip, len) - when char <= 0x7FF do + when char <= 0x7FF do escape_html_chunk(rest, acc, original, skip, len + 2) end + Enum.map(surogate_escapes, fn {byte, escape} -> defp escape_html_chunk(<>, acc, original, skip, len) do part = binary_part(original, skip, len) @@ -522,17 +593,21 @@ defmodule Jason.Encode do escape_html(rest, acc, original, skip + len + 3) end end) + defp escape_html_chunk(<>, acc, original, skip, len) - when char <= 0xFFFF do + when char <= 0xFFFF do escape_html_chunk(rest, acc, original, skip, len + 3) end + defp escape_html_chunk(<<_char::utf8, rest::bits>>, acc, original, skip, len) do escape_html_chunk(rest, acc, original, skip, len + 4) end + defp escape_html_chunk(<<>>, acc, original, skip, len) do part = binary_part(original, skip, len) [acc | part] end + defp escape_html_chunk(<>, _acc, original, _skip, _len) do error({:invalid_byte, byte, original}) end @@ -549,6 +624,7 @@ defmodule Jason.Encode do when byte === unquote(byte) do escape_unicode_chunk(rest, acc, original, skip, 1) end + {byte, _escape} -> defp escape_unicode(<>, acc, original, skip) when byte === unquote(byte) do @@ -556,39 +632,48 @@ defmodule Jason.Encode do escape_unicode(rest, acc, original, skip + 1) end end) + defp escape_unicode(<>, acc, original, skip) when char <= 0xFF do acc = [acc, "\\u00" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + 2) end + defp escape_unicode(<>, acc, original, skip) - when char <= 0x7FF do + when char <= 0x7FF do acc = [acc, "\\u0" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + 2) end + defp escape_unicode(<>, acc, original, skip) - when char <= 0xFFF do + when char <= 0xFFF do acc = [acc, "\\u0" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + 3) end + defp escape_unicode(<>, acc, original, skip) - when char <= 0xFFFF do + when char <= 0xFFFF do acc = [acc, "\\u" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + 3) end + defp escape_unicode(<>, acc, original, skip) do char = char - 0x10000 - acc = - [ - acc, - "\\uD", Integer.to_string(0x800 ||| (char >>> 10), 16), - "\\uD" | Integer.to_string(0xC00 ||| (char &&& 0x3FF), 16) - ] + + acc = [ + acc, + "\\uD", + Integer.to_string(0x800 ||| char >>> 10, 16), + "\\uD" | Integer.to_string(0xC00 ||| (char &&& 0x3FF), 16) + ] + escape_unicode(rest, acc, original, skip + 4) end + defp escape_unicode(<<>>, acc, _original, _skip) do acc end + defp escape_unicode(<>, _acc, original, _skip) do error({:invalid_byte, byte, original}) end @@ -596,62 +681,73 @@ defmodule Jason.Encode do Enum.map(json_jt, fn {byte, :chunk} -> defp escape_unicode_chunk(<>, acc, original, skip, len) - when byte === unquote(byte) do + when byte === unquote(byte) do escape_unicode_chunk(rest, acc, original, skip, len + 1) end + {byte, _escape} -> defp escape_unicode_chunk(<>, acc, original, skip, len) - when byte === unquote(byte) do + when byte === unquote(byte) do part = binary_part(original, skip, len) acc = [acc, part | escape(byte)] escape_unicode(rest, acc, original, skip + len + 1) end end) + defp escape_unicode_chunk(<>, acc, original, skip, len) when char <= 0xFF do part = binary_part(original, skip, len) acc = [acc, part, "\\u00" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + len + 2) end + defp escape_unicode_chunk(<>, acc, original, skip, len) - when char <= 0x7FF do + when char <= 0x7FF do part = binary_part(original, skip, len) acc = [acc, part, "\\u0" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + len + 2) end + defp escape_unicode_chunk(<>, acc, original, skip, len) - when char <= 0xFFF do + when char <= 0xFFF do part = binary_part(original, skip, len) acc = [acc, part, "\\u0" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + len + 3) end + defp escape_unicode_chunk(<>, acc, original, skip, len) - when char <= 0xFFFF do + when char <= 0xFFFF do part = binary_part(original, skip, len) acc = [acc, part, "\\u" | Integer.to_string(char, 16)] escape_unicode(rest, acc, original, skip + len + 3) end + defp escape_unicode_chunk(<>, acc, original, skip, len) do char = char - 0x10000 part = binary_part(original, skip, len) - acc = - [ - acc, part, - "\\uD", Integer.to_string(0x800 ||| (char >>> 10), 16), - "\\uD" | Integer.to_string(0xC00 ||| (char &&& 0x3FF), 16) - ] + + acc = [ + acc, + part, + "\\uD", + Integer.to_string(0x800 ||| char >>> 10, 16), + "\\uD" | Integer.to_string(0xC00 ||| (char &&& 0x3FF), 16) + ] + escape_unicode(rest, acc, original, skip + len + 4) end + defp escape_unicode_chunk(<<>>, acc, original, skip, len) do part = binary_part(original, skip, len) [acc | part] end + defp escape_unicode_chunk(<>, _acc, original, _skip, _len) do error({:invalid_byte, byte, original}) end @compile {:inline, error: 1} defp error(error) do - throw EncodeError.new(error) + throw(EncodeError.new(error)) end end diff --git a/lib/encoder.ex b/lib/encoder.ex index d693399..a77fd57 100644 --- a/lib/encoder.ex +++ b/lib/encoder.ex @@ -1,4 +1,4 @@ -defprotocol Jason.Encoder do +defprotocol JasonV.Encoder do @moduledoc """ Protocol controlling how a value is encoded to JSON. @@ -21,46 +21,46 @@ defprotocol Jason.Encoder do defstruct [:foo, :bar, :baz] end - If we were to call `@derive Jason.Encoder` just before `defstruct`, + If we were to call `@derive JasonV.Encoder` just before `defstruct`, an implementation similar to the following implementation would be generated: - defimpl Jason.Encoder, for: Test do + defimpl JasonV.Encoder, for: Test do def encode(value, opts) do - Jason.Encode.map(Map.take(value, [:foo, :bar, :baz]), opts) + JasonV.Encode.map(Map.take(value, [:foo, :bar, :baz]), opts) end end - If we called `@derive {Jason.Encoder, only: [:foo]}`, an implementation + If we called `@derive {JasonV.Encoder, only: [:foo]}`, an implementation similar to the following implementation would be generated: - defimpl Jason.Encoder, for: Test do + defimpl JasonV.Encoder, for: Test do def encode(value, opts) do - Jason.Encode.map(Map.take(value, [:foo]), opts) + JasonV.Encode.map(Map.take(value, [:foo]), opts) end end - If we called `@derive {Jason.Encoder, except: [:foo]}`, an implementation + If we called `@derive {JasonV.Encoder, except: [:foo]}`, an implementation similar to the following implementation would be generated: - defimpl Jason.Encoder, for: Test do + defimpl JasonV.Encoder, for: Test do def encode(value, opts) do - Jason.Encode.map(Map.take(value, [:bar, :baz]), opts) + JasonV.Encode.map(Map.take(value, [:bar, :baz]), opts) end end The actually generated implementations are more efficient computing some data - during compilation similar to the macros from the `Jason.Helpers` module. + during compilation similar to the macros from the `JasonV.Helpers` module. ## Explicit implementation If you wish to implement the protocol fully yourself, it is advised to - use functions from the `Jason.Encode` module to do the actual iodata + use functions from the `JasonV.Encode` module to do the actual iodata generation - they are highly optimized and verified to always produce valid JSON. """ @type t :: term - @type opts :: Jason.Encode.opts() + @type opts :: JasonV.Encode.opts() @fallback_to_any true @@ -68,24 +68,24 @@ defprotocol Jason.Encoder do Encodes `value` to JSON. The argument `opts` is opaque - it can be passed to various functions in - `Jason.Encode` (or to the protocol function itself) for encoding values to JSON. + `JasonV.Encode` (or to the protocol function itself) for encoding values to JSON. """ @spec encode(t, opts) :: iodata def encode(value, opts) end -defimpl Jason.Encoder, for: Any do +defimpl JasonV.Encoder, for: Any do defmacro __deriving__(module, struct, opts) do fields = fields_to_encode(struct, opts) kv = Enum.map(fields, &{&1, generated_var(&1, __MODULE__)}) escape = quote(do: escape) encode_map = quote(do: encode_map) encode_args = [escape, encode_map] - kv_iodata = Jason.Codegen.build_kv_iodata(kv, encode_args) + kv_iodata = JasonV.Codegen.build_kv_iodata(kv, encode_args) quote do - defimpl Jason.Encoder, for: unquote(module) do - require Jason.Helpers + defimpl JasonV.Encoder, for: unquote(module) do + require JasonV.Helpers def encode(%{unquote_splicing(kv)}, {unquote(escape), unquote(encode_map)}) do unquote(kv_iodata) @@ -104,26 +104,26 @@ defimpl Jason.Encoder, for: Any do protocol: @protocol, value: struct, description: """ - Jason.Encoder protocol must always be explicitly implemented. + JasonV.Encoder protocol must always be explicitly implemented. If you own the struct, you can derive the implementation specifying \ which fields should be encoded to JSON: - @derive {Jason.Encoder, only: [....]} + @derive {JasonV.Encoder, only: [....]} defstruct ... It is also possible to encode all fields, although this should be \ used carefully to avoid accidentally leaking private information \ when new fields are added: - @derive Jason.Encoder + @derive JasonV.Encoder defstruct ... Finally, if you don't own the struct you want to encode to JSON, \ you may use Protocol.derive/3 placed outside of any module: - Protocol.derive(Jason.Encoder, NameOfTheStruct, only: [...]) - Protocol.derive(Jason.Encoder, NameOfTheStruct) + Protocol.derive(JasonV.Encoder, NameOfTheStruct, only: [...]) + Protocol.derive(JasonV.Encoder, NameOfTheStruct) """ end @@ -131,7 +131,7 @@ defimpl Jason.Encoder, for: Any do raise Protocol.UndefinedError, protocol: @protocol, value: value, - description: "Jason.Encoder protocol must always be explicitly implemented" + description: "JasonV.Encoder protocol must always be explicitly implemented" end defp fields_to_encode(struct, opts) do @@ -169,42 +169,42 @@ defimpl Jason.Encoder, for: Any do end # The following implementations are formality - they are already covered -# by the main encoding mechanism in Jason.Encode, but exist mostly for +# by the main encoding mechanism in JasonV.Encode, but exist mostly for # documentation purposes and if anybody had the idea to call the protocol directly. -defimpl Jason.Encoder, for: Atom do +defimpl JasonV.Encoder, for: Atom do def encode(atom, opts) do - Jason.Encode.atom(atom, opts) + JasonV.Encode.atom(atom, opts) end end -defimpl Jason.Encoder, for: Integer do +defimpl JasonV.Encoder, for: Integer do def encode(integer, _opts) do - Jason.Encode.integer(integer) + JasonV.Encode.integer(integer) end end -defimpl Jason.Encoder, for: Float do +defimpl JasonV.Encoder, for: Float do def encode(float, _opts) do - Jason.Encode.float(float) + JasonV.Encode.float(float) end end -defimpl Jason.Encoder, for: List do +defimpl JasonV.Encoder, for: List do def encode(list, opts) do - Jason.Encode.list(list, opts) + JasonV.Encode.list(list, opts) end end -defimpl Jason.Encoder, for: Map do +defimpl JasonV.Encoder, for: Map do def encode(map, opts) do - Jason.Encode.map(map, opts) + JasonV.Encode.map(map, opts) end end -defimpl Jason.Encoder, for: BitString do +defimpl JasonV.Encoder, for: BitString do def encode(binary, opts) when is_binary(binary) do - Jason.Encode.string(binary, opts) + JasonV.Encode.string(binary, opts) end def encode(bitstring, _opts) do @@ -215,13 +215,13 @@ defimpl Jason.Encoder, for: BitString do end end -defimpl Jason.Encoder, for: [Date, Time, NaiveDateTime, DateTime] do +defimpl JasonV.Encoder, for: [Date, Time, NaiveDateTime, DateTime] do def encode(value, _opts) do [?", @for.to_iso8601(value), ?"] end end -defimpl Jason.Encoder, for: Decimal do +defimpl JasonV.Encoder, for: Decimal do def encode(value, _opts) do # silence the xref warning decimal = Decimal @@ -229,7 +229,7 @@ defimpl Jason.Encoder, for: Decimal do end end -defimpl Jason.Encoder, for: Jason.Fragment do +defimpl JasonV.Encoder, for: JasonV.Fragment do def encode(%{encode: encode}, opts) do encode.(opts) end diff --git a/lib/formatter.ex b/lib/formatter.ex index 88826eb..63f2cba 100644 --- a/lib/formatter.ex +++ b/lib/formatter.ex @@ -1,4 +1,4 @@ -defmodule Jason.Formatter do +defmodule JasonV.Formatter do @moduledoc ~S""" Pretty-printing and minimizing functions for JSON-encoded data. @@ -36,7 +36,7 @@ defmodule Jason.Formatter do ## Examples - iex> Jason.Formatter.pretty_print(~s|{"a":{"b": [1, 2]}}|) + iex> JasonV.Formatter.pretty_print(~s|{"a":{"b": [1, 2]}}|) ~s|{ "a": { "b": [ @@ -88,7 +88,7 @@ defmodule Jason.Formatter do ## Examples - iex> Jason.Formatter.minimize(~s|{ "a" : "b" , "c": \n\n 2}|) + iex> JasonV.Formatter.minimize(~s|{ "a" : "b" , "c": \n\n 2}|) ~s|{"a":"b","c":2}| """ @@ -123,6 +123,7 @@ defmodule Jason.Formatter do defp parse_opts([{option, value} | opts], indent, line, record, colon) do value = IO.iodata_to_binary(value) + case option do :indent -> parse_opts(opts, value, line, record, colon) :record_separator -> parse_opts(opts, indent, line, value, colon) @@ -227,8 +228,10 @@ defmodule Jason.Formatter do case :binary.match(binary, ["\"", "\\"]) do :nomatch -> {[output_acc | binary], &pp_string(&1, &2, false, cont)} + {pos, 1} -> {head, tail} = :erlang.split_binary(binary, pos + 1) + case :binary.at(binary, pos) do ?\\ -> pp_string(tail, [output_acc | head], true, cont) ?" -> cont.(tail, [output_acc | head]) diff --git a/lib/fragment.ex b/lib/fragment.ex index 2bcde6b..ffe0940 100644 --- a/lib/fragment.ex +++ b/lib/fragment.ex @@ -1,4 +1,4 @@ -defmodule Jason.Fragment do +defmodule JasonV.Fragment do defstruct [:encode] def new(iodata) when is_list(iodata) or is_binary(iodata) do diff --git a/lib/helpers.ex b/lib/helpers.ex index f94678d..d29711b 100644 --- a/lib/helpers.ex +++ b/lib/helpers.ex @@ -1,18 +1,18 @@ -defmodule Jason.Helpers do +defmodule JasonV.Helpers do @moduledoc """ Provides macro facilities for partial compile-time encoding of JSON. """ - alias Jason.{Codegen, Fragment} + alias JasonV.{Codegen, Fragment} @doc ~S""" Encodes a JSON map from a compile-time keyword. Encodes the keys at compile time and strives to create as flat iodata structure as possible to achieve maximum efficiency. Does encoding - right at the call site, but returns an `%Jason.Fragment{}` struct + right at the call site, but returns an `%JasonV.Fragment{}` struct that needs to be passed to one of the "main" encoding functions - - for example `Jason.encode/2` for final encoding into JSON - this + for example `JasonV.encode/2` for final encoding into JSON - this makes it completely transparent for most uses. Only allows keys that do not require escaping in any of the supported @@ -25,7 +25,7 @@ defmodule Jason.Helpers do ## Example iex> fragment = json_map(foo: 1, bar: 2) - iex> Jason.encode!(fragment) + iex> JasonV.encode!(fragment) "{\"foo\":1,\"bar\":2}" """ @@ -63,7 +63,7 @@ defmodule Jason.Helpers do iex> map = %{a: 1, b: 2, c: 3} iex> fragment = json_map_take(map, [:c, :b]) - iex> Jason.encode!(fragment) + iex> JasonV.encode!(fragment) "{\"c\":3,\"b\":2}" """ diff --git a/lib/jason.ex b/lib/jason.ex index 2bfa013..fe9a424 100644 --- a/lib/jason.ex +++ b/lib/jason.ex @@ -1,9 +1,9 @@ -defmodule Jason do +defmodule JasonV do @moduledoc """ A blazing fast JSON parser and generator in pure Elixir. """ - alias Jason.{Encode, Decoder, DecodeError, EncodeError, Formatter} + alias JasonV.{Encode, Decoder, DecodeError, EncodeError, Formatter} @type escape :: :json | :unicode_safe | :html_safe | :javascript_safe @type maps :: :naive | :strict @@ -48,7 +48,7 @@ defmodule Jason do * `:objects` - controls how objects are decoded. Possible values are: * `:maps` (default) - objects are decoded as maps - * `:ordered_objects` - objects are decoded as `Jason.OrderedObject` structs + * `:ordered_objects` - objects are decoded as `JasonV.OrderedObject` structs ## Decoding keys to atoms @@ -58,11 +58,11 @@ defmodule Jason do ## Examples - iex> Jason.decode("{}") + iex> JasonV.decode("{}") {:ok, %{}} - iex> Jason.decode("invalid") - {:error, %Jason.DecodeError{data: "invalid", position: 0, token: nil}} + iex> JasonV.decode("invalid") + {:error, %JasonV.DecodeError{data: "invalid", position: 0, token: nil}} """ @spec decode(iodata, [decode_opt]) :: {:ok, term} | {:error, DecodeError.t()} def decode(input, opts \\ []) do @@ -78,11 +78,11 @@ defmodule Jason do ## Examples - iex> Jason.decode!("{}") + iex> JasonV.decode!("{}") %{} - iex> Jason.decode!("invalid") - ** (Jason.DecodeError) unexpected byte at position 0: 0x69 ("i") + iex> JasonV.decode!("invalid") + ** (JasonV.DecodeError) unexpected byte at position 0: 0x69 ("i") """ @spec decode!(iodata, [decode_opt]) :: term | no_return @@ -96,7 +96,7 @@ defmodule Jason do @doc """ Generates JSON corresponding to `input`. - The generation is controlled by the `Jason.Encoder` protocol, + The generation is controlled by the `JasonV.Encoder` protocol, please refer to the module to read more on how to define the protocol for custom data types. @@ -122,15 +122,15 @@ defmodule Jason do * `:pretty` - controls pretty printing of the output. Possible values are: * `true` to pretty print with default configuration - * a keyword of options as specified by `Jason.Formatter.pretty_print/2`. + * a keyword of options as specified by `JasonV.Formatter.pretty_print/2`. ## Examples - iex> Jason.encode(%{a: 1}) + iex> JasonV.encode(%{a: 1}) {:ok, ~S|{"a":1}|} - iex> Jason.encode("\\xFF") - {:error, %Jason.EncodeError{message: "invalid byte 0xFF in <<255>>"}} + iex> JasonV.encode("\\xFF") + {:error, %JasonV.EncodeError{message: "invalid byte 0xFF in <<255>>"}} """ @spec encode(term, [encode_opt]) :: @@ -150,11 +150,11 @@ defmodule Jason do ## Examples - iex> Jason.encode!(%{a: 1}) + iex> JasonV.encode!(%{a: 1}) ~S|{"a":1}| - iex> Jason.encode!("\\xFF") - ** (Jason.EncodeError) invalid byte 0xFF in <<255>> + iex> JasonV.encode!("\\xFF") + ** (JasonV.EncodeError) invalid byte 0xFF in <<255>> """ @spec encode!(term, [encode_opt]) :: String.t() | no_return @@ -176,12 +176,12 @@ defmodule Jason do ## Examples - iex> {:ok, iodata} = Jason.encode_to_iodata(%{a: 1}) + iex> {:ok, iodata} = JasonV.encode_to_iodata(%{a: 1}) iex> IO.iodata_to_binary(iodata) ~S|{"a":1}| - iex> Jason.encode_to_iodata("\\xFF") - {:error, %Jason.EncodeError{message: "invalid byte 0xFF in <<255>>"}} + iex> JasonV.encode_to_iodata("\\xFF") + {:error, %JasonV.EncodeError{message: "invalid byte 0xFF in <<255>>"}} """ @spec encode_to_iodata(term, [encode_opt]) :: @@ -198,12 +198,12 @@ defmodule Jason do ## Examples - iex> iodata = Jason.encode_to_iodata!(%{a: 1}) + iex> iodata = JasonV.encode_to_iodata!(%{a: 1}) iex> IO.iodata_to_binary(iodata) ~S|{"a":1}| - iex> Jason.encode_to_iodata!("\\xFF") - ** (Jason.EncodeError) invalid byte 0xFF in <<255>> + iex> JasonV.encode_to_iodata!("\\xFF") + ** (JasonV.EncodeError) invalid byte 0xFF in <<255>> """ @spec encode_to_iodata!(term, [encode_opt]) :: iodata | no_return diff --git a/lib/ordered_object.ex b/lib/ordered_object.ex index 52831f3..c309361 100644 --- a/lib/ordered_object.ex +++ b/lib/ordered_object.ex @@ -1,4 +1,4 @@ -defmodule Jason.OrderedObject do +defmodule JasonV.OrderedObject do @doc """ Struct implementing a JSON object retaining order of properties. @@ -74,7 +74,7 @@ defmodule Jason.OrderedObject do defp delete_key([], _key), do: [] end -defimpl Enumerable, for: Jason.OrderedObject do +defimpl Enumerable, for: JasonV.OrderedObject do def count(%{values: []}), do: {:ok, 0} def count(_obj), do: {:error, __MODULE__} @@ -87,8 +87,8 @@ defimpl Enumerable, for: Jason.OrderedObject do def reduce(%{values: values}, acc, fun), do: Enumerable.List.reduce(values, acc, fun) end -defimpl Jason.Encoder, for: Jason.OrderedObject do +defimpl JasonV.Encoder, for: JasonV.OrderedObject do def encode(%{values: values}, opts) do - Jason.Encode.keyword(values, opts) + JasonV.Encode.keyword(values, opts) end end diff --git a/lib/sigil.ex b/lib/sigil.ex index e5447ef..2c67eae 100644 --- a/lib/sigil.ex +++ b/lib/sigil.ex @@ -1,15 +1,15 @@ -defmodule Jason.Sigil do +defmodule JasonV.Sigil do @doc ~S""" Handles the sigil `~j` for JSON strings. - Calls `Jason.decode!/2` with modifiers mapped to options. + Calls `JasonV.decode!/2` with modifiers mapped to options. Given a string literal without interpolations, decodes the string at compile-time. ## Modifiers - See `Jason.decode/2` for detailed descriptions. + See `JasonV.decode/2` for detailed descriptions. * `a` - equivalent to `{:keys, :atoms}` option * `A` - equivalent to `{:keys, :atoms!}` option @@ -40,11 +40,11 @@ defmodule Jason.Sigil do defmacro sigil_j(term, modifiers) defmacro sigil_j({:<<>>, _meta, [string]}, modifiers) when is_binary(string) do - Macro.escape(Jason.decode!(string, mods_to_opts(modifiers))) + Macro.escape(JasonV.decode!(string, mods_to_opts(modifiers))) end defmacro sigil_j(term, modifiers) do - quote(do: Jason.decode!(unquote(term), unquote(mods_to_opts(modifiers)))) + quote(do: JasonV.decode!(unquote(term), unquote(mods_to_opts(modifiers)))) end @doc ~S""" @@ -67,10 +67,10 @@ defmodule Jason.Sigil do defmacro sigil_J(term, modifiers) defmacro sigil_J({:<<>>, _meta, [string]}, modifiers) when is_binary(string) do - Macro.escape(Jason.decode!(string, mods_to_opts(modifiers))) + Macro.escape(JasonV.decode!(string, mods_to_opts(modifiers))) end - @spec mods_to_opts(charlist) :: [Jason.decode_opt()] + @spec mods_to_opts(charlist) :: [JasonV.decode_opt()] defp mods_to_opts(modifiers) do modifiers |> Enum.map(fn diff --git a/mix.exs b/mix.exs index 5d1baf1..641857e 100644 --- a/mix.exs +++ b/mix.exs @@ -1,4 +1,4 @@ -defmodule Jason.Mixfile do +defmodule JasonV.Mixfile do use Mix.Project @source_url "https://github.com/michalmuskala/jason" @@ -6,7 +6,7 @@ defmodule Jason.Mixfile do def project() do [ - app: :jason, + app: :jason_v, version: @version, elixir: "~> 1.4", start_permanent: Mix.env() == :prod, @@ -66,7 +66,7 @@ defmodule Jason.Mixfile do defp docs() do [ main: "readme", - name: "Jason", + name: "JasonV", source_ref: "v#{@version}", canonical: "http://hexdocs.pm/jason", source_url: @source_url, diff --git a/test/decode_test.exs b/test/decode_test.exs index 937388d..0718f78 100644 --- a/test/decode_test.exs +++ b/test/decode_test.exs @@ -1,7 +1,7 @@ -defmodule Jason.DecodeTest do +defmodule JasonV.DecodeTest do use ExUnit.Case, async: true - alias Jason.DecodeError + alias JasonV.DecodeError test "numbers" do assert_fail_with "-", ~S|unexpected end of input at position 1| @@ -30,7 +30,7 @@ defmodule Jason.DecodeTest do assert parse!("0.1e-1") == 0.1e-1 assert parse!("99.99e99") == 99.99e99 assert parse!("-99.99e-99") == -99.99e-99 - assert parse!("123456789.123456789e123") == 123456789.123456789e123 + assert parse!("123456789.123456789e123") == 123_456_789.123456789e123 end test "strings" do @@ -79,10 +79,12 @@ defmodule Jason.DecodeTest do assert parse!(~s({"foo": "bar"}), keys: :atoms) == %{foo: "bar"} assert parse!(~s({"foo": "bar"}), keys: :atoms!) == %{foo: "bar"} - key = Integer.to_string(System.unique_integer) + key = Integer.to_string(System.unique_integer()) + assert_raise ArgumentError, fn -> parse!(~s({"#{key}": "value"}), keys: :atoms!) end + key = String.to_atom(key) assert parse!(~s({"#{key}": "value"}), keys: :atoms) == %{key => "value"} end @@ -113,7 +115,7 @@ defmodule Jason.DecodeTest do end test "decoding objects preserving order" do - import Jason.OrderedObject, only: [new: 1] + import JasonV.OrderedObject, only: [new: 1] assert parse!("{}", objects: :ordered_objects) == new([]) assert parse!(~s({"foo": "bar"}), objects: :ordered_objects) == new([{"foo", "bar"}]) @@ -172,7 +174,7 @@ defmodule Jason.DecodeTest do end defp parse!(json, opts \\ []) do - Jason.decode!(json, opts) + JasonV.decode!(json, opts) end defp assert_fail_with(string, error) do diff --git a/test/encode_test.exs b/test/encode_test.exs index 4f5d33c..0b8ad61 100644 --- a/test/encode_test.exs +++ b/test/encode_test.exs @@ -1,7 +1,7 @@ -defmodule Jason.EncoderTest do +defmodule JasonV.EncoderTest do use ExUnit.Case, async: true - alias Jason.{EncodeError, Encoder} + alias JasonV.{EncodeError, Encoder} test "atom" do assert to_json(nil) == "null" @@ -31,7 +31,10 @@ defmodule Jason.EncoderTest do assert to_json("๐„žb", escape: :unicode_safe) == ~s("\\uD834\\uDD1Eb") assert to_json("\u2028\u2029abc", escape: :javascript_safe) == ~s("\\u2028\\u2029abc") assert to_json("", escape: :html_safe) == ~s("\\u003C\\/script>") - assert to_json(~s(), escape: :html_safe) == ~s("\\u003Cscript>var s = \\\"\\u2028\\u2029\\\";\\u003C\\/script>") + + assert to_json(~s(), escape: :html_safe) == + ~s("\\u003Cscript>var s = \\\"\\u2028\\u2029\\\";\\u003C\\/script>") + assert to_json("