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:
Binary file not shown.
@@ -0,0 +1,132 @@
|
||||
# Changelog
|
||||
|
||||
## 1.4.4 (26.07.2024)
|
||||
|
||||
* Fix warnings on Elixir 1.17 by conditionally compiling Decimal support
|
||||
|
||||
## 1.4.3 (29.06.2024)
|
||||
|
||||
* Fix derive with _ struct key
|
||||
|
||||
## 1.4.2 (29.06.2024)
|
||||
|
||||
* Fix compiler warnings for Elixir 1.17
|
||||
|
||||
## 1.4.1 (06.07.2023)
|
||||
|
||||
* Add limit to decoded integer sizes of 1024 digits. This can be changed
|
||||
with the `decoding_integer_digit_limit` app env config.
|
||||
|
||||
## 1.4.0 (12.09.2022)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Use the `:erlang.float_to_binary(_, [:short])` function, instead of `io_lib_format.fwrite_g/1`
|
||||
where available (OTP 24.1+). This provides equivalent output with much less memory used
|
||||
and significantly improved performance.
|
||||
|
||||
## 1.3.0 (21.12.2021)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add the `Jason.OrderedObject` struct
|
||||
* Support decoding objects preserving all the keys with `objects: :ordered_objects` option
|
||||
* Support decoding floats to `Decimal` with `floats: :decimals` option
|
||||
* Add `~j` and `~J` sigils in module `Jason.Sigil` to support writing JSON literals in code
|
||||
|
||||
### Fixes
|
||||
* Fix error reporting when decoding strings (it was possible to mis-attribute the offending byte)
|
||||
* Verify fields given to `@derive`
|
||||
|
||||
## 1.2.2 (08.09.2020)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Support Decimal 2.0
|
||||
|
||||
## 1.2.1 (04.05.2020)
|
||||
|
||||
### Security
|
||||
|
||||
* Fix `html_safe` escaping in `Jason.encode`
|
||||
|
||||
The `<!--` sequence of characters would not be escaped in `Jason.encode`
|
||||
with`html_escape` mode, which could lead to DoS attacks when used for
|
||||
embedding of arbitrary, user controlled strings into HTML through JSON
|
||||
(e.g. inside of `<script>` tags).
|
||||
|
||||
If you were not using the `html_safe` option, you are not affected.
|
||||
|
||||
Affected versions: < 1.2.1
|
||||
Patched versions: >= 1.2.1
|
||||
|
||||
## 1.2.0 (17.03.2020)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Add `Jason.Encode.keyword/2`
|
||||
([cb1f26a](https://github.com/michalmuskala/jason/commit/cb1f26a)).
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix `Jason.Helpers.json_map/1` value expansion
|
||||
([70b046a](https://github.com/michalmuskala/jason/commit/70b046a)).
|
||||
|
||||
## 1.1.2 (19.10.2018)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* correctly handle the `pretty: false` option
|
||||
([ba318c8](https://github.com/michalmuskala/jason/commit/ba318c8)).
|
||||
|
||||
## 1.1.1 (10.07.2018)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* correctly handle escape sequences in strings when pretty printing
|
||||
([794bbe4](https://github.com/michalmuskala/jason/commit/794bbe4)).
|
||||
|
||||
## 1.1.0 (02.07.2018)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* pretty-printing support through `Jason.Formatter` and `pretty: true` option
|
||||
in `Jason.encode/2` ([d758e36](https://github.com/michalmuskala/jason/commit/d758e36)).
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* silence variable warnings for fields with underscores used during deriving
|
||||
([88dd85c](https://github.com/michalmuskala/jason/commit/88dd85c)).
|
||||
* **potential incompatibility** don't raise `Protocol.UndefinedError` in non-bang functions
|
||||
([ad0f57b](https://github.com/michalmuskala/jason/commit/ad0f57b)).
|
||||
|
||||
## 1.0.1 (02.07.2018)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* fix `Jason.Encode.escape` type ([a57b430](https://github.com/michalmuskala/jason/commit/a57b430))
|
||||
* multiple documentation improvements
|
||||
|
||||
## 1.0.0 (26.01.2018)
|
||||
|
||||
No changes
|
||||
|
||||
## 1.0.0-rc.3 (26.01.2018)
|
||||
|
||||
### Changes
|
||||
|
||||
* update `escape` option of `Jason.encode/2` to take values:
|
||||
`:json | :unicode_safe | :html_safe | :javascript_safe` for consistency. Old values of
|
||||
`:unicode` and `:javascript` are still supported for compatibility with Poison.
|
||||
([f42dcbd](https://github.com/michalmuskala/jason/commit/f42dcbd))
|
||||
|
||||
## 1.0.0-rc.2 (07.01.2018)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* add type for `strings` option ([b459ee4](https://github.com/michalmuskala/jason/commit/b459ee4))
|
||||
* support iodata in `decode!` ([a1f3456](https://github.com/michalmuskala/jason/commit/a1f3456))
|
||||
|
||||
## 1.0.0-rc.1 (22.12.2017)
|
||||
|
||||
* Initial release
|
||||
@@ -0,0 +1,13 @@
|
||||
Copyright (c) 2017-present Michał Muskała
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,157 @@
|
||||
# Jason
|
||||
|
||||
A blazing fast JSON parser and generator in pure Elixir.
|
||||
|
||||
The parser and generator are at least twice as fast as other Elixir/Erlang libraries
|
||||
(most notably `Poison`).
|
||||
The performance is comparable to `jiffy`, which is implemented in C as a NIF.
|
||||
Jason is usually only twice as slow.
|
||||
|
||||
Both parser and generator fully conform to
|
||||
[RFC 8259](https://tools.ietf.org/html/rfc8259) and
|
||||
[ECMA 404](http://www.ecma-international.org/publications/standards/Ecma-404.htm)
|
||||
standards. The parser is tested using [JSONTestSuite](https://github.com/nst/JSONTestSuite).
|
||||
|
||||
## Installation
|
||||
|
||||
The package can be installed by adding `jason` to your list of dependencies
|
||||
in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[{:jason, "~> 1.4"}]
|
||||
end
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
``` elixir
|
||||
iex(1)> Jason.encode!(%{"age" => 44, "name" => "Steve Irwin", "nationality" => "Australian"})
|
||||
"{\"age\":44,\"name\":\"Steve Irwin\",\"nationality\":\"Australian\"}"
|
||||
|
||||
iex(2)> Jason.decode!(~s({"age":44,"name":"Steve Irwin","nationality":"Australian"}))
|
||||
%{"age" => 44, "name" => "Steve Irwin", "nationality" => "Australian"}
|
||||
```
|
||||
|
||||
Full documentation can be found at [https://hexdocs.pm/jason](https://hexdocs.pm/jason).
|
||||
|
||||
## Use with other libraries
|
||||
|
||||
### Postgrex
|
||||
|
||||
Versions starting at 0.14.0 use `Jason` by default. For earlier versions, please refer to
|
||||
[previous versions of this document](https://github.com/michalmuskala/jason/tree/v1.1.2#postgrex).
|
||||
|
||||
### Ecto
|
||||
|
||||
Versions starting at 3.0.0 use `Jason` by default. For earlier versions, please refer to
|
||||
[previous versions of this document](https://github.com/michalmuskala/jason/tree/v1.1.2#ecto).
|
||||
|
||||
### Plug (and Phoenix)
|
||||
|
||||
Phoenix starting at 1.4.0 uses `Jason` by default. For earlier versions, please refer to
|
||||
[previous versions of this document](https://github.com/michalmuskala/jason/tree/v1.1.2#plug-and-phoenix).
|
||||
|
||||
### Absinthe
|
||||
|
||||
You need to pass the `:json_codec` option to `Absinthe.Plug`
|
||||
|
||||
```elixir
|
||||
# When called directly:
|
||||
plug Absinthe.Plug,
|
||||
schema: MyApp.Schema,
|
||||
json_codec: Jason
|
||||
|
||||
# When used in phoenix router:
|
||||
forward "/api",
|
||||
to: Absinthe.Plug,
|
||||
init_opts: [schema: MyApp.Schema, json_codec: Jason]
|
||||
```
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Detailed benchmarks (including memory measurements):
|
||||
https://gist.github.com/michalmuskala/4d64a5a7696ca84ac7c169a0206640d5
|
||||
|
||||
HTML reports for the benchmark (only performance measurements):
|
||||
http://michal.muskala.eu/jason/decode.html and http://michal.muskala.eu/jason/encode.html
|
||||
|
||||
### Running
|
||||
|
||||
Benchmarks against most popular Elixir & Erlang json libraries can be executed after
|
||||
going into the `bench/` folder and then executing `mix bench.encode` and `mix bench.decode`.
|
||||
A HTML report of the benchmarks (after their execution) can be found in
|
||||
`bench/output/encode.html` and `bench/output/decode.html` respectively.
|
||||
|
||||
## Differences to Poison
|
||||
|
||||
Jason has a couple feature differences compared to Poison.
|
||||
|
||||
* Jason follows the JSON spec more strictly, for example it does not allow
|
||||
unescaped newline characters in JSON strings - e.g. `"\"\n\""` will
|
||||
produce a decoding error.
|
||||
* no support for decoding into data structures (the `as:` option).
|
||||
* no built-in encoders for `MapSet`, `Range` and `Stream`.
|
||||
* no support for encoding arbitrary structs - explicit implementation
|
||||
of the `Jason.Encoder` protocol is always required.
|
||||
* different pretty-printing customisation options (default `pretty: true` works the same)
|
||||
|
||||
### Encoders
|
||||
|
||||
If you require encoders for any of the unsupported collection types, I suggest
|
||||
adding the needed implementations directly to your project:
|
||||
|
||||
```elixir
|
||||
defimpl Jason.Encoder, for: [MapSet, Range, Stream] do
|
||||
def encode(struct, opts) do
|
||||
Jason.Encode.list(Enum.to_list(struct), opts)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
If you need to encode some struct that does not implement the protocol,
|
||||
if you own the struct, you can derive the implementation specifying
|
||||
which fields should be encoded to JSON:
|
||||
|
||||
```elixir
|
||||
@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:
|
||||
|
||||
```elixir
|
||||
@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:
|
||||
|
||||
```elixir
|
||||
Protocol.derive(Jason.Encoder, NameOfTheStruct, only: [...])
|
||||
Protocol.derive(Jason.Encoder, NameOfTheStruct)
|
||||
```
|
||||
|
||||
## Injecting an already encoded JSON inside a to-be-encoded structure
|
||||
|
||||
If parts of the to-be-encoded structure are already JSON-encoded, you can
|
||||
use `Jason.Fragment` to mark the parts as already encoded, and avoid a
|
||||
decoding/encoding roundtrip.
|
||||
|
||||
```elixir
|
||||
already_encoded_json = Jason.encode!(%{hello: "world"})
|
||||
Jason.encode!(%{foo: Jason.Fragment.new(already_encoded_json)})
|
||||
```
|
||||
|
||||
This feature is especially useful if you need to cache a part of the JSON,
|
||||
or if it is already provided by another system (e.g. `jsonb_agg` with Postgres).
|
||||
|
||||
## License
|
||||
|
||||
Jason is released under the Apache License 2.0 - see the [LICENSE](LICENSE) file.
|
||||
|
||||
Some elements of tests and benchmarks have their origins in the
|
||||
[Poison library](https://github.com/devinus/poison) and were initially licensed under [CC0-1.0](https://creativecommons.org/publicdomain/zero/1.0/).
|
||||
@@ -0,0 +1,21 @@
|
||||
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/michalmuskala/jason">>}]}.
|
||||
{<<"name">>,<<"jason">>}.
|
||||
{<<"version">>,<<"1.4.4">>}.
|
||||
{<<"description">>,
|
||||
<<"A blazing fast JSON parser and generator in pure Elixir.">>}.
|
||||
{<<"elixir">>,<<"~> 1.4">>}.
|
||||
{<<"app">>,<<"jason">>}.
|
||||
{<<"licenses">>,[<<"Apache-2.0">>]}.
|
||||
{<<"requirements">>,
|
||||
[[{<<"name">>,<<"decimal">>},
|
||||
{<<"app">>,<<"decimal">>},
|
||||
{<<"optional">>,true},
|
||||
{<<"requirement">>,<<"~> 1.0 or ~> 2.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}]]}.
|
||||
{<<"files">>,
|
||||
[<<"lib">>,<<"lib/jason.ex">>,<<"lib/encoder.ex">>,<<"lib/decoder.ex">>,
|
||||
<<"lib/ordered_object.ex">>,<<"lib/formatter.ex">>,<<"lib/encode.ex">>,
|
||||
<<"lib/codegen.ex">>,<<"lib/helpers.ex">>,<<"lib/sigil.ex">>,
|
||||
<<"lib/fragment.ex">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>,
|
||||
<<"CHANGELOG.md">>]}.
|
||||
{<<"build_tools">>,[<<"mix">>]}.
|
||||
@@ -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
|
||||
@@ -0,0 +1,75 @@
|
||||
defmodule Jason.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
@source_url "https://github.com/michalmuskala/jason"
|
||||
@version "1.4.4"
|
||||
|
||||
def project() do
|
||||
[
|
||||
app: :jason,
|
||||
version: @version,
|
||||
elixir: "~> 1.4",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
consolidate_protocols: Mix.env() != :test,
|
||||
deps: deps(),
|
||||
preferred_cli_env: [docs: :docs],
|
||||
dialyzer: dialyzer(),
|
||||
description: description(),
|
||||
package: package(),
|
||||
docs: docs()
|
||||
]
|
||||
end
|
||||
|
||||
def application() do
|
||||
[
|
||||
extra_applications: []
|
||||
]
|
||||
end
|
||||
|
||||
defp deps() do
|
||||
[
|
||||
{:decimal, "~> 1.0 or ~> 2.0", optional: true},
|
||||
{:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false},
|
||||
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
|
||||
] ++ maybe_stream_data()
|
||||
end
|
||||
|
||||
defp maybe_stream_data() do
|
||||
if Version.match?(System.version(), "~> 1.12") do
|
||||
[{:stream_data, "~> 1.0", only: :test}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp dialyzer() do
|
||||
[
|
||||
plt_add_apps: [:decimal]
|
||||
]
|
||||
end
|
||||
|
||||
defp description() do
|
||||
"""
|
||||
A blazing fast JSON parser and generator in pure Elixir.
|
||||
"""
|
||||
end
|
||||
|
||||
defp package() do
|
||||
[
|
||||
maintainers: ["Michał Muskała"],
|
||||
licenses: ["Apache-2.0"],
|
||||
links: %{"GitHub" => @source_url}
|
||||
]
|
||||
end
|
||||
|
||||
defp docs() do
|
||||
[
|
||||
main: "readme",
|
||||
name: "Jason",
|
||||
source_ref: "v#{@version}",
|
||||
canonical: "http://hexdocs.pm/jason",
|
||||
source_url: @source_url,
|
||||
extras: ["README.md", "CHANGELOG.md", "LICENSE"]
|
||||
]
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user