williamthome / euneus

An incredibly flexible and performant JSON parser, generator and formatter in pure Erlang.
https://hex.pm/packages/euneus
Apache License 2.0
22 stars 2 forks source link

Change the API due to consistency principle #37

Closed williamthome closed 3 months ago

williamthome commented 9 months ago

Closes #36

This PR intends to improve the Euneus API by changing some weird things about the v1. That means many code changes, so a new major version (v2) must be released.

[!WARNING]

This is a draft PR and is under development.

Plugins

Plugins are now called codecs:

A codec is a device or computer program that encodes or decodes a data stream or signal.

So, renaming the plugins option to codecs and the euneus_plugin behavior to euneus_codec makes much sense to me.

The codecs (AKA plugins) now accept arguments that need to be parsed before the encoding and decoding. Arguments are any term and are defined by passing a tuple of two elements to the codecs list, where the first element is the name of a built-in codec or the module name of a custom one, and the second is the arguments, e.g.:

% {Name :: atom() | module(), Arguments :: term()}.
euneus:encode(<<"foo">>, #{binary => #{codecs => [{euneus_foo_codec, [foo_as_bar]}]}}).

% If just the name of the codec is passed to the codecs list, the arguments will be an empty list (`[]`):
euneus:encode(<<"foo">>, #{binary => #{codecs => [euneus_foo_codec]}}).

To remove the need to always normalize these arguments, a new callback init/0 is introduced in euneus_codec (AKA euneus_plugin) and must return {ok, term()}.

So, the euneus_codec behavior have now these callbacks:

-callback init(term()) -> {ok, term()}.

-callback encode(Input, CodecOpts, EncodeSettings) -> Output when
    Input :: term(),
    CodecOpts :: term(),
    EncodeSettings :: encode_settings(),
    Output :: encode(Input).

-callback decode(Input, CodecOpts, DecodeSettings) -> Output when
    Input :: term(),
    CodecOpts :: term(),
    DecodeSettings :: decode_settings(),
    Output :: decode(Input).

Encode

Encode options

The nulls option is now called null_values and its default is now [null, undefined] instead of just [undefined], e.g.:

1> euneus:encode_to_binary([null, undefined]).
{ok,<<"[null,null]">>}

This is more concise with the Javascript result:

> JSON.stringify([null, undefined])
'[null,null]'

Elixir uses :nil to represent null, so you must override the default to get the same result:

1> euneus:encode_to_binary([nil, undefined], #{null_values => [nil, undefined]}).
{ok,<<"[null,null]">>}

The drop_nulls plugin does not exist anymore in Euneus. We now have a skip_values option for maps that drop keys that values are on this list and its default is [undefined], e.g.:

1> euneus:encode_to_binary(#{a => null, b => undefined}, #{}).
{ok,<<"{\"a\":null}">>}

In Javascript:

> JSON.stringify({a: null, b: undefined})
'{"a":null}'

To disable this behavior, the skip_values option must be overridden:

1> euneus:encode_to_binary(#{a => null, b => undefined}, #{map => #{skip_values => []}}).
{ok,<<"{\"a\":null,\"b\":null}">>}

Encode term types

tuple, pid, port, and reference are now encoded by default via:

In v1, these types are encoded via codecs (AKA plugins).

Encode options spec

-type options() :: #{
    null_values => [null()],
    binary => #{
        encode => encode_fun(binary()),
        codecs => [codec(binary())]
    },
    atom => #{
        encode => encode_fun(atom()),
        codecs => [codec(atom())]
    },
    integer => #{
        encode => encode_fun(integer()),
        codecs => [codec(integer())],
        base => integer_base()
    },
    float => #{
        encode => encode_fun(float()),
        codecs => [codec(float())],
        to_binary_options => float_to_binary_options()
    },
    list => #{
        encode => encode_fun(list()),
        codecs => [proplist | codec(list())]
    },
    map => #{
        encode => encode_fun(map()),
        codecs => [codec(map())],
        skip_values => [term()]
    },
    tuple => #{
        encode => encode_fun(tuple()),
        codecs => [ datetime
                  | timestamp
                  | ipv4
                  | ipv6
                  % TODO: {record, Opts}
                  | codec(tuple()) ]
    },
    pid => #{
        encode => encode_fun(pid()),
        codecs => [codec(pid())]
    },
    port => #{
        encode => encode_fun(port()),
        codecs => [codec(port())]
    },
    reference => #{
        encode => encode_fun(reference()),
        codecs => [codec(reference())]
    },
    escape => json
            | html
            | javascript
            | unicode
            | escape_fun(binary()),
    handle_error => handle_error_fun()
}.

[!NOTE]

This is not the end of the description of this PR. More explanation coming soon.

Suggestions welcome.

TODO:

  • [ ] More encoding explanation
  • [ ] Decode
    • [ ] Options explanation
    • [ ] Options spec
  • [ ] Benchmark
    • [ ] Encode
    • [ ] Decode
  • [ ] Update docs
asabil commented 8 months ago

I haven't had much time the last few weeks to look into this in depth, but not that I have, I think this looks really great!

williamthome commented 8 months ago

Thanks, @asabil! I do not have too much time in the last month to look at these changes and finish the PR. I hope to finish it next month.

williamthome commented 3 months ago

Closing this PR in favor of #43