diff --git a/.gitignore b/.gitignore index 427b0f8..0f367e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ _build +_checkouts doc/ erl_crash.dump rebar3.crashdump - diff --git a/README.md b/README.md index 1a0d93b..3bd64e3 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,16 @@ An incredibly flexible and performant JSON parser, generator and formatter in pu Euneus is built on the top of the new [OTP json module](https://erlang.org/documentation/doc-15.0-rc3/lib/stdlib-6.0/doc/html/json.html). -## ⚠️ Disclaimer +Both encoder and decoder fully conform to [RFC 8259](https://datatracker.ietf.org/doc/html/rfc8259) +and [ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) standards +and are tested using [JSONTestSuite](https://github.com/nst/JSONTestSuite). -This is a work-in-progress branch aiming to release a v2.0 of Euneus. - -For v1.0, please look at the [v1.2.2](https://github.com/williamthome/euneus/tree/v1.2.2) tag. +Detailed examples and further explanation can be found at [hexdocs](https://hexdocs.pm/euneus). ## Installation +### Erlang + ```erlang % rebar.config {deps, [ @@ -20,6 +22,18 @@ For v1.0, please look at the [v1.2.2](https://github.com/williamthome/euneus/tre ]}. ``` +### Elixir + +```elixir +# mix.exs +defp deps do + [ + {:json_polyfill, "~> 0.1.1"}, # Required only for OTP < 27 + {:euneus, git: "https://github.com/williamthome/euneus.git", branch: "main"} + ] +end +``` + ## Basic usage ```erlang @@ -31,180 +45,90 @@ For v1.0, please look at the [v1.2.2](https://github.com/williamthome/euneus/tre ## Encode -The functions `euneus:encode/1,2` encode an Erlang term into a binary JSON. -The second argument of `euneus:encode/2` are options, and this is the spec: +The functions `euneus:encode/1` `euneus:encode/2` encodes an Erlang term into a binary JSON. +The second argument of `euneus:encode/2` are options. -```erlang --type options() :: #{ - codecs => [codec()], - nulls => [term()], - skip_values => [term()], - key_to_binary => fun((term()) -> binary()), - sort_keys => boolean(), - proplists => boolean() | {true, is_proplist()}, - escape => fun((binary()) -> iodata()), - encode_integer => encode(integer()), - encode_float => encode(float()), - encode_atom => encode(atom()), - encode_list => encode(list()), - encode_map => encode(map()), - encode_tuple => encode(tuple()), - encode_pid => encode(pid()), - encode_port => encode(port()), - encode_reference => encode(reference()) -}. - --type codec() :: - datetime - | timestamp - | ipv4 - | ipv6 - % {records, #{foo => {record_info(fields, foo), record_info(size, foo)}}} - | {records, #{Name :: atom() := {Fields :: [atom()], Size :: pos_integer()}}} - | codec_callback(). - --type codec_callback() :: fun((tuple()) -> next | {halt, term()}). - --type is_proplist() :: fun((list()) -> boolean()). - --type encode(Type) :: fun((Type, json:encoder(), state()) -> iodata()). -``` +Please see the `m:euneus_encoder` [documentation](https://hexdocs.pm/euneus/doc/euneus_encoder.html) +for more examples and detailed explanation. -### Encode example - -```erlang -1> Term = #{ -.. id => 1, -.. date => {{1970,1,1},{0,0,0}}, -.. ip => {0,0,0,0} -.. }. -#{id => 1,date => {{1970,1,1},{0,0,0}},ip => {0,0,0,0}} -2> Opts = #{tuple => [datetime, ipv4]}. -#{tuple => [datetime,ipv4]} -3> euneus:encode(Term, Opts). -<<"{\"id\":1,\"date\":\"1970-01-01T00:00:00Z\",\"ip\":\"0.0.0.0\"}">> -``` +The data mapping and error reasons can be found in the OTP json encode function [documentation](https://erlang.org/documentation/doc-15.0-rc3/lib/stdlib-6.0/doc/html/json.html#encode/1). ## Decode -The functions `euneus:decode/1,2` decode a binary JSON into an Erlang term. -The second argument of `euneus:decode/2` are options, and this is the spec: +The functions `euneus:decode/1` and `euneus:decode/2` decodes a binary JSON into an Erlang term. +The second argument of `euneus:decode/2` are options. -```erlang --type options() :: #{ - codecs => [codec()], - null => term(), - binary_to_float => json:from_binary_fun(), - binary_to_integer => json:from_binary_fun(), - array_start => json:array_start_fun(), - array_push => json:array_push_fun(), - array_finish => - ordered - | reversed - | json:array_finish_fun(), - object_start => json:object_start_fun(), - object_keys => - copy - | atom - | existing_atom - | json:from_binary_fun(), - object_push => json:object_push_fun(), - object_finish => - map - | proplist - | reversed_proplist - | json:object_finish_fun() -}. - --type codec() :: - copy - | timestamp - | datetime - | ipv4 - | ipv6 - | pid - | port - | reference - | codec_callback(). - --type codec_callback() :: fun((binary()) -> next | {halt, term()}). -``` +Please see the `m:euneus_decoder` [documentation](https://hexdocs.pm/euneus/doc/euneus_decoder.html) +for more examples and detailed explanation. -### Decode example +The data mapping and error reasons can be found in the OTP json decode function [documentation](https://erlang.org/documentation/doc-15.0-rc3/lib/stdlib-6.0/doc/html/json.html#decode/1). -```erlang -1> JSON = ~""" -.. { -.. "id": 1, -.. "date": "1970-01-01T00:00:00Z", -.. "ip": "0.0.0.0" -.. } -.. """. -<<"{\n \"id\": 1,\n \"date\": \"1970-01-01T00:00:00Z\",\n \"ip\": \"0.0.0.0\"\n}">> -2> Opts = #{ -.. codecs => [datetime, ipv4], -.. object_keys => atom -.. }. -#{codecs => [datetime,ipv4],object_keys => atom} -3> euneus:decode(JSON, Opts). -#{id => 1,date => {{1970,1,1},{0,0,0}},ip => {0,0,0,0}} +## Format -``` +Two functions provide JSON formatting: -## Benchmark +- `euneus:minify/1` - Removes any extra spaces and new line characters; +- `euneus:format/2` - Formats the JSON by passing custom options. + +Please see the `m:euneus_formatter` [documentation](https://hexdocs.pm/euneus/doc/euneus_formatter.html) +for more examples and detailed explanation. -The benchmarks are implemented very simply, but they are a good start for optimizing Euneus -since no optimizations have been made. You will find the data and tests under the test folder. +## Benchmark -```shell +The benchmarks are implemented very simply, but they are a good start foroptimizing +Euneus since no optimizations have been made. You will find the benchmark commands +in `euneus_benchmarker`, and data and tests under the test folder. + +> [!IMPORTANT] +> For the first benchmark run, bootstrapping `erlperf` is required: +> +> ```console +> $ rebar3 as benchmark shell +> +> 1> euneus_benchmarker:bootstrap(). +> ===> Verifying dependencies... +> ===> Analyzing applications... +> ===> Compiling erlperf +> ===> Building escript for erlperf... +> ok +> ``` + +### Results + +> Setup: +> +> - OS : Linux +> - CPU: 12th Gen Intel(R) Core(TM) i9-12900HX +> - VM : Erlang/OTP 27 [erts-15.0.1] [source] [64-bit] [smp:24:24] [ds:24:24:10] [async-threads:1] [jit:ns] + +```console $ rebar3 as benchmark shell -% --------------------------------------------------------------------- -% The following command builds the erlperf escript. Run it only for the -% first time or if it does not exist. -% --------------------------------------------------------------------- -1> euneus_benchmarker:bootstrap(). -===> Verifying dependencies... -===> Analyzing applications... -===> Compiling erlperf -===> Building escript for erlperf... -ok - -% --------------------------------------------------------------------- -% Since erlperf currently does not accept labels: -% - euneus: #Fun -% - jiffy: #Fun -% - thoas: #Fun -% --------------------------------------------------------------------- -2> euneus_benchmarker:encode_benchmark(). -OS : Linux -CPU: 12th Gen Intel(R) Core(TM) i9-12900HX -VM : Erlang/OTP 27 [erts-15.0.1] [source] [64-bit] [smp:24:24] [ds:24:24:10] [async-threads:1] [jit:ns] - -Code || Samples Avg StdDev Median P99 Iteration Rel -#Fun 1 3 37 2.70% 37 38 27036 us 100% -#Fun 1 3 24 6.28% 24 26 41110 us 66% -#Fun 1 3 14 4.22% 14 14 73195 us 37% -ok - -% --------------------------------------------------------------------- -% Since erlperf currently does not accept labels: -% - euneus: #Fun -% - jiffy: #Fun -% - thoas: #Fun -% --------------------------------------------------------------------- -3> euneus_benchmarker:decode_benchmark(). -OS : Linux -CPU: 12th Gen Intel(R) Core(TM) i9-12900HX -VM : Erlang/OTP 27 [erts-15.0.1] [source] [64-bit] [smp:24:24] [ds:24:24:10] [async-threads:1] [jit:ns] - -Code || Samples Avg StdDev Median P99 Iteration Rel -#Fun 1 3 24 2.44% 24 24 42268 us 100% -#Fun 1 3 19 3.09% 19 19 53589 us 79% -#Fun 1 3 14 0.00% 14 14 71452 us 59% -ok +1> euneus_benchmarker:encode_benchmark(). +Code || Samples Avg StdDev Median P99 Iteration Rel +jiffy 1 3 37 2.70% 37 38 27036 us 100% +euneus 1 3 24 6.28% 24 26 41110 us 66% +thoas 1 3 14 4.22% 14 14 73195 us 37% + +2> euneus_benchmarker:decode_benchmark(). +Code || Samples Avg StdDev Median P99 Iteration Rel +euneus 1 3 24 2.44% 24 24 42268 us 100% +jiffy 1 3 19 3.09% 19 19 53589 us 79% +thoas 1 3 14 0.00% 14 14 71452 us 59% ``` +> [!NOTE] +> Since `erlperf` currently does not accept labels, `Code` returns something like: +> +> - euneus_benchmarker:encode_benchmark/0: +> - #Fun = euneus +> - #Fun = jiffy +> - #Fun = thoas +> - euneus_benchmarker:decode_benchmark/0: +> - #Fun = euneus +> - #Fun = jiffy +> - #Fun = thoas + ## Sponsors If you like this tool, please consider [sponsoring me](https://github.com/sponsors/williamthome). diff --git a/rebar.config b/rebar.config index 9d6ad90..b308a8a 100644 --- a/rebar.config +++ b/rebar.config @@ -75,11 +75,13 @@ {plt_extra_apps, [ common_test, eunit, + doctest, jiffy, thoas ]} ]}, {deps, [ + {doctest, "0.9.3"}, {jiffy, "1.1.2"}, {thoas, "1.2.1"} ]}, @@ -117,3 +119,5 @@ ex_doc ]} ]}. + +{eunit_opts, [no_tty, {report, {doctest_eunit_report, []}}]}. diff --git a/src/euneus.erl b/src/euneus.erl index b853865..d8d2cf2 100644 --- a/src/euneus.erl +++ b/src/euneus.erl @@ -28,43 +28,123 @@ -ignore_xref([minify/1]). -ignore_xref([format/2]). +%% -------------------------------------------------------------------- +%% DocTest +%% -------------------------------------------------------------------- + +-ifdef(TEST). +-include_lib("doctest/include/doctest.hrl"). +-endif. + %% -------------------------------------------------------------------- %% API functions %% -------------------------------------------------------------------- -spec encode(term()) -> binary(). +%% @doc Encodes a term into a binary JSON. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode(foo). +%% <<"\"foo\"">> +%% ''' encode(Term) -> encode(Term, #{}). -spec encode(term(), euneus_encoder:options()) -> binary(). +%% @doc Encodes a term into a binary JSON. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode(foo, #{}). +%% <<"\"foo\"">> +%% ''' encode(Term, Opts) -> iolist_to_binary(euneus_encoder:encode(Term, Opts)). -spec encode_iodata(term()) -> iodata(). +%% @doc Encodes a term into an iodata JSON. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode_iodata(foo). +%% [$", <<"foo">>, $"] +%% ''' encode_iodata(Term) -> encode_iodata(Term, #{}). -spec encode_iodata(term(), euneus_encoder:options()) -> iodata(). +%% @doc Encodes a term into an iodata JSON. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode_iodata(foo, #{}). +%% [$", <<"foo">>, $"] +%% ''' encode_iodata(Term, Opts) -> euneus_encoder:encode(Term, Opts). -spec decode(binary()) -> term(). +%% @doc Decodes a binary JSON into a term. +%% +%% Example: +%% +%% ``` +%% 1> euneus:decode(<<"\"foo\"">>). +%% <<"foo">> +%% ''' decode(JSON) -> decode(JSON, #{}). -spec decode(binary(), euneus_decoder:options()) -> term(). +%% @doc Decodes a binary JSON into a term. +%% +%% Example: +%% +%% ``` +%% 1> euneus:decode(<<"\"foo\"">>, #{}). +%% <<"foo">> +%% ''' decode(JSON, Opts) -> euneus_decoder:decode(JSON, Opts). -spec decode_iodata(iodata()) -> term(). +%% @doc Decodes an iodata JSON into a term. +%% +%% Example: +%% +%% ``` +%% 1> euneus:decode_iodata([$", <<"foo">>, $"]). +%% <<"foo">> +%% ''' decode_iodata(JSON) -> decode_iodata(JSON, #{}). -spec decode_iodata(iodata(), euneus_decoder:options()) -> term(). +%% @doc Decodes an iodata JSON into a term. +%% +%% Example: +%% +%% ``` +%% 1> euneus:decode_iodata([$", <<"foo">>, $"], #{}). +%% <<"foo">> +%% ''' decode_iodata(JSON, Opts) -> euneus_decoder:decode(iolist_to_binary(JSON), Opts). -spec minify(binary()) -> binary(). +%% @doc Minifies a binary JSON. +%% +%% Example: +%% +%% ``` +%% 1> euneus:minify(<<" \n{\"foo\" : [ true , \n null ] \n } ">>). +%% <<"{\"foo\":[true,null]}">> +%% ''' minify(JSON) -> format(JSON, #{ indent_type => spaces, @@ -74,5 +154,20 @@ minify(JSON) -> }). -spec format(binary(), euneus_formatter:options()) -> binary(). +%% @doc Formats a binary JSON. +%% +%% Example: +%% +%% ``` +%% 1> Opts = #{ +%% .. indent_type => tabs, +%% .. indent_width => 1, +%% .. spaced_values => true, +%% .. crlf => n +%% .. }. +%% #{indent_type => tabs,indent_width => 1,spaced_values => true, crlf => n} +%% 2> euneus:format(<<" \n{\"foo\" : [ true , \n null ] \n } ">>, Opts). +%% <<"{\n\t\"foo\": [\n\t\ttrue,\n\t\tnull\n\t]\n}">> +%% ''' format(JSON, Opts) -> iolist_to_binary(euneus_formatter:format(JSON, Opts)). diff --git a/src/euneus_decoder.erl b/src/euneus_decoder.erl index adc5363..d6043cc 100644 --- a/src/euneus_decoder.erl +++ b/src/euneus_decoder.erl @@ -47,7 +47,8 @@ | json:array_finish_fun(), object_start => json:object_start_fun(), object_keys => - copy + binary + | copy | atom | existing_atom | json:from_binary_fun(), @@ -72,11 +73,287 @@ -type codec_callback() :: fun((binary()) -> next | {halt, term()}). +%% -------------------------------------------------------------------- +%% DocTest +%% -------------------------------------------------------------------- + +-ifdef(TEST). +-include_lib("doctest/include/doctest.hrl"). +-endif. + %% -------------------------------------------------------------------- %% API functions %% -------------------------------------------------------------------- -spec decode(binary(), options()) -> term(). +%% @doc Decodes a binary JSON into a term. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode(<<"\"foo\"">>, #{}). +%% <<"foo">> +%% ''' +%% +%% Option details: +%% +%%
    +%%
  • +%% `codecs' - Transforms a JSON binary value into an Erlang term. +%% By returning `next', the next codec will be called, or by returning +%% `{halt, Term :: term()}', the Term is returned as the value. +%% +%% You can use the built-in codecs or your own. +%% Please see the `t:euneus_decoder:codec/0' type for details. +%% +%% Default is `[]'. +%% +%% Built-in codecs: +%% +%%
      +%%
    • +%% `timestamp' - Transforms an ISO 8601 string with milliseconds into +%% an `t:erlang:timestamp/0'. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode(<<"\"1970-01-01T00:00:00.000Z\"">>, #{codecs => [timestamp]}). +%% {0, 0, 0} +%% ''' +%%
    • +%%
    • +%% `datetime' - Transforms an ISO 8601 string into a `t:calendar:datetime/0'. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode(<<"\"1970-01-01T00:00:00Z\"">>, #{codecs => [datetime]}). +%% {{1970, 01, 01},{00, 00, 00}} +%% ''' +%%
    • +%%
    • +%% `ipv4' - Transforms a JSON string into an `t:inet:ip4_address/0'. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode(<<"\"127.0.0.1\"">>, #{codecs => [ipv4]}). +%% {127, 0, 0, 1} +%% ''' +%%
    • +%%
    • +%% `ipv6' - Transforms a JSON string into an `t:inet:ip6_address/0'. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode(<<"\"::\"">>, #{codecs => [ipv6]}). +%% {0, 0, 0, 0, 0, 0, 0, 0} +%% 2> euneus_decoder:decode(<<"\"::1\"">>, #{codecs => [ipv6]}). +%% {0, 0, 0, 0, 0, 0, 0, 1} +%% 3> euneus_decoder:decode(<<"\"::192.168.42.2\"">>, #{codecs => [ipv6]}). +%% {0, 0, 0, 0, 0, 0, (192 bsl 8) bor 168, (42 bsl 8) bor 2} +%% 4> euneus_decoder:decode(<<"\"::ffff:192.168.42.2\"">>, #{codecs => [ipv6]}). +%% {0, 0, 0, 0, 0, 16#FFFF, (192 bsl 8) bor 168, (42 bsl 8) bor 2} +%% 5> euneus_decoder:decode(<<"\"3ffe:b80:1f8d:2:204:acff:fe17:bf38\"">>, #{codecs => [ipv6]}). +%% {16#3ffe, 16#b80, 16#1f8d, 16#2, 16#204, 16#acff, 16#fe17, 16#bf38} +%% 6> euneus_decoder:decode(<<"\"fe80::204:acff:fe17:bf38\"">>, #{codecs => [ipv6]}). +%% {16#fe80, 0, 0, 0, 16#204, 16#acff, 16#fe17, 16#bf38} +%% ''' +%%
    • +%%
    • +%% `pid' - Transforms a JSON string into an `t:erlang:pid/0'. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode(<<"\"<0.92.0>\"">>, #{codecs => [pid]}) +%% .. =:= list_to_pid("<0.92.0>"). +%% true +%% ''' +%%
    • +%%
    • +%% `port' - Transforms a JSON string into an `t:erlang:port/0'. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode(<<"\"#Port<0.1>\"">>, #{codecs => [port]}) +%% .. =:= list_to_port("#Port<0.1>"). +%% true +%% ''' +%%
    • +%%
    • +%% `reference' - Transforms a JSON string into an `t:erlang:reference/0'. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode(<<"\"#Ref<0.314572725.1088159747.110918>\"">>, #{codecs => [reference]}) +%% .. =:= list_to_ref("#Ref<0.314572725.1088159747.110918>"). +%% true +%% ''' +%%
    • +%%
    +%% +%% Custom codec example: +%% +%% ``` +%% 1> euneus:decode(<<"\"foo\"">>, #{codecs => [fun(<<"foo">>) -> {halt, foo} end]}). +%% foo +%% ''' +%%
  • +%%
  • +%% `null' - Defines which term should be considered null. +%% +%% Default is `null'. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode(<<"null">>, #{null => nil}). +%% nil +%% ''' +%%
  • +%%
  • +%% `binary_to_float' - Overrides the default binary to float conversion. +%% +%%
  • +%%
  • +%% `binary_to_integer' - Overrides the default binary to integer conversion.. +%% +%%
  • +%%
  • +%% `array_start' - Overrides the `t:json:array_start_fun/0' callback. +%% +%%
  • +%%
  • +%% `array_push' - Overrides the `t:json:array_push_fun/0' callback. +%% +%%
  • +%%
  • +%% `array_finish' - Overrides the `t:json:array_finish_fun/0' callback. +%% +%% In addition to the custom function, there are: +%% +%%
      +%%
    • +%% `ordered' - Returns the array in the same order as the JSON. +%% +%% That's the slower option. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode(<<"[1,2,3]">>, #{array_finish => ordered}). +%% [1,2,3] +%% ''' +%%
    • +%%
    • +%% `reversed' - Returns the array in a reversed order. +%% +%% That's the faster option. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode(<<"[1,2,3]">>, #{array_finish => reversed}). +%% [3,2,1] +%% ''' +%%
    • +%%
    +%% +%% Default is `ordered'. +%% +%%
  • +%%
  • +%% `object_start' - Overrides the `t:json:object_start_fun/0' callback. +%% +%%
  • +%%
  • +%% `object_keys' - Transforms JSON objects key into Erlang term. +%% +%% In addition to the custom function, there are: +%% +%%
      +%%
    • +%% `binary' - Returns the key as `t:erlang:binary/0'. +%%
    • +%%
    • +%% `copy' - Copies the key via `binary:copy/1' returning it as `t:erlang:binary/0'. +%%
    • +%%
    • +%% `atom' - Returns the key as `t:erlang:atom/0' via `erlang:binary_to_atom/2'. +%%
    • +%%
    • +%% `existing_atom' - Returns the key as `t:erlang:atom/0' via +%% `erlang:binary_to_existing_atom/2'. +%%
    • +%%
    +%% +%% Default is `binary'. +%% +%%
  • +%%
  • +%% `object_push' - Overrides the `t:json:object_push_fun/0' callback. +%% +%%
  • +%%
  • +%% `object_finish' - Overrides the `t:json:object_finish_fun/0' callback. +%% +%% In addition to the custom function, there are: +%% +%%
      +%%
    • +%% `map' - Returns the object as a `t:erlang:map/0'. +%% +%% That's the slower option. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode( +%% .. <<"{\"a\":\"a\",\"b\":\"b\",\"c\":\"c\"}">>, +%% .. #{object_finish => map} +%% .. ). +%% #{<<"a">> => <<"a">>,<<"b">> => <<"b">>,<<"c">> => <<"c">>} +%% ''' +%%
    • +%%
    • +%% `proplist' - Returns the object as an ordered `t:proplists:proplist/0'. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode( +%% .. <<"{\"a\":\"a\",\"b\":\"b\",\"c\":\"c\"}">>, +%% .. #{object_finish => proplist} +%% .. ). +%% [{<<"a">>, <<"a">>},{<<"b">>, <<"b">>},{<<"c">>, <<"c">>}] +%% ''' +%%
    • +%%
    • +%% `reversed_proplist' - Returns the object as a reversed `t:proplists:proplist/0'. +%% +%% That's the faster option. +%% +%% Example: +%% +%% ``` +%% 1> euneus_decoder:decode( +%% .. <<"{\"a\":\"a\",\"b\":\"b\",\"c\":\"c\"}">>, +%% .. #{object_finish => reversed_proplist} +%% .. ). +%% [{<<"c">>, <<"c">>},{<<"b">>, <<"b">>},{<<"a">>, <<"a">>}] +%% ''' +%%
    • +%%
    +%% +%% Default is `map'. +%% +%%
  • +%%
decode(JSON, Opts) when is_binary(JSON), is_map(Opts) -> Codecs = maps:get(codecs, Opts, []), Decoders = decoders(Codecs, Opts), diff --git a/src/euneus_encoder.erl b/src/euneus_encoder.erl index 938c4e6..32e0a4c 100644 --- a/src/euneus_encoder.erl +++ b/src/euneus_encoder.erl @@ -71,7 +71,6 @@ | datetime | ipv4 | ipv6 - % {records, #{foo => {record_info(fields, foo), record_info(size, foo)}}} | {records, #{Name :: atom() := {Fields :: [atom()], Size :: pos_integer()}}} | codec_callback(). -export_type([codec/0]). @@ -102,11 +101,231 @@ }). -opaque state() :: #state{}. +%% -------------------------------------------------------------------- +%% DocTest +%% -------------------------------------------------------------------- + +-ifdef(TEST). +-include_lib("doctest/include/doctest.hrl"). +-endif. + %% -------------------------------------------------------------------- %% API functions %% -------------------------------------------------------------------- -spec encode(term(), options()) -> iodata(). +%% @doc Encode a term into an iodata JSON. +%% +%% Example: +%% +%% ``` +%% 1> euneus_encoder:encode(foo, #{}). +%% [$", <<"foo">>, $"] +%% ''' +%% +%% Option details: +%% +%%
    +%%
    +%%

    Note

    +%% For better visualization and understanding, all options examples use +%% `euneus:encode/2', which returns a binary. +%%
    +%%
  • +%% `codecs' - Transforms tuples into any other Erlang term that will be encoded +%% again into a JSON value. By returning `next', the next codec will be called, +%% or by returning `{halt, Term :: term()}', the Term will be encoded again. +%% +%% You can use the built-in codecs or your own. +%% Please see the `t:euneus_encoder:codec/0' type for details. +%% +%% Default is `[]'. +%% +%% Built-in codecs: +%% +%%
      +%%
    • +%% `timestamp' - Transforms an `t:erlang:timestamp/0' into an ISO 8601 string +%% with milliseconds. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode({0, 0, 0}, #{codecs => [timestamp]}). +%% <<"\"1970-01-01T00:00:00.000Z\"">> +%% ''' +%%
    • +%%
    • +%% `datetime' - Transforms a `t:calendar:datetime/0' into an ISO 8601 string. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode({{1970, 01, 01}, {00, 00, 00}}, #{codecs => [datetime]}). +%% <<"\"1970-01-01T00:00:00Z\"">> +%% ''' +%%
    • +%%
    • +%% `ipv4' - Transforms an `t:inet:ip4_address/0' into a JSON string. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode({127, 0, 0, 1}, #{codecs => [ipv4]}). +%% <<"\"127.0.0.1\"">> +%% ''' +%%
    • +%%
    • +%% `ipv6' - Transforms an `t:inet:ip6_address/0' into a JSON string. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode({0, 0, 0, 0, 0, 0, 0, 0}, #{codecs => [ipv6]}). +%% <<"\"::\"">> +%% 2> euneus:encode({0, 0, 0, 0, 0, 0, 0, 1}, #{codecs => [ipv6]}). +%% <<"\"::1\"">> +%% 3> euneus:encode( +%% .. {0, 0, 0, 0, 0, 0, (192 bsl 8) bor 168, (42 bsl 8) bor 2}, +%% .. #{codecs => [ipv6]} +%% .. ). +%% <<"\"::192.168.42.2\"">> +%% 4> euneus:encode( +%% .. {0, 0, 0, 0, 0, 16#FFFF, (192 bsl 8) bor 168, (42 bsl 8) bor 2}, +%% .. #{codecs => [ipv6]} +%% .. ). +%% <<"\"::ffff:192.168.42.2\"">> +%% 5> euneus:encode( +%% .. {16#3ffe, 16#b80, 16#1f8d, 16#2, 16#204, 16#acff, 16#fe17, 16#bf38}, +%% .. #{codecs => [ipv6]} +%% .. ). +%% <<"\"3ffe:b80:1f8d:2:204:acff:fe17:bf38\"">> +%% 6> euneus:encode( +%% .. {16#fe80, 0, 0, 0, 16#204, 16#acff, 16#fe17, 16#bf38}, +%% .. #{codecs => [ipv6]} +%% .. ). +%% <<"\"fe80::204:acff:fe17:bf38\"">> +%% ''' +%%
    • +%%
    • +%% `records' - Transforms records into JSON objects. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode( +%% .. % Same as '-record(foo, {bar, baz}).' +%% .. {foo, bar, baz}, +%% .. #{codecs => [{records, #{ +%% .. % Same as 'foo => {record_info(fields, foo), record_info(size, foo)}' +%% .. foo => {[bar, baz], 3} +%% .. }}]} +%% .. ). +%% <<"{\"bar\":\"bar\",\"baz\":\"baz\"}">> +%% ''' +%%
    • +%%
    +%% +%% Custom codec example: +%% +%% ``` +%% 1> euneus:encode({foo}, #{codecs => [fun({foo}) -> {halt, foo} end]}). +%% <<"\"foo\"">> +%% ''' +%%
  • +%%
  • +%% `nulls' - Defines which values should be encoded as null. +%% +%% Default is `[null]'. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode([null, nil, foo], #{nulls => [null, nil]}). +%% <<"[null,null,\"foo\"]">> +%% ''' +%%
  • +%%
  • +%% `skip_values' - Defines which map values should be ignored. +%% This option permits achieves the same behavior as Javascript, +%% which ignores undefined values of objects. +%% +%% Default is `[undefined]'. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode( +%% .. #{foo => bar, bar => undefined, baz => null}, +%% .. #{skip_values => [undefined, null]} +%% .. ). +%% <<"{\"foo\":\"bar\"}">> +%% ''' +%%
  • +%%
  • +%% `key_to_binary' - Overrides the default conversion of map keys to a string. +%%
  • +%%
  • +%% `sort_keys' - Defines if the object keys should be sorted. +%% +%% Default is `false'. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode(#{c => c, a => a, b => b}, #{sort_keys => true}). +%% <<"{\"a\":\"a\",\"b\":\"b\",\"c\":\"c\"}">> +%% ''' +%%
  • +%%
  • +%% `proplists' - If true, converts proplists into objects. +%% +%% Default is `false'. +%% +%% Example: +%% +%% ``` +%% 1> euneus:encode([{foo, bar}, baz], #{proplists => true}). +%% <<"{\"foo\":\"bar\",\"baz\":true}">> +%% 2> euneus:encode( +%% .. [{foo, bar}, {baz, true}], +%% .. % Overrides the default is proplist check: +%% .. #{proplists => {true, fun([{_, _} | _]) -> true end}} +%% .. ). +%% <<"{\"foo\":\"bar\",\"baz\":true}">> +%% ''' +%%
  • +%%
  • +%% `escape' - Overrides the default string escaping. +%%
  • +%%
  • +%% `encode_integer' - Overrides the default integer encoder. +%%
  • +%%
  • +%% `encode_float' - Overrides the default float encoder. +%%
  • +%%
  • +%% `encode_atom' - Overrides the default atom encoder. +%%
  • +%%
  • +%% `encode_list' - Overrides the default list encoder. +%%
  • +%%
  • +%% `encode_map' - Overrides the default map encoder. +%%
  • +%%
  • +%% `encode_tuple'- Overrides the default tuple encoder. +%%
  • +%%
  • +%% `encode_pid' - Overrides the default pid encoder. +%%
  • +%%
  • +%% `encode_port' - Overrides the default port encoder. +%%
  • +%%
  • +%% `encode_reference' - Overrides the default reference encoder. +%%
  • +%%
encode(Input, Opts) -> State = new_state(Opts), json:encode(Input, fun(Term, Encode) -> diff --git a/src/euneus_formatter.erl b/src/euneus_formatter.erl index 91760c4..4fee440 100644 --- a/src/euneus_formatter.erl +++ b/src/euneus_formatter.erl @@ -20,14 +20,16 @@ indent_type := tabs | spaces, indent_width := non_neg_integer(), spaced_values := boolean(), - crlf := r | n | rn | none + crlf := crlf() }. +-type crlf() :: r | n | rn | none. + -record(state, { - depth, - indent, - spaces, - crlf + depth :: non_neg_integer(), + indent :: binary(), + spaces :: binary(), + crlf :: crlf() }). %% -------------------------------------------------------------------- @@ -35,6 +37,62 @@ %% -------------------------------------------------------------------- -spec format(binary(), options()) -> iodata(). +%% @doc Formats a binary JSON. +%% +%% Option details: +%% +%%
    +%%
    +%%

    Note

    +%% There is no default for any option, all are required. +%%
    +%%
  • +%% `indent_type' - Indent using `tabs' or `spaces'. +%% +%%
      +%%
    • +%% `tabs' - The indent char will be `$\t'. +%% +%%
    • +%%
    • +%% `spaces' - The indent char will be `$\s'. +%% +%%
    • +%%
    +%% +%%
  • +%%
  • +%% `indent_width' - The `indent_type' will be copied N times based on it. +%% +%%
  • +%%
  • +%% `spaced_values' - Defines if keys and values of objects should be +%% spaced by one `$\s' char. +%% +%%
  • +%%
  • +%% `crlf' - Defines the Carriage Return/Line Feed. +%% +%%
      +%%
    • +%% `r' - The CRLF will be `<<$\r>>'. +%% +%%
    • +%%
    • +%% `n' - The CRLF will be `<<$\n>>'. +%% +%%
    • +%%
    • +%% `rn' - The CRLF will be `<<$\r, $\n>>'. +%% +%%
    • +%%
    • +%% `none' - The CRLF will be `<<>>'. +%% +%%
    • +%%
    +%%
  • +%%
format(JSON, Opts) when is_binary(JSON), is_map(Opts) -> do_format(JSON, new_state(Opts)). @@ -71,8 +129,8 @@ parse_spaces(false) -> incr_depth(State) -> State#state{depth = State#state.depth + 1}. -decr_depth(State) -> - State#state{depth = State#state.depth - 1}. +decr_depth(#state{depth = Depth} = State) when Depth > 0 -> + State#state{depth = Depth - 1}. % Format diff --git a/test/euneus_encoder_SUITE.erl b/test/euneus_encoder_SUITE.erl index 1b238cd..bc6fd65 100644 --- a/test/euneus_encoder_SUITE.erl +++ b/test/euneus_encoder_SUITE.erl @@ -270,7 +270,7 @@ encode_reference_test(Config) when is_list(Config) -> ) ]. -unsuported_term_test() -> +unsuported_term_test(Config) when is_list(Config) -> ?assertError(unsuported_term, encode(fun() -> error end)). %% --------------------------------------------------------------------