Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change the API due to consistency principle #37

Closed
wants to merge 1 commit into from

Conversation

williamthome
Copy link
Owner

@williamthome williamthome commented Nov 28, 2023

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:

  • tuple: tuple_to_list/1;
  • pid: pid_to_list/1;
  • port: port_to_list/1;
  • reference: ref_to_list/1.

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
Copy link

asabil commented Dec 22, 2023

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
Copy link
Owner Author

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
Copy link
Owner Author

Closing this PR in favor of #43

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

null from Erlang to JSON and vice versa
2 participants