Restructure as monorepo and add flake packages/apps
- Move Elixir code to server/ subdirectory for monorepo structure - Update flake.nix to provide packages and apps outputs for nix run support - Update nix/package.nix to accept src parameter instead of fetchgit - Add NixOS module export for easy consumption Now supports: nix run, nix build, and nix develop from git repo 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
defmodule Jason.Codegen do
|
||||
@moduledoc false
|
||||
|
||||
alias Jason.{Encode, EncodeError}
|
||||
|
||||
def jump_table(ranges, default) do
|
||||
ranges
|
||||
|> ranges_to_orddict()
|
||||
|> :array.from_orddict(default)
|
||||
|> :array.to_orddict()
|
||||
end
|
||||
|
||||
def jump_table(ranges, default, max) do
|
||||
ranges
|
||||
|> ranges_to_orddict()
|
||||
|> :array.from_orddict(default)
|
||||
|> resize(max)
|
||||
|> :array.to_orddict()
|
||||
end
|
||||
|
||||
defmacro bytecase(var, do: clauses) do
|
||||
{ranges, default, literals} = clauses_to_ranges(clauses, [], __CALLER__)
|
||||
|
||||
jump_table = jump_table(ranges, default)
|
||||
|
||||
quote do
|
||||
case unquote(var) do
|
||||
unquote(jump_table_to_clauses(jump_table, literals))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro bytecase(var, max, do: clauses) do
|
||||
{ranges, default, empty} = clauses_to_ranges(clauses, [], __CALLER__)
|
||||
|
||||
jump_table = jump_table(ranges, default, max)
|
||||
|
||||
quote do
|
||||
case unquote(var) do
|
||||
unquote(jump_table_to_clauses(jump_table, empty))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_kv_iodata(kv, encode_args) do
|
||||
elements =
|
||||
kv
|
||||
|> Enum.map(&encode_pair(&1, encode_args))
|
||||
|> Enum.intersperse(",")
|
||||
|
||||
collapse_static(List.flatten(["{", elements] ++ ~c'}'))
|
||||
end
|
||||
|
||||
defp clauses_to_ranges([{:->, _, [[{:in, _, [byte, range]}, rest], action]} | tail], acc, env) do
|
||||
range = Macro.expand(range, env)
|
||||
clauses_to_ranges(tail, [{range, {byte, rest, action}} | acc], env)
|
||||
end
|
||||
|
||||
defp clauses_to_ranges([{:->, _, [[default, rest], action]} | tail], acc, _env) do
|
||||
{Enum.reverse(acc), {default, rest, action}, literal_clauses(tail)}
|
||||
end
|
||||
|
||||
defp literal_clauses(clauses) do
|
||||
Enum.map(clauses, fn {:->, _, [[literal], action]} ->
|
||||
{literal, action}
|
||||
end)
|
||||
end
|
||||
|
||||
defp jump_table_to_clauses([{val, {{:_, _, _}, rest, action}} | tail], empty) do
|
||||
quote do
|
||||
<<unquote(val), unquote(rest)::bits>> ->
|
||||
unquote(action)
|
||||
end ++ jump_table_to_clauses(tail, empty)
|
||||
end
|
||||
|
||||
defp jump_table_to_clauses([{val, {byte, rest, action}} | tail], empty) do
|
||||
quote do
|
||||
<<unquote(byte), unquote(rest)::bits>> when unquote(byte) === unquote(val) ->
|
||||
unquote(action)
|
||||
end ++ jump_table_to_clauses(tail, empty)
|
||||
end
|
||||
|
||||
defp jump_table_to_clauses([], literals) do
|
||||
Enum.flat_map(literals, fn {pattern, action} ->
|
||||
quote do
|
||||
unquote(pattern) ->
|
||||
unquote(action)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp resize(array, size), do: :array.resize(size, array)
|
||||
|
||||
defp ranges_to_orddict(ranges) do
|
||||
ranges
|
||||
|> Enum.flat_map(fn
|
||||
{int, value} when is_integer(int) ->
|
||||
[{int, value}]
|
||||
|
||||
{enum, value} ->
|
||||
Enum.map(enum, &{&1, value})
|
||||
end)
|
||||
|> :orddict.from_list()
|
||||
end
|
||||
|
||||
defp encode_pair({key, value}, encode_args) do
|
||||
key = IO.iodata_to_binary(Encode.key(key, &escape_key/3))
|
||||
key = "\"" <> key <> "\":"
|
||||
[key, quote(do: Encode.value(unquote(value), unquote_splicing(encode_args)))]
|
||||
end
|
||||
|
||||
defp escape_key(binary, _original, _skip) do
|
||||
check_safe_key!(binary)
|
||||
binary
|
||||
end
|
||||
|
||||
defp check_safe_key!(binary) do
|
||||
for <<(<<byte>> <- binary)>> do
|
||||
if byte > 0x7F or byte < 0x1F or byte in ~c'"\\/' do
|
||||
raise EncodeError,
|
||||
"invalid byte #{inspect(byte, base: :hex)} in literal key: #{inspect(binary)}"
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp collapse_static([bin1, bin2 | rest]) when is_binary(bin1) and is_binary(bin2) do
|
||||
collapse_static([bin1 <> bin2 | rest])
|
||||
end
|
||||
|
||||
defp collapse_static([other | rest]) do
|
||||
[other | collapse_static(rest)]
|
||||
end
|
||||
|
||||
defp collapse_static([]) do
|
||||
[]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,708 @@
|
||||
defmodule Jason.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}"
|
||||
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 = <<byte>>
|
||||
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}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Jason.Decoder do
|
||||
@moduledoc false
|
||||
|
||||
import Bitwise
|
||||
|
||||
alias Jason.{DecodeError, Codegen}
|
||||
|
||||
import Codegen, only: [bytecase: 2, bytecase: 3]
|
||||
import Record
|
||||
|
||||
@dialyzer :no_improper_lists
|
||||
|
||||
# @compile :native
|
||||
|
||||
# We use integers instead of atoms to take advantage of the jump table
|
||||
# optimization
|
||||
@terminate 0
|
||||
@array 1
|
||||
@key 2
|
||||
@object 3
|
||||
|
||||
defrecordp :decode, [keys: nil, strings: nil, objects: nil, floats: nil]
|
||||
|
||||
def parse(data, opts) when is_binary(data) do
|
||||
key_decode = key_decode_function(opts)
|
||||
string_decode = string_decode_function(opts)
|
||||
float_decode = float_decode_function(opts)
|
||||
object_decode = object_decode_function(opts)
|
||||
decode = decode(keys: key_decode, strings: string_decode, objects: object_decode, floats: float_decode)
|
||||
try do
|
||||
value(data, data, 0, [@terminate], decode)
|
||||
catch
|
||||
{:position, position} ->
|
||||
{:error, %DecodeError{position: position, data: data}}
|
||||
{:token, token, position} ->
|
||||
{:error, %DecodeError{token: token, position: position, data: data}}
|
||||
else
|
||||
value ->
|
||||
{:ok, value}
|
||||
end
|
||||
end
|
||||
|
||||
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: 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 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 float_decode_function(%{floats: :native}) do
|
||||
fn string, token, skip ->
|
||||
try do
|
||||
:erlang.binary_to_float(string)
|
||||
catch
|
||||
:error, :badarg ->
|
||||
token_error(token, skip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Decimal) do
|
||||
defp float_decode_function(%{floats: :decimals}) do
|
||||
fn string, token, skip ->
|
||||
try do
|
||||
Decimal.new(string)
|
||||
rescue
|
||||
Decimal.Error ->
|
||||
token_error(token, skip)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
defp float_decode_function(%{floats: :decimals}) do
|
||||
raise ArgumentError, "decimal library not found, :decimals option not available"
|
||||
end
|
||||
end
|
||||
|
||||
defp value(data, original, skip, stack, decode) do
|
||||
bytecase data do
|
||||
_ in ~c'\s\n\t\r', rest ->
|
||||
value(rest, original, skip + 1, stack, decode)
|
||||
_ in ~c'0', rest ->
|
||||
number_zero(rest, original, skip, stack, decode, 1)
|
||||
_ in ~c'123456789', rest ->
|
||||
number(rest, original, skip, stack, decode, 1)
|
||||
_ in ~c'-', rest ->
|
||||
number_minus(rest, original, skip, stack, decode)
|
||||
_ in ~c'"', rest ->
|
||||
string(rest, original, skip + 1, stack, decode, 0)
|
||||
_ in ~c'[', rest ->
|
||||
array(rest, original, skip + 1, stack, decode)
|
||||
_ in ~c'{', rest ->
|
||||
object(rest, original, skip + 1, stack, decode)
|
||||
_ in ~c']', rest ->
|
||||
empty_array(rest, original, skip + 1, stack, decode)
|
||||
_ in ~c't', rest ->
|
||||
case rest do
|
||||
<<"rue", rest::bits>> ->
|
||||
continue(rest, original, skip + 4, stack, decode, true)
|
||||
<<_::bits>> ->
|
||||
error(original, skip)
|
||||
end
|
||||
_ in ~c'f', rest ->
|
||||
case rest do
|
||||
<<"alse", rest::bits>> ->
|
||||
continue(rest, original, skip + 5, stack, decode, false)
|
||||
<<_::bits>> ->
|
||||
error(original, skip)
|
||||
end
|
||||
_ in ~c'n', rest ->
|
||||
case rest do
|
||||
<<"ull", rest::bits>> ->
|
||||
continue(rest, original, skip + 4, stack, decode, nil)
|
||||
<<_::bits>> ->
|
||||
error(original, skip)
|
||||
end
|
||||
_, rest ->
|
||||
error(rest, original, skip + 1, stack, decode)
|
||||
<<_::bits>> ->
|
||||
error(original, skip)
|
||||
end
|
||||
end
|
||||
|
||||
defp number_minus(<<?0, rest::bits>>, original, skip, stack, decode) do
|
||||
number_zero(rest, original, skip, stack, decode, 2)
|
||||
end
|
||||
defp number_minus(<<byte, rest::bits>>, original, skip, stack, decode)
|
||||
when byte in ~c'123456789' do
|
||||
number(rest, original, skip, stack, decode, 2)
|
||||
end
|
||||
defp number_minus(<<_rest::bits>>, original, skip, _stack, _decode) do
|
||||
error(original, skip + 1)
|
||||
end
|
||||
|
||||
if function_exported?(Application, :compile_env, 3) do
|
||||
@integer_digit_limit Application.compile_env(:jason, :decoding_integer_digit_limit, 1024)
|
||||
else
|
||||
# use apply to avoid warnings in newer Elixir versions
|
||||
@integer_digit_limit apply(Application, :get_env, [:jason, :decoding_integer_digit_limit, 1024])
|
||||
end
|
||||
|
||||
defp number(<<byte, rest::bits>>, original, skip, stack, decode, len)
|
||||
when byte in ~c'0123456789' do
|
||||
number(rest, original, skip, stack, decode, len + 1)
|
||||
end
|
||||
defp number(<<?., rest::bits>>, original, skip, stack, decode, len) do
|
||||
number_frac(rest, original, skip, stack, decode, len + 1)
|
||||
end
|
||||
defp number(<<e, rest::bits>>, original, skip, stack, decode, len) when e in ~c'eE' do
|
||||
prefix = binary_part(original, skip, len)
|
||||
number_exp_copy(rest, original, skip + len + 1, stack, decode, prefix)
|
||||
end
|
||||
defp number(<<rest::bits>>, original, skip, stack, decode, len) do
|
||||
token = binary_part(original, skip, len)
|
||||
if byte_size(token) > @integer_digit_limit do
|
||||
token_error(token, skip)
|
||||
end
|
||||
int = String.to_integer(token)
|
||||
continue(rest, original, skip + len, stack, decode, int)
|
||||
end
|
||||
|
||||
defp number_frac(<<byte, rest::bits>>, original, skip, stack, decode, len)
|
||||
when byte in ~c'0123456789' do
|
||||
number_frac_cont(rest, original, skip, stack, decode, len + 1)
|
||||
end
|
||||
defp number_frac(<<_rest::bits>>, original, skip, _stack, _decode, len) do
|
||||
error(original, skip + len)
|
||||
end
|
||||
|
||||
defp number_frac_cont(<<byte, rest::bits>>, original, skip, stack, decode, len)
|
||||
when byte in ~c'0123456789' do
|
||||
number_frac_cont(rest, original, skip, stack, decode, len + 1)
|
||||
end
|
||||
defp number_frac_cont(<<e, rest::bits>>, original, skip, stack, decode, len)
|
||||
when e in ~c'eE' do
|
||||
number_exp(rest, original, skip, stack, decode, len + 1)
|
||||
end
|
||||
defp number_frac_cont(<<rest::bits>>, original, skip, stack, decode, len) do
|
||||
token = binary_part(original, skip, len)
|
||||
decode(floats: float_decode) = decode
|
||||
float = float_decode.(token, token, skip)
|
||||
continue(rest, original, skip + len, stack, decode, float)
|
||||
end
|
||||
|
||||
defp number_exp(<<byte, rest::bits>>, original, skip, stack, decode, len)
|
||||
when byte in ~c'0123456789' do
|
||||
number_exp_cont(rest, original, skip, stack, decode, len + 1)
|
||||
end
|
||||
defp number_exp(<<byte, rest::bits>>, original, skip, stack, decode, len)
|
||||
when byte in ~c'+-' do
|
||||
number_exp_sign(rest, original, skip, stack, decode, len + 1)
|
||||
end
|
||||
defp number_exp(<<_rest::bits>>, original, skip, _stack, _decode, len) do
|
||||
error(original, skip + len)
|
||||
end
|
||||
|
||||
defp number_exp_sign(<<byte, rest::bits>>, original, skip, stack, decode, len)
|
||||
when byte in ~c'0123456789' do
|
||||
number_exp_cont(rest, original, skip, stack, decode, len + 1)
|
||||
end
|
||||
defp number_exp_sign(<<_rest::bits>>, original, skip, _stack, _decode, len) do
|
||||
error(original, skip + len)
|
||||
end
|
||||
|
||||
defp number_exp_cont(<<byte, rest::bits>>, original, skip, stack, decode, len)
|
||||
when byte in ~c'0123456789' do
|
||||
number_exp_cont(rest, original, skip, stack, decode, len + 1)
|
||||
end
|
||||
defp number_exp_cont(<<rest::bits>>, original, skip, stack, decode, len) do
|
||||
token = binary_part(original, skip, len)
|
||||
decode(floats: float_decode) = decode
|
||||
float = float_decode.(token, token, skip)
|
||||
continue(rest, original, skip + len, stack, decode, float)
|
||||
end
|
||||
|
||||
defp number_exp_copy(<<byte, rest::bits>>, original, skip, stack, decode, prefix)
|
||||
when byte in ~c'0123456789' do
|
||||
number_exp_cont(rest, original, skip, stack, decode, prefix, 1)
|
||||
end
|
||||
defp number_exp_copy(<<byte, rest::bits>>, original, skip, stack, decode, prefix)
|
||||
when byte in ~c'+-' do
|
||||
number_exp_sign(rest, original, skip, stack, decode, prefix, 1)
|
||||
end
|
||||
defp number_exp_copy(<<_rest::bits>>, original, skip, _stack, _decode, _prefix) do
|
||||
error(original, skip)
|
||||
end
|
||||
|
||||
defp number_exp_sign(<<byte, rest::bits>>, original, skip, stack, decode, prefix, len)
|
||||
when byte in ~c'0123456789' do
|
||||
number_exp_cont(rest, original, skip, stack, decode, prefix, len + 1)
|
||||
end
|
||||
defp number_exp_sign(<<_rest::bits>>, original, skip, _stack, _decode, _prefix, len) do
|
||||
error(original, skip + len)
|
||||
end
|
||||
|
||||
defp number_exp_cont(<<byte, rest::bits>>, original, skip, stack, decode, prefix, len)
|
||||
when byte in ~c'0123456789' do
|
||||
number_exp_cont(rest, original, skip, stack, decode, prefix, len + 1)
|
||||
end
|
||||
defp number_exp_cont(<<rest::bits>>, original, skip, stack, decode, prefix, len) do
|
||||
suffix = binary_part(original, skip, len)
|
||||
string = prefix <> ".0e" <> suffix
|
||||
prefix_size = byte_size(prefix)
|
||||
initial_skip = skip - prefix_size - 1
|
||||
final_skip = skip + len
|
||||
token = binary_part(original, initial_skip, prefix_size + len + 1)
|
||||
decode(floats: float_decode) = decode
|
||||
float = float_decode.(string, token, initial_skip)
|
||||
continue(rest, original, final_skip, stack, decode, float)
|
||||
end
|
||||
|
||||
defp number_zero(<<?., rest::bits>>, original, skip, stack, decode, len) do
|
||||
number_frac(rest, original, skip, stack, decode, len + 1)
|
||||
end
|
||||
defp number_zero(<<e, rest::bits>>, original, skip, stack, decode, len) when e in ~c'eE' do
|
||||
number_exp_copy(rest, original, skip + len + 1, stack, decode, "0")
|
||||
end
|
||||
defp number_zero(<<rest::bits>>, original, skip, stack, decode, len) do
|
||||
continue(rest, original, skip + len, stack, decode, 0)
|
||||
end
|
||||
|
||||
@compile {:inline, array: 5}
|
||||
|
||||
defp array(rest, original, skip, stack, decode) do
|
||||
value(rest, original, skip, [@array, [] | stack], decode)
|
||||
end
|
||||
|
||||
defp empty_array(<<rest::bits>>, original, skip, stack, decode) do
|
||||
case stack do
|
||||
[@array, [] | stack] ->
|
||||
continue(rest, original, skip, stack, decode, [])
|
||||
_ ->
|
||||
error(original, skip - 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp array(data, original, skip, stack, decode, value) do
|
||||
bytecase data do
|
||||
_ in ~c'\s\n\t\r', rest ->
|
||||
array(rest, original, skip + 1, stack, decode, value)
|
||||
_ in ~c']', rest ->
|
||||
[acc | stack] = stack
|
||||
value = :lists.reverse(acc, [value])
|
||||
continue(rest, original, skip + 1, stack, decode, value)
|
||||
_ in ~c',', rest ->
|
||||
[acc | stack] = stack
|
||||
value(rest, original, skip + 1, [@array, [value | acc] | stack], decode)
|
||||
_, _rest ->
|
||||
error(original, skip)
|
||||
<<_::bits>> ->
|
||||
empty_error(original, skip)
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, object: 5}
|
||||
|
||||
defp object(rest, original, skip, stack, decode) do
|
||||
key(rest, original, skip, [[] | stack], decode)
|
||||
end
|
||||
|
||||
defp object(data, original, skip, stack, decode, value) do
|
||||
bytecase data do
|
||||
_ in ~c'\s\n\t\r', rest ->
|
||||
object(rest, original, skip + 1, stack, decode, value)
|
||||
_ in ~c'}', rest ->
|
||||
skip = skip + 1
|
||||
[key, acc | stack] = stack
|
||||
decode(keys: key_decode) = decode
|
||||
final = [{key_decode.(key), value} | acc]
|
||||
decode(objects: object_decode) = decode
|
||||
continue(rest, original, skip, stack, decode, object_decode.(final))
|
||||
_ in ~c',', rest ->
|
||||
skip = skip + 1
|
||||
[key, acc | stack] = stack
|
||||
decode(keys: key_decode) = decode
|
||||
acc = [{key_decode.(key), value} | acc]
|
||||
key(rest, original, skip, [acc | stack], decode)
|
||||
_, _rest ->
|
||||
error(original, skip)
|
||||
<<_::bits>> ->
|
||||
empty_error(original, skip)
|
||||
end
|
||||
end
|
||||
|
||||
defp key(data, original, skip, stack, decode) do
|
||||
bytecase data do
|
||||
_ in ~c'\s\n\t\r', rest ->
|
||||
key(rest, original, skip + 1, stack, decode)
|
||||
_ in ~c'}', rest ->
|
||||
case stack do
|
||||
[[] | stack] ->
|
||||
decode(objects: object_decode) = decode
|
||||
continue(rest, original, skip + 1, stack, decode, object_decode.([]))
|
||||
_ ->
|
||||
error(original, skip)
|
||||
end
|
||||
_ in ~c'"', rest ->
|
||||
string(rest, original, skip + 1, [@key | stack], decode, 0)
|
||||
_, _rest ->
|
||||
error(original, skip)
|
||||
<<_::bits>> ->
|
||||
empty_error(original, skip)
|
||||
end
|
||||
end
|
||||
|
||||
defp key(data, original, skip, stack, decode, value) do
|
||||
bytecase data do
|
||||
_ in ~c'\s\n\t\r', rest ->
|
||||
key(rest, original, skip + 1, stack, decode, value)
|
||||
_ in ~c':', rest ->
|
||||
value(rest, original, skip + 1, [@object, value | stack], decode)
|
||||
_, _rest ->
|
||||
error(original, skip)
|
||||
<<_::bits>> ->
|
||||
empty_error(original, skip)
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: check if this approach would be faster:
|
||||
# https://git.ninenines.eu/cowlib.git/tree/src/cow_ws.erl#n469
|
||||
# http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
||||
defp string(data, original, skip, stack, decode, len) do
|
||||
bytecase data, 128 do
|
||||
_ in ~c'"', rest ->
|
||||
decode(strings: string_decode) = decode
|
||||
string = string_decode.(binary_part(original, skip, len))
|
||||
continue(rest, original, skip + len + 1, stack, decode, string)
|
||||
_ in ~c'\\', rest ->
|
||||
part = binary_part(original, skip, len)
|
||||
escape(rest, original, skip + len, stack, decode, part)
|
||||
_ in unquote(0x00..0x1F), _rest ->
|
||||
error(original, skip + len)
|
||||
_, rest ->
|
||||
string(rest, original, skip, stack, decode, len + 1)
|
||||
<<char::utf8, rest::bits>> when char <= 0x7FF ->
|
||||
string(rest, original, skip, stack, decode, len + 2)
|
||||
<<char::utf8, rest::bits>> when char <= 0xFFFF ->
|
||||
string(rest, original, skip, stack, decode, len + 3)
|
||||
<<_char::utf8, rest::bits>> ->
|
||||
string(rest, original, skip, stack, decode, len + 4)
|
||||
<<_::bits>> ->
|
||||
empty_error(original, skip + len)
|
||||
end
|
||||
end
|
||||
|
||||
defp string(data, original, skip, stack, decode, acc, len) do
|
||||
bytecase data, 128 do
|
||||
_ in ~c'"', rest ->
|
||||
last = binary_part(original, skip, len)
|
||||
string = IO.iodata_to_binary([acc | last])
|
||||
continue(rest, original, skip + len + 1, stack, decode, string)
|
||||
_ in ~c'\\', rest ->
|
||||
part = binary_part(original, skip, len)
|
||||
escape(rest, original, skip + len, stack, decode, [acc | part])
|
||||
_ in unquote(0x00..0x1F), _rest ->
|
||||
error(original, skip + len)
|
||||
_, rest ->
|
||||
string(rest, original, skip, stack, decode, acc, len + 1)
|
||||
<<char::utf8, rest::bits>> when char <= 0x7FF ->
|
||||
string(rest, original, skip, stack, decode, acc, len + 2)
|
||||
<<char::utf8, rest::bits>> when char <= 0xFFFF ->
|
||||
string(rest, original, skip, stack, decode, acc, len + 3)
|
||||
<<_char::utf8, rest::bits>> ->
|
||||
string(rest, original, skip, stack, decode, acc, len + 4)
|
||||
<<_::bits>> ->
|
||||
empty_error(original, skip + len)
|
||||
end
|
||||
end
|
||||
|
||||
defp escape(data, original, skip, stack, decode, acc) do
|
||||
bytecase data do
|
||||
_ in ~c'b', rest ->
|
||||
string(rest, original, skip + 2, stack, decode, [acc | ~c'\b'], 0)
|
||||
_ in ~c't', rest ->
|
||||
string(rest, original, skip + 2, stack, decode, [acc | ~c'\t'], 0)
|
||||
_ in ~c'n', rest ->
|
||||
string(rest, original, skip + 2, stack, decode, [acc | ~c'\n'], 0)
|
||||
_ in ~c'f', rest ->
|
||||
string(rest, original, skip + 2, stack, decode, [acc | ~c'\f'], 0)
|
||||
_ in ~c'r', rest ->
|
||||
string(rest, original, skip + 2, stack, decode, [acc | ~c'\r'], 0)
|
||||
_ in ~c'"', rest ->
|
||||
string(rest, original, skip + 2, stack, decode, [acc | ~c'\"'], 0)
|
||||
_ in ~c'/', rest ->
|
||||
string(rest, original, skip + 2, stack, decode, [acc | ~c'/'], 0)
|
||||
_ in ~c'\\', rest ->
|
||||
string(rest, original, skip + 2, stack, decode, [acc | ~c'\\'], 0)
|
||||
_ in ~c'u', rest ->
|
||||
escapeu(rest, original, skip, stack, decode, acc)
|
||||
_, _rest ->
|
||||
error(original, skip + 1)
|
||||
<<_::bits>> ->
|
||||
empty_error(original, skip)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Unescape do
|
||||
@moduledoc false
|
||||
|
||||
import Bitwise
|
||||
|
||||
@digits Enum.concat([?0..?9, ?A..?F, ?a..?f])
|
||||
|
||||
def unicode_escapes(chars1 \\ @digits, chars2 \\ @digits) do
|
||||
for char1 <- chars1, char2 <- chars2 do
|
||||
{(char1 <<< 8) + char2, integer8(char1, char2)}
|
||||
end
|
||||
end
|
||||
|
||||
defp integer8(char1, char2) do
|
||||
(integer4(char1) <<< 4) + integer4(char2)
|
||||
end
|
||||
|
||||
defp integer4(char) when char in ?0..?9, do: char - ?0
|
||||
defp integer4(char) when char in ?A..?F, do: char - ?A + 10
|
||||
defp integer4(char) when char in ?a..?f, do: char - ?a + 10
|
||||
|
||||
defp token_error_clause(original, skip, len) do
|
||||
quote do
|
||||
_ ->
|
||||
token_error(unquote_splicing([original, skip, len]))
|
||||
end
|
||||
end
|
||||
|
||||
defmacro escapeu_first(int, last, rest, original, skip, stack, decode, acc) do
|
||||
clauses = escapeu_first_clauses(last, rest, original, skip, stack, decode, acc)
|
||||
quote location: :keep do
|
||||
case unquote(int) do
|
||||
unquote(clauses ++ token_error_clause(original, skip, 6))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp escapeu_first_clauses(last, rest, original, skip, stack, decode, acc) do
|
||||
for {int, first} <- unicode_escapes(),
|
||||
not (first in 0xDC..0xDF) do
|
||||
escapeu_first_clause(int, first, last, rest, original, skip, stack, decode, acc)
|
||||
end
|
||||
end
|
||||
|
||||
defp escapeu_first_clause(int, first, last, rest, original, skip, stack, decode, acc)
|
||||
when first in 0xD8..0xDB do
|
||||
hi =
|
||||
quote bind_quoted: [first: first, last: last] do
|
||||
0x10000 + ((((first &&& 0x03) <<< 8) + last) <<< 10)
|
||||
end
|
||||
args = [rest, original, skip, stack, decode, acc, hi]
|
||||
[clause] =
|
||||
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)
|
||||
acc =
|
||||
quote bind_quoted: [acc: acc, first: first, last: last] do
|
||||
if last <= 0x7F do
|
||||
# 0?????
|
||||
[acc, last]
|
||||
else
|
||||
# 110xxxx?? 10?????
|
||||
byte1 = ((0b110 <<< 5) + (first <<< 2)) + (last >>> 6)
|
||||
byte2 = (0b10 <<< 6) + (last &&& 0b111111)
|
||||
[acc, byte1, byte2]
|
||||
end
|
||||
end
|
||||
args = [rest, original, skip, stack, decode, acc, 0]
|
||||
[clause] =
|
||||
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)
|
||||
acc =
|
||||
quote bind_quoted: [acc: acc, first: first, last: last] do
|
||||
# 110xxx?? 10??????
|
||||
byte1 = ((0b110 <<< 5) + (first <<< 2)) + (last >>> 6)
|
||||
byte2 = (0b10 <<< 6) + (last &&& 0b111111)
|
||||
[acc, byte1, byte2]
|
||||
end
|
||||
args = [rest, original, skip, stack, decode, acc, 0]
|
||||
[clause] =
|
||||
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)
|
||||
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)
|
||||
byte3 = (0b10 <<< 6) + (last &&& 0b111111)
|
||||
[acc, byte1, byte2, byte3]
|
||||
end
|
||||
args = [rest, original, skip, stack, decode, acc, 0]
|
||||
[clause] =
|
||||
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))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp escapeu_last_clauses() do
|
||||
for {int, last} <- unicode_escapes() do
|
||||
[clause] =
|
||||
quote do
|
||||
unquote(int) -> unquote(last)
|
||||
end
|
||||
clause
|
||||
end
|
||||
end
|
||||
|
||||
defmacro escapeu_surrogate(int, last, rest, original, skip, stack, decode, acc,
|
||||
hi) do
|
||||
clauses = escapeu_surrogate_clauses(last, rest, original, skip, stack, decode, acc, hi)
|
||||
quote location: :keep do
|
||||
case unquote(int) do
|
||||
unquote(clauses ++ token_error_clause(original, skip, 12))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp escapeu_surrogate_clauses(last, rest, original, skip, stack, decode, acc, hi) do
|
||||
digits1 = ~c'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
|
||||
end
|
||||
|
||||
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>>]
|
||||
end
|
||||
args = [rest, original, skip, stack, decode, acc, 0]
|
||||
[clause] =
|
||||
quote do
|
||||
unquote(int) ->
|
||||
string(unquote_splicing(args))
|
||||
end
|
||||
clause
|
||||
end
|
||||
end
|
||||
|
||||
defp escapeu(<<int1::16, int2::16, rest::bits>>, original, skip, stack, decode, acc) do
|
||||
require Unescape
|
||||
last = escapeu_last(int2, original, skip)
|
||||
Unescape.escapeu_first(int1, last, rest, original, skip, stack, decode, acc)
|
||||
end
|
||||
defp escapeu(<<_rest::bits>>, original, skip, _stack, _decode, _acc) do
|
||||
empty_error(original, skip)
|
||||
end
|
||||
|
||||
# @compile {:inline, escapeu_last: 3}
|
||||
|
||||
defp escapeu_last(int, original, skip) do
|
||||
require Unescape
|
||||
Unescape.escapeu_last(int, original, skip)
|
||||
end
|
||||
|
||||
defp escape_surrogate(<<?\\, ?u, int1::16, int2::16, rest::bits>>, original,
|
||||
skip, stack, decode, acc, hi) do
|
||||
require Unescape
|
||||
last = escapeu_last(int2, original, skip + 6)
|
||||
Unescape.escapeu_surrogate(int1, last, rest, original, skip, stack, decode, acc, hi)
|
||||
end
|
||||
defp escape_surrogate(<<_rest::bits>>, original, skip, _stack, _decode, _acc, _hi) do
|
||||
error(original, skip + 6)
|
||||
end
|
||||
|
||||
defp error(<<_rest::bits>>, _original, skip, _stack, _decode) do
|
||||
throw {:position, skip - 1}
|
||||
end
|
||||
|
||||
defp empty_error(_original, skip) do
|
||||
throw {:position, skip}
|
||||
end
|
||||
|
||||
@compile {:inline, error: 2, token_error: 2, token_error: 3}
|
||||
defp error(_original, skip) do
|
||||
throw {:position, skip}
|
||||
end
|
||||
|
||||
defp token_error(token, position) do
|
||||
throw {:token, token, position}
|
||||
end
|
||||
|
||||
defp token_error(token, position, len) do
|
||||
throw {:token, binary_part(token, position, len), position}
|
||||
end
|
||||
|
||||
@compile {:inline, continue: 6}
|
||||
defp continue(rest, original, skip, stack, decode, value) do
|
||||
case stack do
|
||||
[@terminate | stack] ->
|
||||
terminate(rest, original, skip, stack, decode, value)
|
||||
[@array | stack] ->
|
||||
array(rest, original, skip, stack, decode, value)
|
||||
[@key | stack] ->
|
||||
key(rest, original, skip, stack, decode, value)
|
||||
[@object | stack] ->
|
||||
object(rest, original, skip, stack, decode, value)
|
||||
end
|
||||
end
|
||||
|
||||
defp terminate(<<byte, rest::bits>>, original, skip, stack, decode, value)
|
||||
when byte in ~c'\s\n\r\t' do
|
||||
terminate(rest, original, skip + 1, stack, decode, value)
|
||||
end
|
||||
defp terminate(<<>>, _original, _skip, _stack, _decode, value) do
|
||||
value
|
||||
end
|
||||
defp terminate(<<_rest::bits>>, original, skip, _stack, _decode, _value) do
|
||||
error(original, skip)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,657 @@
|
||||
defmodule Jason.EncodeError do
|
||||
defexception [:message]
|
||||
|
||||
@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}"}
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Jason.Encode do
|
||||
@moduledoc """
|
||||
Utilities for encoding elixir values to JSON.
|
||||
"""
|
||||
|
||||
import Bitwise
|
||||
|
||||
alias Jason.{Codegen, EncodeError, Encoder, Fragment, OrderedObject}
|
||||
|
||||
@typep escape :: (String.t, String.t, integer -> iodata)
|
||||
@typep encode_map :: (map, escape, encode_map -> iodata)
|
||||
@opaque opts :: {escape, encode_map}
|
||||
|
||||
@dialyzer :no_improper_lists
|
||||
|
||||
# @compile :native
|
||||
|
||||
@doc false
|
||||
@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, e}
|
||||
end
|
||||
end
|
||||
|
||||
defp encode_map_function(%{maps: maps}) do
|
||||
case maps do
|
||||
:naive -> &map_naive/3
|
||||
:strict -> &map_strict/3
|
||||
end
|
||||
end
|
||||
|
||||
defp escape_function(%{escape: escape}) do
|
||||
case escape do
|
||||
:json -> &escape_json/3
|
||||
:html_safe -> &escape_html/3
|
||||
:unicode_safe -> &escape_unicode/3
|
||||
:javascript_safe -> &escape_javascript/3
|
||||
# Keep for compatibility with Poison
|
||||
:javascript -> &escape_javascript/3
|
||||
:unicode -> &escape_unicode/3
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Equivalent to calling the `Jason.Encoder.encode/2` protocol function.
|
||||
|
||||
Slightly more efficient for built-in types because of the internal dispatching.
|
||||
"""
|
||||
@spec value(term, opts) :: iodata
|
||||
def value(value, {escape, encode_map}) do
|
||||
value(value, escape, encode_map)
|
||||
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
|
||||
encode_atom(value, escape)
|
||||
end
|
||||
|
||||
def value(value, escape, _encode_map) when is_binary(value) do
|
||||
encode_string(value, escape)
|
||||
end
|
||||
|
||||
def value(value, _escape, _encode_map) when is_integer(value) do
|
||||
integer(value)
|
||||
end
|
||||
|
||||
def value(value, _escape, _encode_map) when is_float(value) do
|
||||
float(value)
|
||||
end
|
||||
|
||||
def value(value, escape, encode_map) when is_list(value) do
|
||||
list(value, escape, encode_map)
|
||||
end
|
||||
|
||||
def value(%{__struct__: module} = value, escape, encode_map) do
|
||||
struct(value, escape, encode_map, module)
|
||||
end
|
||||
|
||||
def value(value, escape, encode_map) when is_map(value) do
|
||||
case Map.to_list(value) do
|
||||
[] -> "{}"
|
||||
keyword -> encode_map.(keyword, escape, encode_map)
|
||||
end
|
||||
end
|
||||
|
||||
def value(value, escape, encode_map) do
|
||||
Encoder.encode(value, {escape, encode_map})
|
||||
end
|
||||
|
||||
@compile {:inline, integer: 1, float: 1}
|
||||
|
||||
@spec atom(atom, opts) :: iodata
|
||||
def atom(atom, {escape, _encode_map}) do
|
||||
encode_atom(atom, escape)
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
@spec integer(integer) :: iodata
|
||||
def integer(integer) do
|
||||
Integer.to_string(integer)
|
||||
end
|
||||
|
||||
has_short_format = try do
|
||||
:erlang.float_to_binary(1.0, [:short])
|
||||
catch
|
||||
_, _ -> false
|
||||
else
|
||||
_ -> true
|
||||
end
|
||||
|
||||
@spec float(float) :: iodata
|
||||
if has_short_format do
|
||||
def float(float) do
|
||||
:erlang.float_to_binary(float, [:short])
|
||||
end
|
||||
else
|
||||
def float(float) do
|
||||
:io_lib_format.fwrite_g(float)
|
||||
end
|
||||
end
|
||||
|
||||
@spec list(list, opts) :: iodata
|
||||
def list(list, {escape, encode_map}) do
|
||||
list(list, escape, encode_map)
|
||||
end
|
||||
|
||||
defp list([], _escape, _encode_map) do
|
||||
"[]"
|
||||
end
|
||||
|
||||
defp list([head | tail], escape, encode_map) do
|
||||
[?[, value(head, escape, encode_map)
|
||||
| list_loop(tail, escape, encode_map)]
|
||||
end
|
||||
|
||||
defp list_loop([], _escape, _encode_map) do
|
||||
~c']'
|
||||
end
|
||||
|
||||
defp list_loop([head | tail], escape, encode_map) do
|
||||
[?,, 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
|
||||
|
||||
@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)
|
||||
end
|
||||
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)]
|
||||
end
|
||||
|
||||
defp map_naive_loop([], _escape, _encode_map) do
|
||||
~c'}'
|
||||
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)]
|
||||
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)]
|
||||
end
|
||||
|
||||
defp map_strict_loop([], _encode_map, _escape, _visited) do
|
||||
~c'}'
|
||||
end
|
||||
|
||||
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)]
|
||||
end
|
||||
end
|
||||
|
||||
@spec struct(struct, opts) :: iodata
|
||||
def struct(%module{} = value, {escape, encode_map}) do
|
||||
struct(value, escape, encode_map, module)
|
||||
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
|
||||
[?", unquote(module).to_iso8601(value), ?"]
|
||||
end
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Decimal) do
|
||||
defp struct(value, _escape, _encode_map, Decimal) do
|
||||
[?", Decimal.to_string(value, :normal), ?"]
|
||||
end
|
||||
end
|
||||
|
||||
defp struct(value, escape, encode_map, Fragment) do
|
||||
%{encode: encode} = value
|
||||
encode.({escape, encode_map})
|
||||
end
|
||||
|
||||
defp struct(value, escape, encode_map, OrderedObject) do
|
||||
case value do
|
||||
%{values: []} -> "{}"
|
||||
%{values: values} -> encode_map.(values, escape, encode_map)
|
||||
end
|
||||
end
|
||||
|
||||
defp struct(value, escape, encode_map, _module) do
|
||||
Encoder.encode(value, {escape, encode_map})
|
||||
end
|
||||
|
||||
@doc false
|
||||
# This is used in the helpers and deriving implementation
|
||||
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
|
||||
def string(string, {escape, _encode_map}) do
|
||||
encode_string(string, escape)
|
||||
end
|
||||
|
||||
defp encode_string(string, escape) do
|
||||
[?", escape.(string, string, 0), ?"]
|
||||
end
|
||||
|
||||
slash_escapes = Enum.zip(~c'\b\t\n\f\r\"\\', ~c'btnfr"\\')
|
||||
surogate_escapes = Enum.zip([0x2028, 0x2029], ["\\u2028", "\\u2029"])
|
||||
ranges = [{0x00..0x1F, :unicode} | slash_escapes]
|
||||
html_ranges = [{0x00..0x1F, :unicode}, {?<, :unicode}, {?/, ?/} | slash_escapes]
|
||||
escape_jt = Codegen.jump_table(html_ranges, :error)
|
||||
|
||||
Enum.each(escape_jt, fn
|
||||
{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(<<?\\, char>>)
|
||||
{byte, :error} ->
|
||||
defp escape(unquote(byte)), do: throw(:error)
|
||||
end)
|
||||
|
||||
## regular JSON escape
|
||||
|
||||
json_jt = Codegen.jump_table(ranges, :chunk, 0x7F + 1)
|
||||
|
||||
defp escape_json(data, original, skip) do
|
||||
escape_json(data, [], original, skip)
|
||||
end
|
||||
|
||||
Enum.map(json_jt, fn
|
||||
{byte, :chunk} ->
|
||||
defp escape_json(<<byte, rest::bits>>, acc, original, skip)
|
||||
when byte === unquote(byte) do
|
||||
escape_json_chunk(rest, acc, original, skip, 1)
|
||||
end
|
||||
{byte, _escape} ->
|
||||
defp escape_json(<<byte, rest::bits>>, acc, original, skip)
|
||||
when byte === unquote(byte) do
|
||||
acc = [acc | escape(byte)]
|
||||
escape_json(rest, acc, original, skip + 1)
|
||||
end
|
||||
end)
|
||||
defp escape_json(<<char::utf8, rest::bits>>, acc, original, skip)
|
||||
when char <= 0x7FF do
|
||||
escape_json_chunk(rest, acc, original, skip, 2)
|
||||
end
|
||||
defp escape_json(<<char::utf8, rest::bits>>, 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(<<byte, _rest::bits>>, _acc, original, _skip) do
|
||||
error({:invalid_byte, byte, original})
|
||||
end
|
||||
|
||||
Enum.map(json_jt, fn
|
||||
{byte, :chunk} ->
|
||||
defp escape_json_chunk(<<byte, rest::bits>>, acc, original, skip, len)
|
||||
when byte === unquote(byte) do
|
||||
escape_json_chunk(rest, acc, original, skip, len + 1)
|
||||
end
|
||||
{byte, _escape} ->
|
||||
defp escape_json_chunk(<<byte, rest::bits>>, acc, original, skip, len)
|
||||
when byte === unquote(byte) do
|
||||
part = binary_part(original, skip, len)
|
||||
acc = [acc, part | escape(byte)]
|
||||
escape_json(rest, acc, original, skip + len + 1)
|
||||
end
|
||||
end)
|
||||
defp escape_json_chunk(<<char::utf8, rest::bits>>, acc, original, skip, len)
|
||||
when char <= 0x7FF do
|
||||
escape_json_chunk(rest, acc, original, skip, len + 2)
|
||||
end
|
||||
defp escape_json_chunk(<<char::utf8, rest::bits>>, 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(<<byte, _rest::bits>>, _acc, original, _skip, _len) do
|
||||
error({:invalid_byte, byte, original})
|
||||
end
|
||||
|
||||
## javascript safe JSON escape
|
||||
|
||||
defp escape_javascript(data, original, skip) do
|
||||
escape_javascript(data, [], original, skip)
|
||||
end
|
||||
|
||||
Enum.map(json_jt, fn
|
||||
{byte, :chunk} ->
|
||||
defp escape_javascript(<<byte, rest::bits>>, acc, original, skip)
|
||||
when byte === unquote(byte) do
|
||||
escape_javascript_chunk(rest, acc, original, skip, 1)
|
||||
end
|
||||
{byte, _escape} ->
|
||||
defp escape_javascript(<<byte, rest::bits>>, acc, original, skip)
|
||||
when byte === unquote(byte) do
|
||||
acc = [acc | escape(byte)]
|
||||
escape_javascript(rest, acc, original, skip + 1)
|
||||
end
|
||||
end)
|
||||
defp escape_javascript(<<char::utf8, rest::bits>>, acc, original, skip)
|
||||
when char <= 0x7FF do
|
||||
escape_javascript_chunk(rest, acc, original, skip, 2)
|
||||
end
|
||||
Enum.map(surogate_escapes, fn {byte, escape} ->
|
||||
defp escape_javascript(<<unquote(byte)::utf8, rest::bits>>, acc, original, skip) do
|
||||
acc = [acc | unquote(escape)]
|
||||
escape_javascript(rest, acc, original, skip + 3)
|
||||
end
|
||||
end)
|
||||
defp escape_javascript(<<char::utf8, rest::bits>>, acc, original, skip)
|
||||
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(<<byte, _rest::bits>>, _acc, original, _skip) do
|
||||
error({:invalid_byte, byte, original})
|
||||
end
|
||||
|
||||
Enum.map(json_jt, fn
|
||||
{byte, :chunk} ->
|
||||
defp escape_javascript_chunk(<<byte, rest::bits>>, acc, original, skip, len)
|
||||
when byte === unquote(byte) do
|
||||
escape_javascript_chunk(rest, acc, original, skip, len + 1)
|
||||
end
|
||||
{byte, _escape} ->
|
||||
defp escape_javascript_chunk(<<byte, rest::bits>>, acc, original, skip, len)
|
||||
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(<<char::utf8, rest::bits>>, acc, original, skip, len)
|
||||
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(<<unquote(byte)::utf8, rest::bits>>, acc, original, skip, len) do
|
||||
part = binary_part(original, skip, len)
|
||||
acc = [acc, part | unquote(escape)]
|
||||
escape_javascript(rest, acc, original, skip + len + 3)
|
||||
end
|
||||
end)
|
||||
defp escape_javascript_chunk(<<char::utf8, rest::bits>>, acc, original, skip, len)
|
||||
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(<<byte, _rest::bits>>, _acc, original, _skip, _len) do
|
||||
error({:invalid_byte, byte, original})
|
||||
end
|
||||
|
||||
## HTML safe JSON escape
|
||||
|
||||
html_jt = Codegen.jump_table(html_ranges, :chunk, 0x7F + 1)
|
||||
|
||||
defp escape_html(data, original, skip) do
|
||||
escape_html(data, [], original, skip)
|
||||
end
|
||||
|
||||
Enum.map(html_jt, fn
|
||||
{byte, :chunk} ->
|
||||
defp escape_html(<<byte, rest::bits>>, acc, original, skip)
|
||||
when byte === unquote(byte) do
|
||||
escape_html_chunk(rest, acc, original, skip, 1)
|
||||
end
|
||||
{byte, _escape} ->
|
||||
defp escape_html(<<byte, rest::bits>>, acc, original, skip)
|
||||
when byte === unquote(byte) do
|
||||
acc = [acc | escape(byte)]
|
||||
escape_html(rest, acc, original, skip + 1)
|
||||
end
|
||||
end)
|
||||
defp escape_html(<<char::utf8, rest::bits>>, acc, original, skip)
|
||||
when char <= 0x7FF do
|
||||
escape_html_chunk(rest, acc, original, skip, 2)
|
||||
end
|
||||
Enum.map(surogate_escapes, fn {byte, escape} ->
|
||||
defp escape_html(<<unquote(byte)::utf8, rest::bits>>, acc, original, skip) do
|
||||
acc = [acc | unquote(escape)]
|
||||
escape_html(rest, acc, original, skip + 3)
|
||||
end
|
||||
end)
|
||||
defp escape_html(<<char::utf8, rest::bits>>, acc, original, skip)
|
||||
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(<<byte, _rest::bits>>, _acc, original, _skip) do
|
||||
error({:invalid_byte, byte, original})
|
||||
end
|
||||
|
||||
Enum.map(html_jt, fn
|
||||
{byte, :chunk} ->
|
||||
defp escape_html_chunk(<<byte, rest::bits>>, acc, original, skip, len)
|
||||
when byte === unquote(byte) do
|
||||
escape_html_chunk(rest, acc, original, skip, len + 1)
|
||||
end
|
||||
{byte, _escape} ->
|
||||
defp escape_html_chunk(<<byte, rest::bits>>, acc, original, skip, len)
|
||||
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(<<char::utf8, rest::bits>>, acc, original, skip, len)
|
||||
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(<<unquote(byte)::utf8, rest::bits>>, acc, original, skip, len) do
|
||||
part = binary_part(original, skip, len)
|
||||
acc = [acc, part | unquote(escape)]
|
||||
escape_html(rest, acc, original, skip + len + 3)
|
||||
end
|
||||
end)
|
||||
defp escape_html_chunk(<<char::utf8, rest::bits>>, acc, original, skip, len)
|
||||
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(<<byte, _rest::bits>>, _acc, original, _skip, _len) do
|
||||
error({:invalid_byte, byte, original})
|
||||
end
|
||||
|
||||
## unicode escape
|
||||
|
||||
defp escape_unicode(data, original, skip) do
|
||||
escape_unicode(data, [], original, skip)
|
||||
end
|
||||
|
||||
Enum.map(json_jt, fn
|
||||
{byte, :chunk} ->
|
||||
defp escape_unicode(<<byte, rest::bits>>, acc, original, skip)
|
||||
when byte === unquote(byte) do
|
||||
escape_unicode_chunk(rest, acc, original, skip, 1)
|
||||
end
|
||||
{byte, _escape} ->
|
||||
defp escape_unicode(<<byte, rest::bits>>, acc, original, skip)
|
||||
when byte === unquote(byte) do
|
||||
acc = [acc | escape(byte)]
|
||||
escape_unicode(rest, acc, original, skip + 1)
|
||||
end
|
||||
end)
|
||||
defp escape_unicode(<<char::utf8, rest::bits>>, 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(<<char::utf8, rest::bits>>, acc, original, skip)
|
||||
when char <= 0x7FF do
|
||||
acc = [acc, "\\u0" | Integer.to_string(char, 16)]
|
||||
escape_unicode(rest, acc, original, skip + 2)
|
||||
end
|
||||
defp escape_unicode(<<char::utf8, rest::bits>>, acc, original, skip)
|
||||
when char <= 0xFFF do
|
||||
acc = [acc, "\\u0" | Integer.to_string(char, 16)]
|
||||
escape_unicode(rest, acc, original, skip + 3)
|
||||
end
|
||||
defp escape_unicode(<<char::utf8, rest::bits>>, acc, original, skip)
|
||||
when char <= 0xFFFF do
|
||||
acc = [acc, "\\u" | Integer.to_string(char, 16)]
|
||||
escape_unicode(rest, acc, original, skip + 3)
|
||||
end
|
||||
defp escape_unicode(<<char::utf8, rest::bits>>, 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)
|
||||
]
|
||||
escape_unicode(rest, acc, original, skip + 4)
|
||||
end
|
||||
defp escape_unicode(<<>>, acc, _original, _skip) do
|
||||
acc
|
||||
end
|
||||
defp escape_unicode(<<byte, _rest::bits>>, _acc, original, _skip) do
|
||||
error({:invalid_byte, byte, original})
|
||||
end
|
||||
|
||||
Enum.map(json_jt, fn
|
||||
{byte, :chunk} ->
|
||||
defp escape_unicode_chunk(<<byte, rest::bits>>, acc, original, skip, len)
|
||||
when byte === unquote(byte) do
|
||||
escape_unicode_chunk(rest, acc, original, skip, len + 1)
|
||||
end
|
||||
{byte, _escape} ->
|
||||
defp escape_unicode_chunk(<<byte, rest::bits>>, acc, original, skip, len)
|
||||
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(<<char::utf8, rest::bits>>, 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(<<char::utf8, rest::bits>>, acc, original, skip, len)
|
||||
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(<<char::utf8, rest::bits>>, acc, original, skip, len)
|
||||
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(<<char::utf8, rest::bits>>, acc, original, skip, len)
|
||||
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(<<char::utf8, rest::bits>>, 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)
|
||||
]
|
||||
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(<<byte, _rest::bits>>, _acc, original, _skip, _len) do
|
||||
error({:invalid_byte, byte, original})
|
||||
end
|
||||
|
||||
@compile {:inline, error: 1}
|
||||
defp error(error) do
|
||||
throw EncodeError.new(error)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,239 @@
|
||||
defprotocol Jason.Encoder do
|
||||
@moduledoc """
|
||||
Protocol controlling how a value is encoded to JSON.
|
||||
|
||||
## Deriving
|
||||
|
||||
The protocol allows leveraging the Elixir's `@derive` feature
|
||||
to simplify protocol implementation in trivial cases. Accepted
|
||||
options are:
|
||||
|
||||
* `:only` - encodes only values of specified keys.
|
||||
* `:except` - encodes all struct fields except specified keys.
|
||||
|
||||
By default all keys except the `:__struct__` key are encoded.
|
||||
|
||||
## Example
|
||||
|
||||
Let's assume a presence of the following struct:
|
||||
|
||||
defmodule Test do
|
||||
defstruct [:foo, :bar, :baz]
|
||||
end
|
||||
|
||||
If we were to call `@derive Jason.Encoder` just before `defstruct`,
|
||||
an implementation similar to the following implementation would be generated:
|
||||
|
||||
defimpl Jason.Encoder, for: Test do
|
||||
def encode(value, opts) do
|
||||
Jason.Encode.map(Map.take(value, [:foo, :bar, :baz]), opts)
|
||||
end
|
||||
end
|
||||
|
||||
If we called `@derive {Jason.Encoder, only: [:foo]}`, an implementation
|
||||
similar to the following implementation would be generated:
|
||||
|
||||
defimpl Jason.Encoder, for: Test do
|
||||
def encode(value, opts) do
|
||||
Jason.Encode.map(Map.take(value, [:foo]), opts)
|
||||
end
|
||||
end
|
||||
|
||||
If we called `@derive {Jason.Encoder, except: [:foo]}`, an implementation
|
||||
similar to the following implementation would be generated:
|
||||
|
||||
defimpl Jason.Encoder, for: Test do
|
||||
def encode(value, opts) do
|
||||
Jason.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.
|
||||
|
||||
## 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
|
||||
generation - they are highly optimized and verified to always produce
|
||||
valid JSON.
|
||||
"""
|
||||
|
||||
@type t :: term
|
||||
@type opts :: Jason.Encode.opts()
|
||||
|
||||
@fallback_to_any true
|
||||
|
||||
@doc """
|
||||
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.
|
||||
"""
|
||||
@spec encode(t, opts) :: iodata
|
||||
def encode(value, opts)
|
||||
end
|
||||
|
||||
defimpl Jason.Encoder, for: Any do
|
||||
defmacro __deriving__(module, struct, opts) do
|
||||
fields = fields_to_encode(struct, opts)
|
||||
kv = Enum.map(fields, &{&1, generated_var(&1)})
|
||||
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)
|
||||
|
||||
quote do
|
||||
defimpl Jason.Encoder, for: unquote(module) do
|
||||
require Jason.Helpers
|
||||
|
||||
def encode(%{unquote_splicing(kv)}, {unquote(escape), unquote(encode_map)}) do
|
||||
unquote(kv_iodata)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The same as Macro.var/2 except it sets generated: true and handles _ key
|
||||
defp generated_var(:_) do
|
||||
{:__, [generated: true], __MODULE__.Underscore}
|
||||
end
|
||||
defp generated_var(name) do
|
||||
{name, [generated: true], __MODULE__}
|
||||
end
|
||||
|
||||
def encode(%_{} = struct, _opts) do
|
||||
raise Protocol.UndefinedError,
|
||||
protocol: @protocol,
|
||||
value: struct,
|
||||
description: """
|
||||
Jason.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: [....]}
|
||||
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
|
||||
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)
|
||||
"""
|
||||
end
|
||||
|
||||
def encode(value, _opts) do
|
||||
raise Protocol.UndefinedError,
|
||||
protocol: @protocol,
|
||||
value: value,
|
||||
description: "Jason.Encoder protocol must always be explicitly implemented"
|
||||
end
|
||||
|
||||
defp fields_to_encode(struct, opts) do
|
||||
fields = Map.keys(struct)
|
||||
|
||||
cond do
|
||||
only = Keyword.get(opts, :only) ->
|
||||
case only -- fields do
|
||||
[] ->
|
||||
only
|
||||
|
||||
error_keys ->
|
||||
raise ArgumentError,
|
||||
"`:only` specified keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
|
||||
"#{inspect(fields -- [:__struct__])}"
|
||||
|
||||
end
|
||||
|
||||
except = Keyword.get(opts, :except) ->
|
||||
case except -- fields do
|
||||
[] ->
|
||||
fields -- [:__struct__ | except]
|
||||
|
||||
error_keys ->
|
||||
raise ArgumentError,
|
||||
"`:except` specified keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
|
||||
"#{inspect(fields -- [:__struct__])}"
|
||||
|
||||
end
|
||||
|
||||
true ->
|
||||
fields -- [:__struct__]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The following implementations are formality - they are already covered
|
||||
# by the main encoding mechanism in Jason.Encode, but exist mostly for
|
||||
# documentation purposes and if anybody had the idea to call the protocol directly.
|
||||
|
||||
defimpl Jason.Encoder, for: Atom do
|
||||
def encode(atom, opts) do
|
||||
Jason.Encode.atom(atom, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Jason.Encoder, for: Integer do
|
||||
def encode(integer, _opts) do
|
||||
Jason.Encode.integer(integer)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Jason.Encoder, for: Float do
|
||||
def encode(float, _opts) do
|
||||
Jason.Encode.float(float)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Jason.Encoder, for: List do
|
||||
def encode(list, opts) do
|
||||
Jason.Encode.list(list, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Jason.Encoder, for: Map do
|
||||
def encode(map, opts) do
|
||||
Jason.Encode.map(map, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Jason.Encoder, for: BitString do
|
||||
def encode(binary, opts) when is_binary(binary) do
|
||||
Jason.Encode.string(binary, opts)
|
||||
end
|
||||
|
||||
def encode(bitstring, _opts) do
|
||||
raise Protocol.UndefinedError,
|
||||
protocol: @protocol,
|
||||
value: bitstring,
|
||||
description: "cannot encode a bitstring to JSON"
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Jason.Encoder, for: [Date, Time, NaiveDateTime, DateTime] do
|
||||
def encode(value, _opts) do
|
||||
[?", @for.to_iso8601(value), ?"]
|
||||
end
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Decimal) do
|
||||
defimpl Jason.Encoder, for: Decimal do
|
||||
def encode(value, _opts) do
|
||||
[?", Decimal.to_string(value), ?"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Jason.Encoder, for: Jason.Fragment do
|
||||
def encode(%{encode: encode}, opts) do
|
||||
encode.(opts)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,255 @@
|
||||
defmodule Jason.Formatter do
|
||||
@moduledoc ~S"""
|
||||
Pretty-printing and minimizing functions for JSON-encoded data.
|
||||
|
||||
Input is required to be in an 8-bit-wide encoding such as UTF-8 or Latin-1
|
||||
in `t:iodata/0` format. Input must have valid JSON, invalid JSON may produce
|
||||
unexpected results or errors.
|
||||
"""
|
||||
|
||||
@type opts :: [
|
||||
{:indent, iodata}
|
||||
| {:line_separator, iodata}
|
||||
| {:record_separator, iodata}
|
||||
| {:after_colon, iodata}
|
||||
]
|
||||
|
||||
import Record
|
||||
defrecordp :opts, [:indent, :line, :record, :colon]
|
||||
|
||||
@dialyzer :no_improper_lists
|
||||
|
||||
@doc ~S"""
|
||||
Pretty-prints JSON-encoded `input`.
|
||||
|
||||
`input` may contain multiple JSON objects or arrays, optionally separated
|
||||
by whitespace (e.g., one object per line). Objects in output will be
|
||||
separated by newlines. No trailing newline is emitted.
|
||||
|
||||
## Options
|
||||
|
||||
* `:indent` - used for nested objects and arrays (default: two spaces - `" "`);
|
||||
* `:line_separator` - used in nested objects (default: `"\n"`);
|
||||
* `:record_separator` - separates root-level objects and arrays
|
||||
(default is the value for `:line_separator` option);
|
||||
* `:after_colon` - printed after a colon inside objects (default: one space - `" "`).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Jason.Formatter.pretty_print(~s|{"a":{"b": [1, 2]}}|)
|
||||
~s|{
|
||||
"a": {
|
||||
"b": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
}
|
||||
}|
|
||||
|
||||
"""
|
||||
@spec pretty_print(iodata, opts) :: binary
|
||||
def pretty_print(input, opts \\ []) do
|
||||
input
|
||||
|> pretty_print_to_iodata(opts)
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Pretty-prints JSON-encoded `input` and returns iodata.
|
||||
|
||||
This function should be preferred to `pretty_print/2`, if the pretty-printed
|
||||
JSON will be handed over to one of the IO functions or sent
|
||||
over the socket. The Erlang runtime is able to leverage vectorised
|
||||
writes and avoid allocating a continuous buffer for the whole
|
||||
resulting string, lowering memory use and increasing performance.
|
||||
"""
|
||||
@spec pretty_print_to_iodata(iodata, opts) :: iodata
|
||||
def pretty_print_to_iodata(input, opts \\ []) do
|
||||
opts = parse_opts(opts, " ", "\n", nil, " ")
|
||||
|
||||
depth = :first
|
||||
empty = false
|
||||
|
||||
{output, _state} = pp_iodata(input, [], depth, empty, opts)
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Minimizes JSON-encoded `input`.
|
||||
|
||||
`input` may contain multiple JSON objects or arrays, optionally
|
||||
separated by whitespace (e.g., one object per line). Minimized
|
||||
output will contain one object per line. No trailing newline is emitted.
|
||||
|
||||
## Options
|
||||
|
||||
* `:record_separator` - controls the string used as newline (default: `"\n"`).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Jason.Formatter.minimize(~s|{ "a" : "b" , "c": \n\n 2}|)
|
||||
~s|{"a":"b","c":2}|
|
||||
|
||||
"""
|
||||
@spec minimize(iodata, opts) :: binary
|
||||
def minimize(input, opts \\ []) do
|
||||
input
|
||||
|> minimize_to_iodata(opts)
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Minimizes JSON-encoded `input` and returns iodata.
|
||||
|
||||
This function should be preferred to `minimize/2`, if the minimized
|
||||
JSON will be handed over to one of the IO functions or sent
|
||||
over the socket. The Erlang runtime is able to leverage vectorised
|
||||
writes and avoid allocating a continuous buffer for the whole
|
||||
resulting string, lowering memory use and increasing performance.
|
||||
"""
|
||||
@spec minimize_to_iodata(iodata, opts) :: iodata
|
||||
def minimize_to_iodata(input, opts) do
|
||||
record = Keyword.get(opts, :record_separator, "\n")
|
||||
opts = opts(indent: "", line: "", record: record, colon: "")
|
||||
|
||||
depth = :first
|
||||
empty = false
|
||||
|
||||
{output, _state} = pp_iodata(input, [], depth, empty, opts)
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
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)
|
||||
:after_colon -> parse_opts(opts, indent, line, record, value)
|
||||
:line_separator -> parse_opts(opts, indent, value, record || value, colon)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_opts([], indent, line, record, colon) do
|
||||
opts(indent: indent, line: line, record: record || line, colon: colon)
|
||||
end
|
||||
|
||||
for depth <- 1..16 do
|
||||
defp tab(" ", unquote(depth)), do: unquote(String.duplicate(" ", depth))
|
||||
end
|
||||
|
||||
defp tab("", _), do: ""
|
||||
defp tab(indent, depth), do: List.duplicate(indent, depth)
|
||||
|
||||
defp pp_iodata(<<>>, output_acc, depth, empty, opts) do
|
||||
{output_acc, &pp_iodata(&1, &2, depth, empty, opts)}
|
||||
end
|
||||
|
||||
defp pp_iodata(<<byte, rest::binary>>, output_acc, depth, empty, opts) do
|
||||
pp_byte(byte, rest, output_acc, depth, empty, opts)
|
||||
end
|
||||
|
||||
defp pp_iodata([], output_acc, depth, empty, opts) do
|
||||
{output_acc, &pp_iodata(&1, &2, depth, empty, opts)}
|
||||
end
|
||||
|
||||
defp pp_iodata([byte | rest], output_acc, depth, empty, opts) when is_integer(byte) do
|
||||
pp_byte(byte, rest, output_acc, depth, empty, opts)
|
||||
end
|
||||
|
||||
defp pp_iodata([head | tail], output_acc, depth, empty, opts) do
|
||||
{output_acc, cont} = pp_iodata(head, output_acc, depth, empty, opts)
|
||||
cont.(tail, output_acc)
|
||||
end
|
||||
|
||||
defp pp_byte(byte, rest, output, depth, empty, opts) when byte in ~c' \n\r\t' do
|
||||
pp_iodata(rest, output, depth, empty, opts)
|
||||
end
|
||||
|
||||
defp pp_byte(byte, rest, output, depth, empty, opts) when byte in ~c'{[' do
|
||||
{out, depth} =
|
||||
cond do
|
||||
depth == :first -> {byte, 1}
|
||||
depth == 0 -> {[opts(opts, :record), byte], 1}
|
||||
empty -> {[opts(opts, :line), tab(opts(opts, :indent), depth), byte], depth + 1}
|
||||
true -> {byte, depth + 1}
|
||||
end
|
||||
|
||||
empty = true
|
||||
pp_iodata(rest, [output, out], depth, empty, opts)
|
||||
end
|
||||
|
||||
defp pp_byte(byte, rest, output, depth, true = _empty, opts) when byte in ~c'}]' do
|
||||
empty = false
|
||||
depth = depth - 1
|
||||
pp_iodata(rest, [output, byte], depth, empty, opts)
|
||||
end
|
||||
|
||||
defp pp_byte(byte, rest, output, depth, false = empty, opts) when byte in ~c'}]' do
|
||||
depth = depth - 1
|
||||
out = [opts(opts, :line), tab(opts(opts, :indent), depth), byte]
|
||||
pp_iodata(rest, [output, out], depth, empty, opts)
|
||||
end
|
||||
|
||||
defp pp_byte(byte, rest, output, depth, _empty, opts) when byte in ~c',' do
|
||||
empty = false
|
||||
out = [byte, opts(opts, :line), tab(opts(opts, :indent), depth)]
|
||||
pp_iodata(rest, [output, out], depth, empty, opts)
|
||||
end
|
||||
|
||||
defp pp_byte(byte, rest, output, depth, empty, opts) when byte in ~c':' do
|
||||
out = [byte, opts(opts, :colon)]
|
||||
pp_iodata(rest, [output, out], depth, empty, opts)
|
||||
end
|
||||
|
||||
defp pp_byte(byte, rest, output, depth, empty, opts) do
|
||||
out = if empty, do: [opts(opts, :line), tab(opts(opts, :indent), depth), byte], else: byte
|
||||
empty = false
|
||||
|
||||
if byte == ?" do
|
||||
pp_string(rest, [output, out], _in_bs = false, &pp_iodata(&1, &2, depth, empty, opts))
|
||||
else
|
||||
pp_iodata(rest, [output, out], depth, empty, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp pp_string(<<>>, output_acc, in_bs, cont) do
|
||||
{output_acc, &pp_string(&1, &2, in_bs, cont)}
|
||||
end
|
||||
|
||||
defp pp_string(binary, output_acc, true = _in_bs, cont) when is_binary(binary) do
|
||||
<<byte, rest::binary>> = binary
|
||||
pp_string(rest, [output_acc, byte], false, cont)
|
||||
end
|
||||
|
||||
defp pp_string(binary, output_acc, false = _in_bs, cont) when is_binary(binary) 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])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp pp_string([], output_acc, in_bs, cont) do
|
||||
{output_acc, &pp_string(&1, &2, in_bs, cont)}
|
||||
end
|
||||
|
||||
defp pp_string([byte | rest], output_acc, in_bs, cont) when is_integer(byte) do
|
||||
cond do
|
||||
in_bs -> pp_string(rest, [output_acc, byte], false, cont)
|
||||
byte == ?" -> cont.(rest, [output_acc, byte])
|
||||
true -> pp_string(rest, [output_acc, byte], byte == ?\\, cont)
|
||||
end
|
||||
end
|
||||
|
||||
defp pp_string([head | tail], output_acc, in_bs, cont) do
|
||||
{output_acc, cont} = pp_string(head, output_acc, in_bs, cont)
|
||||
cont.(tail, output_acc)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
defmodule Jason.Fragment do
|
||||
@moduledoc ~S"""
|
||||
Provides a way to inject an already-encoded JSON structure into a
|
||||
to-be-encoded structure in optimized fashion.
|
||||
|
||||
This avoids a decoding/encoding round-trip for the subpart.
|
||||
|
||||
This feature can be used for caching parts of the JSON, or delegating
|
||||
the generation of the JSON to a third-party system (e.g. Postgres).
|
||||
"""
|
||||
|
||||
defstruct [:encode]
|
||||
|
||||
def new(iodata) when is_list(iodata) or is_binary(iodata) do
|
||||
%__MODULE__{encode: fn _ -> iodata end}
|
||||
end
|
||||
|
||||
def new(encode) when is_function(encode, 1) do
|
||||
%__MODULE__{encode: encode}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,98 @@
|
||||
defmodule Jason.Helpers do
|
||||
@moduledoc """
|
||||
Provides macro facilities for partial compile-time encoding of JSON.
|
||||
"""
|
||||
|
||||
alias Jason.{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
|
||||
that needs to be passed to one of the "main" encoding functions -
|
||||
for example `Jason.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
|
||||
encoding modes. This means only ASCII characters from the range
|
||||
0x1F..0x7F excluding '\', '/' and '"' are allowed - this also excludes
|
||||
all control characters like newlines.
|
||||
|
||||
Preserves the order of the keys.
|
||||
|
||||
## Example
|
||||
|
||||
iex> fragment = json_map(foo: 1, bar: 2)
|
||||
iex> Jason.encode!(fragment)
|
||||
"{\"foo\":1,\"bar\":2}"
|
||||
|
||||
"""
|
||||
defmacro json_map(kv) do
|
||||
kv_values = Macro.expand(kv, __CALLER__)
|
||||
kv_vars = Enum.map(kv_values, fn {key, _} -> {key, generated_var(key, Codegen)} end)
|
||||
|
||||
values = Enum.map(kv_values, &elem(&1, 1))
|
||||
vars = Enum.map(kv_vars, &elem(&1, 1))
|
||||
|
||||
escape = quote(do: escape)
|
||||
encode_map = quote(do: encode_map)
|
||||
encode_args = [escape, encode_map]
|
||||
kv_iodata = Codegen.build_kv_iodata(kv_vars, encode_args)
|
||||
|
||||
quote do
|
||||
{unquote_splicing(vars)} = {unquote_splicing(values)}
|
||||
|
||||
%Fragment{
|
||||
encode: fn {unquote(escape), unquote(encode_map)} ->
|
||||
unquote(kv_iodata)
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Encodes a JSON map from a variable containing a map and a compile-time
|
||||
list of keys.
|
||||
|
||||
It is equivalent to calling `Map.take/2` before encoding. Otherwise works
|
||||
similar to `json_map/2`.
|
||||
|
||||
## Example
|
||||
|
||||
iex> map = %{a: 1, b: 2, c: 3}
|
||||
iex> fragment = json_map_take(map, [:c, :b])
|
||||
iex> Jason.encode!(fragment)
|
||||
"{\"c\":3,\"b\":2}"
|
||||
|
||||
"""
|
||||
defmacro json_map_take(map, take) do
|
||||
take = Macro.expand(take, __CALLER__)
|
||||
kv = Enum.map(take, &{&1, generated_var(&1, Codegen)})
|
||||
escape = quote(do: escape)
|
||||
encode_map = quote(do: encode_map)
|
||||
encode_args = [escape, encode_map]
|
||||
kv_iodata = Codegen.build_kv_iodata(kv, encode_args)
|
||||
|
||||
quote do
|
||||
case unquote(map) do
|
||||
%{unquote_splicing(kv)} ->
|
||||
%Fragment{
|
||||
encode: fn {unquote(escape), unquote(encode_map)} ->
|
||||
unquote(kv_iodata)
|
||||
end
|
||||
}
|
||||
|
||||
other ->
|
||||
raise ArgumentError,
|
||||
"expected a map with keys: #{unquote(inspect(take))}, got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The same as Macro.var/2 except it sets generated: true
|
||||
defp generated_var(name, context) do
|
||||
{name, [generated: true], context}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,242 @@
|
||||
defmodule Jason do
|
||||
@moduledoc """
|
||||
A blazing fast JSON parser and generator in pure Elixir.
|
||||
"""
|
||||
|
||||
alias Jason.{Encode, Decoder, DecodeError, EncodeError, Formatter}
|
||||
|
||||
@type escape :: :json | :unicode_safe | :html_safe | :javascript_safe
|
||||
@type maps :: :naive | :strict
|
||||
|
||||
@type encode_opt :: {:escape, escape} | {:maps, maps} | {:pretty, boolean | Formatter.opts()}
|
||||
|
||||
@type keys :: :atoms | :atoms! | :strings | :copy | (String.t() -> term)
|
||||
|
||||
@type strings :: :reference | :copy
|
||||
|
||||
@type floats :: :native | :decimals
|
||||
|
||||
@type objects :: :maps | :ordered_objects
|
||||
|
||||
@type decode_opt :: {:keys, keys} | {:strings, strings} | {:floats, floats} | {:objects, objects}
|
||||
|
||||
@doc """
|
||||
Parses a JSON value from `input` iodata.
|
||||
|
||||
## Options
|
||||
|
||||
* `:keys` - controls how keys in objects are decoded. Possible values are:
|
||||
|
||||
* `:strings` (default) - decodes keys as binary strings,
|
||||
* `:atoms` - keys are converted to atoms using `String.to_atom/1`,
|
||||
* `:atoms!` - keys are converted to atoms using `String.to_existing_atom/1`,
|
||||
* custom decoder - additionally a function accepting a string and returning a key
|
||||
is accepted.
|
||||
|
||||
* `:strings` - controls how strings (including keys) are decoded. Possible values are:
|
||||
|
||||
* `:reference` (default) - when possible tries to create a sub-binary into the original
|
||||
* `:copy` - always copies the strings. This option is especially useful when parts of the
|
||||
decoded data will be stored for a long time (in ets or some process) to avoid keeping
|
||||
the reference to the original data.
|
||||
|
||||
* `:floats` - controls how floats are decoded. Possible values are:
|
||||
|
||||
* `:native` (default) - Native conversion from binary to float using `:erlang.binary_to_float/1`,
|
||||
* `:decimals` - uses `Decimal.new/1` to parse the binary into a Decimal struct with arbitrary precision.
|
||||
|
||||
* `: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
|
||||
|
||||
## Decoding keys to atoms
|
||||
|
||||
The `:atoms` option uses the `String.to_atom/1` call that can create atoms at runtime.
|
||||
Since the atoms are not garbage collected, this can pose a DoS attack vector when used
|
||||
on user-controlled data.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Jason.decode("{}")
|
||||
{:ok, %{}}
|
||||
|
||||
iex> Jason.decode("invalid")
|
||||
{:error, %Jason.DecodeError{data: "invalid", position: 0, token: nil}}
|
||||
"""
|
||||
@spec decode(iodata, [decode_opt]) :: {:ok, term} | {:error, DecodeError.t()}
|
||||
def decode(input, opts \\ []) do
|
||||
input = IO.iodata_to_binary(input)
|
||||
Decoder.parse(input, format_decode_opts(opts))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses a JSON value from `input` iodata.
|
||||
|
||||
Similar to `decode/2` except it will unwrap the error tuple and raise
|
||||
in case of errors.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Jason.decode!("{}")
|
||||
%{}
|
||||
|
||||
iex> Jason.decode!("invalid")
|
||||
** (Jason.DecodeError) unexpected byte at position 0: 0x69 ("i")
|
||||
|
||||
"""
|
||||
@spec decode!(iodata, [decode_opt]) :: term | no_return
|
||||
def decode!(input, opts \\ []) do
|
||||
case decode(input, opts) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates JSON corresponding to `input`.
|
||||
|
||||
The generation is controlled by the `Jason.Encoder` protocol,
|
||||
please refer to the module to read more on how to define the protocol
|
||||
for custom data types.
|
||||
|
||||
## Options
|
||||
|
||||
* `:escape` - controls how strings are encoded. Possible values are:
|
||||
|
||||
* `:json` (default) - the regular JSON escaping as defined by RFC 7159.
|
||||
* `:javascript_safe` - additionally escapes the LINE SEPARATOR (U+2028)
|
||||
and PARAGRAPH SEPARATOR (U+2029) characters to make the produced JSON
|
||||
valid JavaScript.
|
||||
* `:html_safe` - similar to `:javascript_safe`, but also escapes the `/`
|
||||
character to prevent XSS.
|
||||
* `:unicode_safe` - escapes all non-ascii characters.
|
||||
|
||||
* `:maps` - controls how maps are encoded. Possible values are:
|
||||
|
||||
* `:strict` - checks the encoded map for duplicate keys and raises
|
||||
if they appear. For example `%{:foo => 1, "foo" => 2}` would be
|
||||
rejected, since both keys would be encoded to the string `"foo"`.
|
||||
* `:naive` (default) - does not perform the check.
|
||||
|
||||
* `: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`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Jason.encode(%{a: 1})
|
||||
{:ok, ~S|{"a":1}|}
|
||||
|
||||
iex> Jason.encode("\\xFF")
|
||||
{:error, %Jason.EncodeError{message: "invalid byte 0xFF in <<255>>"}}
|
||||
|
||||
"""
|
||||
@spec encode(term, [encode_opt]) ::
|
||||
{:ok, String.t()} | {:error, EncodeError.t() | Exception.t()}
|
||||
def encode(input, opts \\ []) do
|
||||
case do_encode(input, format_encode_opts(opts)) do
|
||||
{:ok, result} -> {:ok, IO.iodata_to_binary(result)}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates JSON corresponding to `input`.
|
||||
|
||||
Similar to `encode/1` except it will unwrap the error tuple and raise
|
||||
in case of errors.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Jason.encode!(%{a: 1})
|
||||
~S|{"a":1}|
|
||||
|
||||
iex> Jason.encode!("\\xFF")
|
||||
** (Jason.EncodeError) invalid byte 0xFF in <<255>>
|
||||
|
||||
"""
|
||||
@spec encode!(term, [encode_opt]) :: String.t() | no_return
|
||||
def encode!(input, opts \\ []) do
|
||||
case do_encode(input, format_encode_opts(opts)) do
|
||||
{:ok, result} -> IO.iodata_to_binary(result)
|
||||
{:error, error} -> raise error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates JSON corresponding to `input` and returns iodata.
|
||||
|
||||
This function should be preferred to `encode/2`, if the generated
|
||||
JSON will be handed over to one of the IO functions or sent
|
||||
over the socket. The Erlang runtime is able to leverage vectorised
|
||||
writes and avoid allocating a continuous buffer for the whole
|
||||
resulting string, lowering memory use and increasing performance.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, iodata} = Jason.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>>"}}
|
||||
|
||||
"""
|
||||
@spec encode_to_iodata(term, [encode_opt]) ::
|
||||
{:ok, iodata} | {:error, EncodeError.t() | Exception.t()}
|
||||
def encode_to_iodata(input, opts \\ []) do
|
||||
do_encode(input, format_encode_opts(opts))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates JSON corresponding to `input` and returns iodata.
|
||||
|
||||
Similar to `encode_to_iodata/1` except it will unwrap the error tuple
|
||||
and raise in case of errors.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> iodata = Jason.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>>
|
||||
|
||||
"""
|
||||
@spec encode_to_iodata!(term, [encode_opt]) :: iodata | no_return
|
||||
def encode_to_iodata!(input, opts \\ []) do
|
||||
case do_encode(input, format_encode_opts(opts)) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise error
|
||||
end
|
||||
end
|
||||
|
||||
defp do_encode(input, %{pretty: true} = opts) do
|
||||
case Encode.encode(input, opts) do
|
||||
{:ok, encoded} -> {:ok, Formatter.pretty_print_to_iodata(encoded)}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp do_encode(input, %{pretty: pretty} = opts) when pretty !== false do
|
||||
case Encode.encode(input, opts) do
|
||||
{:ok, encoded} -> {:ok, Formatter.pretty_print_to_iodata(encoded, pretty)}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp do_encode(input, opts) do
|
||||
Encode.encode(input, opts)
|
||||
end
|
||||
|
||||
defp format_encode_opts(opts) do
|
||||
Enum.into(opts, %{escape: :json, maps: :naive})
|
||||
end
|
||||
|
||||
defp format_decode_opts(opts) do
|
||||
Enum.into(opts, %{keys: :strings, strings: :reference, floats: :native, objects: :maps})
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,94 @@
|
||||
defmodule Jason.OrderedObject do
|
||||
@doc """
|
||||
Struct implementing a JSON object retaining order of properties.
|
||||
|
||||
A wrapper around a keyword (that supports non-atom keys) allowing for
|
||||
proper protocol implementations.
|
||||
|
||||
Implements the `Access` behaviour and `Enumerable` protocol with
|
||||
complexity similar to keywords/lists.
|
||||
"""
|
||||
|
||||
@behaviour Access
|
||||
|
||||
@type t :: %__MODULE__{values: [{String.Chars.t(), term()}]}
|
||||
|
||||
defstruct values: []
|
||||
|
||||
def new(values) when is_list(values) do
|
||||
%__MODULE__{values: values}
|
||||
end
|
||||
|
||||
@impl Access
|
||||
def fetch(%__MODULE__{values: values}, key) do
|
||||
case :lists.keyfind(key, 1, values) do
|
||||
{_, value} -> {:ok, value}
|
||||
false -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@impl Access
|
||||
def get_and_update(%__MODULE__{values: values} = obj, key, function) do
|
||||
{result, new_values} = get_and_update(values, [], key, function)
|
||||
{result, %{obj | values: new_values}}
|
||||
end
|
||||
|
||||
@impl Access
|
||||
def pop(%__MODULE__{values: values} = obj, key, default \\ nil) do
|
||||
case :lists.keyfind(key, 1, values) do
|
||||
{_, value} -> {value, %{obj | values: delete_key(values, key)}}
|
||||
false -> {default, obj}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update([{key, current} | t], acc, key, fun) do
|
||||
case fun.(current) do
|
||||
{get, value} ->
|
||||
{get, :lists.reverse(acc, [{key, value} | t])}
|
||||
|
||||
:pop ->
|
||||
{current, :lists.reverse(acc, t)}
|
||||
|
||||
other ->
|
||||
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update([{_, _} = h | t], acc, key, fun), do: get_and_update(t, [h | acc], key, fun)
|
||||
|
||||
defp get_and_update([], acc, key, fun) do
|
||||
case fun.(nil) do
|
||||
{get, update} ->
|
||||
{get, [{key, update} | :lists.reverse(acc)]}
|
||||
|
||||
:pop ->
|
||||
{nil, :lists.reverse(acc)}
|
||||
|
||||
other ->
|
||||
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_key([{key, _} | tail], key), do: delete_key(tail, key)
|
||||
defp delete_key([{_, _} = pair | tail], key), do: [pair | delete_key(tail, key)]
|
||||
defp delete_key([], _key), do: []
|
||||
end
|
||||
|
||||
defimpl Enumerable, for: Jason.OrderedObject do
|
||||
def count(%{values: []}), do: {:ok, 0}
|
||||
def count(_obj), do: {:error, __MODULE__}
|
||||
|
||||
def member?(%{values: []}, _value), do: {:ok, false}
|
||||
def member?(_obj, _value), do: {:error, __MODULE__}
|
||||
|
||||
def slice(%{values: []}), do: {:ok, 0, fn _, _ -> [] end}
|
||||
def slice(_obj), do: {:error, __MODULE__}
|
||||
|
||||
def reduce(%{values: values}, acc, fun), do: Enumerable.List.reduce(values, acc, fun)
|
||||
end
|
||||
|
||||
defimpl Jason.Encoder, for: Jason.OrderedObject do
|
||||
def encode(%{values: values}, opts) do
|
||||
Jason.Encode.keyword(values, opts)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,84 @@
|
||||
defmodule Jason.Sigil do
|
||||
@doc ~S"""
|
||||
Handles the sigil `~j` for JSON strings.
|
||||
|
||||
Calls `Jason.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.
|
||||
|
||||
* `a` - equivalent to `{:keys, :atoms}` option
|
||||
* `A` - equivalent to `{:keys, :atoms!}` option
|
||||
* `r` - equivalent to `{:strings, :reference}` option
|
||||
* `c` - equivalent to `{:strings, :copy}` option
|
||||
|
||||
## Examples
|
||||
|
||||
iex> ~j"0"
|
||||
0
|
||||
|
||||
iex> ~j"[1, 2, 3]"
|
||||
[1, 2, 3]
|
||||
|
||||
iex> ~j'"string"'r
|
||||
"string"
|
||||
|
||||
iex> ~j"{}"
|
||||
%{}
|
||||
|
||||
iex> ~j'{"atom": "value"}'a
|
||||
%{atom: "value"}
|
||||
|
||||
iex> ~j'{"#{:j}": #{~c'"j"'}}'A
|
||||
%{j: "j"}
|
||||
|
||||
"""
|
||||
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)))
|
||||
end
|
||||
|
||||
defmacro sigil_j(term, modifiers) do
|
||||
quote(do: Jason.decode!(unquote(term), unquote(mods_to_opts(modifiers))))
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Handles the sigil `~J` for raw JSON strings.
|
||||
|
||||
Decodes a raw string ignoring Elixir interpolations and
|
||||
escape characters at compile-time.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> ~J'"#{string}"'
|
||||
"\#{string}"
|
||||
|
||||
iex> ~J'"\u0078\\y"'
|
||||
"x\\y"
|
||||
|
||||
iex> ~J'{"#{key}": "#{}"}'a
|
||||
%{"\#{key}": "\#{}"}
|
||||
"""
|
||||
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)))
|
||||
end
|
||||
|
||||
@spec mods_to_opts(charlist) :: [Jason.decode_opt()]
|
||||
defp mods_to_opts(modifiers) do
|
||||
modifiers
|
||||
|> Enum.map(fn
|
||||
?a -> {:keys, :atoms}
|
||||
?A -> {:keys, :atoms!}
|
||||
?r -> {:strings, :reference}
|
||||
?c -> {:strings, :copy}
|
||||
m -> raise ArgumentError, "unknown sigil modifier #{<<?", m, ?">>}"
|
||||
end)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user