diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02678c2..2225e8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,14 @@ concurrency: jobs: ci: - name: CI + name: CI OTP-${{matrix.otp-version}} - runs-on: ubuntu-24.04 + runs-on: ${{matrix.os}} + + strategy: + matrix: + otp-version: [24, 25, 26, 27] + os: [ubuntu-24.04] steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 @@ -27,8 +32,8 @@ jobs: - uses: erlef/setup-beam@b9c58b0450cd832ccdb3c17cc156a47065d2114f # v1.18.1 id: setup-beam with: - version-type: strict - version-file: .tool-versions + otp-version: ${{matrix.otp-version}} + rebar3-version: 3.23.0 - name: Restore _build uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 @@ -50,7 +55,13 @@ jobs: -rebar3-${{steps.setup-beam.outputs.rebar3-version}}\ -hash-${{hashFiles('rebar.lock')}}" + - name: Continuous Integration (Polyfill) + if: ${{fromJSON(matrix.otp-version) < 27}} + run: | + rebar3 as test polyfill_ci + - name: Continuous Integration + if: ${{fromJSON(matrix.otp-version) >= 27}} run: | rebar3 as test ci diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 1ae24a9..0000000 --- a/.tool-versions +++ /dev/null @@ -1,2 +0,0 @@ -erlang 27.0.1 -rebar 3.23.0 diff --git a/rebar.config b/rebar.config index b308a8a..e0cc173 100644 --- a/rebar.config +++ b/rebar.config @@ -1,3 +1,5 @@ +{minimum_otp_vsn, "24"}. + {erl_opts, [ debug_info, warnings_as_errors, @@ -13,8 +15,7 @@ {warnings, [ unknown, unmatched_returns - ]}, - incremental + ]} ]}. {deps, []}. @@ -81,6 +82,7 @@ ]} ]}, {deps, [ + {json_polyfill, "0.1.3"}, {doctest, "0.9.3"}, {jiffy, "1.1.2"}, {thoas, "1.2.1"} @@ -117,6 +119,13 @@ ct, {cover, "--min_coverage 100"}, ex_doc + ]}, + {polyfill_ci, [ + lint, + hank, + xref, + eunit, + ct ]} ]}. diff --git a/src/euneus.erl b/src/euneus.erl index d8d2cf2..ac34a0f 100644 --- a/src/euneus.erl +++ b/src/euneus.erl @@ -32,9 +32,11 @@ %% DocTest %% -------------------------------------------------------------------- +-if(?OTP_RELEASE >= 27). -ifdef(TEST). -include_lib("doctest/include/doctest.hrl"). -endif. +-endif. %% -------------------------------------------------------------------- %% API functions diff --git a/src/euneus_decoder.erl b/src/euneus_decoder.erl index d6043cc..4806212 100644 --- a/src/euneus_decoder.erl +++ b/src/euneus_decoder.erl @@ -77,9 +77,11 @@ %% DocTest %% -------------------------------------------------------------------- +-if(?OTP_RELEASE >= 27). -ifdef(TEST). -include_lib("doctest/include/doctest.hrl"). -endif. +-endif. %% -------------------------------------------------------------------- %% API functions diff --git a/src/euneus_encoder.erl b/src/euneus_encoder.erl index 32e0a4c..217e65c 100644 --- a/src/euneus_encoder.erl +++ b/src/euneus_encoder.erl @@ -105,9 +105,11 @@ %% DocTest %% -------------------------------------------------------------------- +-if(?OTP_RELEASE >= 27). -ifdef(TEST). -include_lib("doctest/include/doctest.hrl"). -endif. +-endif. %% -------------------------------------------------------------------- %% API functions @@ -491,8 +493,13 @@ encode_term(Term, Encode, State) -> encode_integer(Int, _Encode, _State) -> erlang:integer_to_binary(Int, 10). +-if(?OTP_RELEASE >= 25). encode_float(Float, _Encode, _State) -> erlang:float_to_binary(Float, [short]). +-else. +encode_float(Float, _Encode, _State) -> + erlang:float_to_binary(Float, [compact, {decimals, 10}]). +-endif. encode_atom(true, _Encode, _State) -> <<"true">>; @@ -537,17 +544,48 @@ is_proplist_prop({Key, _}) -> is_proplist_prop(Key) -> is_atom(Key). +-if(?OTP_RELEASE >= 26). encode_map(Map, Encode, #state{sort_keys = false, skip_values = ValuesToSkip} = State) -> do_encode_map([ [$,, escape_map_key(Key, State), $: | encode_term(Value, Encode, State)] || Key := Value <- Map, not is_map_key(Value, ValuesToSkip) ]); -encode_map(Map, Encode, #state{sort_keys = true, skip_values = ValuesToSkip} = State) -> +encode_map(Map, Encode, State) -> + encode_sort_keys_map(Map, Encode, State). +-else. +encode_map(Map, Encode, #state{sort_keys = false, skip_values = ValuesToSkip} = State) -> + do_encode_map( + maps:fold( + fun(Key, Value, Acc) -> + case is_map_key(Value, ValuesToSkip) of + true -> + []; + false -> + [ + [ + $,, + escape_map_key(Key, State), + $: + | encode_term(Value, Encode, State) + ] + | Acc + ] + end + end, + [], + Map + ) + ); +encode_map(Map, Encode, State) -> + encode_sort_keys_map(Map, Encode, State). +-endif. + +encode_sort_keys_map(Map, Encode, #state{sort_keys = true} = State) -> do_encode_map([ [$,, escape_map_key(Key, State), $: | encode_term(Value, Encode, State)] || {Key, Value} <- lists:keysort(1, maps:to_list(Map)), - not is_map_key(Value, ValuesToSkip) + not is_map_key(Value, State#state.skip_values) ]). escape_map_key(Key, State) -> diff --git a/test/euneus_encoder_SUITE.erl b/test/euneus_encoder_SUITE.erl index bc6fd65..5d05ab3 100644 --- a/test/euneus_encoder_SUITE.erl +++ b/test/euneus_encoder_SUITE.erl @@ -3,6 +3,10 @@ -include_lib("stdlib/include/assert.hrl"). -compile([export_all, nowarn_export_all]). +% + +-elvis([{elvis_style, dont_repeat_yourself, disable}]). + %% -------------------------------------------------------------------- %% Behaviour (ct_suite) callbacks %% -------------------------------------------------------------------- @@ -86,6 +90,7 @@ ipv6_codec_test(Config) when is_list(Config) -> -record(foo, {foo, bar}). -record(bar, {bar, baz}). +-if(?OTP_RELEASE >= 26). records_codec_test(Config) when is_list(Config) -> [ ?assertError( @@ -116,6 +121,38 @@ records_codec_test(Config) when is_list(Config) -> ) ) ]. +-else. +records_codec_test(Config) when is_list(Config) -> + [ + ?assertError( + unsuported_tuple, + encode({foo, bar, baz, qux}, #{ + codecs => [ + {records, #{ + foo => {record_info(fields, foo), record_info(size, foo)} + }} + ] + }) + ), + ?assertEqual( + <<"[{\"foo\":\"foo\",\"bar\":\"bar\"},{\"baz\":\"baz\",\"bar\":\"bar\"}]">>, + encode( + [ + #foo{foo = foo, bar = bar}, + #bar{bar = bar, baz = baz} + ], + #{ + codecs => [ + {records, #{ + foo => {record_info(fields, foo), record_info(size, foo)}, + bar => {record_info(fields, bar), record_info(size, bar)} + }} + ] + } + ) + ) + ]. +-endif. nulls_test(Config) when is_list(Config) -> [ @@ -132,6 +169,7 @@ skip_values_test(Config) when is_list(Config) -> ) ]. +-if(?OTP_RELEASE >= 26). key_to_binary_test(Config) when is_list(Config) -> [ ?assertEqual( @@ -148,6 +186,24 @@ key_to_binary_test(Config) when is_list(Config) -> encode(#{foo => world}, #{key_to_binary => fun(_) -> <<"hello">> end}) ) ]. +-else. +key_to_binary_test(Config) when is_list(Config) -> + [ + ?assertEqual( + <<"{\"foo\":\"foo\",\"baz\":\"baz\",\"bar\":\"bar\",\"0\":0}">>, + encode(#{ + <<"foo">> => foo, + bar => bar, + "baz" => baz, + 0 => 0 + }) + ), + ?assertEqual( + <<"{\"hello\":\"world\"}">>, + encode(#{foo => world}, #{key_to_binary => fun(_) -> <<"hello">> end}) + ) + ]. +-endif. sort_keys_test(Config) when is_list(Config) -> [ @@ -158,6 +214,7 @@ sort_keys_test(Config) when is_list(Config) -> ) ]. +-if(?OTP_RELEASE >= 26). proplists_test(Config) when is_list(Config) -> [ ?assertEqual(<<"[]">>, encode([], #{proplists => true})), @@ -176,6 +233,26 @@ proplists_test(Config) when is_list(Config) -> }) ) ]. +-else. +proplists_test(Config) when is_list(Config) -> + [ + ?assertEqual(<<"[]">>, encode([], #{proplists => true})), + ?assertEqual( + <<"[null,{\"foo\":\"bar\",\"baz\":true,\"0\":0}]">>, + encode([null, [{foo, bar}, baz, {0, 0}]], #{proplists => true}) + ), + ?assertEqual( + <<"[null,{\"foo\":\"bar\"}]">>, + encode([null, [{foo, bar}]], #{ + proplists => + {true, fun + ([{_, _}]) -> true; + (_) -> false + end} + }) + ) + ]. +-endif. escape_test(Config) when is_list(Config) -> ?assertEqual(<<"bar">>, encode(foo, #{escape => fun(_) -> <<"bar">> end})).