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:
2025-08-02 19:54:17 -07:00
co-authored by Claude
parent 46e585ec92
commit b6769abbe9
197 changed files with 9555 additions and 7 deletions
+139
View File
@@ -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
+708
View File
@@ -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
+657
View File
@@ -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
+239
View File
@@ -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
+255
View File
@@ -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
+21
View File
@@ -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
+98
View File
@@ -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
+242
View File
@@ -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
+94
View File
@@ -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
+84
View File
@@ -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