diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..ceefb78 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,33 @@ +name: EUnit + +on: + pull_request: + branches: + - 'master' + push: + branches: + - 'master' + +jobs: + build: + name: Test on OTP ${{ matrix.otp_version }} and ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + strategy: + matrix: + # important to check a pre-23 version that still uses nodetool + # plus 23 once it is released + otp_version: [22.3.2] + os: [ubuntu-latest] + + steps: + - uses: actions/checkout@v2 + - uses: gleam-lang/setup-erlang@v1.0.0 + with: + otp-version: ${{ matrix.otp_version }} + + - name: compile + run: rebar3 compile + + - name: test + run: rebar3 eunit diff --git a/.gitignore b/.gitignore index 344bb12..332485a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.rebar3 .eunit deps ebin diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 468a670..0000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: erlang -script: rebar compile && rebar skip_deps=true eunit -otp_release: - - 22.0.7 - - 21.3.3 - - 20.3 - - 19.3 - - 18.3 - - 17.5 - - R16B03-1 diff --git a/CHANGES.md b/CHANGES.md index 8f404be..633a8a9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +v3.0 + +* drop support for OTP versions before 17.0 +* remove definition options for disabling maps globally, `{return_maps, false}` is still an accepted option to `decode/2` + v2.8.2 * enable `debug_info` for rebar3 diff --git a/README.md b/README.md index 1659553..f4e27ce 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,17 @@ -# jsx (v2.9.0) # +# jsx (v3.0.0) # an erlang application for consuming, producing and manipulating [json][json]. inspired by [yajl][yajl] -**jsx** is built via [rebar3][rebar3], [rebar][rebar] or [mix][mix] and continuous integration testing provided courtesy [travis-ci][travis] +**jsx** is built via [rebar3][rebar3] -current status: [![Build Status](https://secure.travis-ci.org/talentdeficit/jsx.png?branch=develop)](http://travis-ci.org/talentdeficit/jsx) +current status: ![](https://github.com/talentdeficit/jsx/workflows/EUnit/badge.svg) **jsx** is released under the terms of the [MIT][MIT] license copyright 2010-2016 alisdair sullivan -## really important note ## - -there are a few changes for users upgrading from 1.x. see [CHANGES.md](CHANGES.md) -for the overview or [migrating from 1.x](#migrating) for the details - - ## index ## * [quickstart](#quickstart) @@ -39,7 +33,6 @@ for the overview or [migrating from 1.x](#migrating) for the details - [`prettify/1`](#prettify1) - [`is_json/1,2`](#is_json12) - [`is_term/1,2`](#is_term12) - - [`maps_support/0`](#maps_support0) * [callback exports](#callback_exports) - [`Module:init/1`](#moduleinit1) - [`Module:handle_event/2`](#modulehandle_event2) @@ -55,7 +48,7 @@ Add to `rebar.config` {erl_opts, [debug_info]}. {deps, [ ... - {jsx, {git, "https://github.com/talentdeficit/jsx.git", {branch, "v2.8.0"}}} + {jsx, "~> 3.0"} ]}. ... ``` @@ -65,19 +58,15 @@ Add to `rebar.config` ```bash $ rebar3 compile $ rebar3 eunit -$ rebar compile -$ rebar eunit -$ mix compile -$ mix eunit ``` #### to convert a utf8 binary containing a json string into an erlang term #### ```erlang -1> jsx:decode(<<"{\"library\": \"jsx\", \"awesome\": true}">>). -[{<<"library">>,<<"jsx">>},{<<"awesome">>,true}] -2> jsx:decode(<<"{\"library\": \"jsx\", \"awesome\": true}">>, [return_maps]). +1> jsx:decode(<<"{\"library\": \"jsx\", \"awesome\": true}">>, []). #{<<"awesome">> => true,<<"library">> => <<"jsx">>} +2> jsx:decode(<<"{\"library\": \"jsx\", \"awesome\": true}">>, [{return_maps, false}]). +[{<<"library">>,<<"jsx">>},{<<"awesome">>,true}] 3> jsx:decode(<<"[\"a\",\"list\",\"of\",\"words\"]">>). [<<"a">>, <<"list">>, <<"of">>, <<"words">>] ``` @@ -85,10 +74,10 @@ $ mix eunit #### to convert an erlang term into a utf8 binary containing a json string #### ```erlang -1> jsx:encode([{<<"library">>,<<"jsx">>},{<<"awesome">>,true}]). -<<"{\"library\": \"jsx\", \"awesome\": true}">> -2> jsx:encode(#{<<"library">> => <<"jsx">>, <<"awesome">> => true}). +1> jsx:encode(#{<<"library">> => <<"jsx">>, <<"awesome">> => true}). <<"{\"awesome\":true,\"library\":\"jsx\"}">> +2> jsx:encode([{<<"library">>,<<"jsx">>},{<<"awesome">>,true}]). +<<"{\"library\": \"jsx\", \"awesome\": true}">> 3> jsx:encode([<<"a">>, <<"list">>, <<"of">>, <<"words">>]). <<"[\"a\",\"list\",\"of\",\"words\"]">> ``` @@ -132,13 +121,6 @@ false }">> ``` -#### to compile **jsx** so that it always decodes json objects to maps #### - -```bash -$ JSX_FORCE_MAPS rebar3 compile -$ JSX_FORCE_MAPS mix compile -``` - ## description ## @@ -172,29 +154,6 @@ quotes but must end with single quotes and must escape any single quotes they co json and **jsx** only recognize escape sequences as outlined in the json spec. it just ignores bad escape sequences leaving them in strings unaltered - -### migrating from 1.x ### - -if you're migrating from jsx v1.x to v2.x in most cases you won't need to -make any changes to your code - -support for otp 17.0's new map type is now enabled by default when compiling -via rebar for any release that supports them. jsx should still compile cleanly for -earlier releases without any user intervention - -if you used any of `replaced_bad_utf8`, `single_quoted_strings`, `comments`, -`ignored_bad_escapes` or `relax` you can simply omit them from your calls to jsx, -they are all enabled by default now. if you want stricter parsing see the new -[`strict` options](#option) available - -if you were using jsx to parse partial json using it's streaming features it is now -disabled by default. you'll need to pass the `stream` option to calls to jsx functions -to reenable it - -support for `pre_encode` and `post_decode` has been removed. they were fragile and hard -to understand and they prevented evolution of the encoding and decoding code - - ### json <-> erlang mapping ### **json** | **erlang** @@ -261,18 +220,7 @@ see below | `datetime()` * objects - json objects are represented by erlang proplists. json maps may also be - encoded to json and optionally decoded to maps (via the `return_maps` - option) - - the empty object has the special representation `[{}]` to differentiate it - from the empty list. ambiguities like `[true, false]` prevent the use of - the shorthand form of property lists using atoms as properties so all - properties must be tuples. all keys must be encoded as in `string` or as - atoms or integers (which will be escaped and converted to binaries for - presentation to handlers). values should be valid json values. repeated - keys are tolerated in json text decoded to erlang terms but are not allowed - in erlang terms encoded to json + json objects are represented by erlang maps. * datetime @@ -533,9 +481,8 @@ new atoms to the atom table and will result in a `badarg` error if the atom does not exist. `attempt_atom` will convert keys to atoms when they exist, and leave them as binary otherwise -the option `return_maps` will attempt to return objects as maps instead of -proplists. this option has no effect when used with releases that do not -support maps +the option `{return_maps, false}` will return objects as proplists instead +of maps. raises a `badarg` error exception if input is not valid json @@ -650,17 +597,6 @@ returns true if input is a valid erlang representation of json, false if not what exactly constitutes valid json may be altered via [options](#option) - -#### `maps_support/0` #### - -```erlang -maps_support() -> true | false -``` - -if **jsx** was compiled with map support enabled returns `true`, else -`false` - - ## callback exports ## the following functions should be exported from a **jsx** callback module @@ -753,8 +689,6 @@ jsx wouldn't be what it is without the contributions of [Paul J. Davis](https:// [yajl]: http://lloyd.github.com/yajl [MIT]: http://www.opensource.org/licenses/mit-license.html [rebar3]: https://rebar3.org -[rebar]: https://github.com/rebar/rebar -[mix]: http://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html [meck]: https://github.com/eproxus/meck [rfc4627]: http://tools.ietf.org/html/rfc4627 [travis]: https://travis-ci.org/ diff --git a/mix.exs b/mix.exs deleted file mode 100644 index c410fc0..0000000 --- a/mix.exs +++ /dev/null @@ -1,43 +0,0 @@ -defmodule JSX.Mixfile do -use Mix.Project - - def project do - [ - app: :jsx, - version: "2.11.0", - description: "an erlang application for consuming, producing and manipulating json. inspired by yajl", - deps: deps(Mix.env), - package: package(), - language: :erlang, - erlc_options: opts(Mix.env) - ] - end - - defp opts(:dev), do: [d: :TEST] ++ opts(:prod) - defp opts(_) do - force_maps = case System.get_env("JSX_FORCE_MAPS") do - nil -> [] - _ -> [d: :maps_always] - end - [:debug_info, d: :maps_support] ++ force_maps - end - - defp deps(_), do: [{:mixunit, "~> 0.9.2", only: :dev}] - - defp package do - [ - files: [ - "CHANGES.md", - "LICENSE", - "mix.exs", - "rebar.config", - "rebar.config.script", - "README.md", - "src" - ], - contributors: ["alisdair sullivan"], - links: %{"github" => "https://github.com/talentdeficit/jsx"}, - licenses: ["MIT"] - ] - end -end diff --git a/mix.lock b/mix.lock deleted file mode 100644 index ccf4987..0000000 --- a/mix.lock +++ /dev/null @@ -1 +0,0 @@ -%{"mixunit": {:hex, :mixunit, "0.9.2"}} diff --git a/rebar.config.script b/rebar.config.script deleted file mode 100644 index 5841b7d..0000000 --- a/rebar.config.script +++ /dev/null @@ -1,15 +0,0 @@ -Def0 = case erlang:is_builtin(erlang, binary_to_integer, 1) andalso - erlang:is_builtin(erlang, binary_to_float, 1) of - true -> []; - false -> [{d, no_binary_to_whatever}] - end, -Def1 = case erlang:is_builtin(erlang, is_map, 1) of - true -> [{d, maps_support}|Def0]; - false -> Def0 - end, -Defs = case os:getenv("JSX_FORCE_MAPS") of - false -> Def1; - _ -> [{d, maps_always}|Def1] - end, -lists:keystore(erl_opts, 1, CONFIG, - {erl_opts, proplists:get_value(erl_opts, CONFIG, []) ++ Defs}). diff --git a/src/jsx.app.src b/src/jsx.app.src index d78fdd7..52f6715 100644 --- a/src/jsx.app.src +++ b/src/jsx.app.src @@ -1,7 +1,7 @@ {application, jsx, [ {description, "a streaming, evented json parsing toolkit"}, - {vsn, "2.11.0"}, + {vsn, "3.0.0"}, {modules, [ jsx, jsx_encoder, @@ -18,11 +18,7 @@ stdlib ]}, {env, []}, - {files, [ - "src", - "rebar.config", "rebar.config.script", "rebar.lock" - "README.md", "CHANGES.md", "LICENSE" - ]}, + {licenses, ["MIT"]}, {links, [{"Github", "https://github.com/talentdeficit/jsx"}]} ]}. diff --git a/src/jsx.erl b/src/jsx.erl index 7560b28..1b4ceed 100644 --- a/src/jsx.erl +++ b/src/jsx.erl @@ -29,7 +29,6 @@ -export([consult/1, consult/2]). -export([encoder/3, decoder/3, parser/3]). -export([resume/3]). --export([maps_support/0]). -export_type([json_term/0, json_text/0, token/0]). -export_type([encoder/0, decoder/0, parser/0, internal_state/0]). @@ -42,18 +41,6 @@ -export([init/1, handle_event/2]). -endif. - --ifndef(maps_support). --type json_term() :: [{binary() | atom(), json_term()}] | [{},...] - | [json_term()] | [] - | {with_tail, json_term(), binary()} - | true | false | null - | integer() | float() - | binary() | atom() - | calendar:datetime(). --endif. - --ifdef(maps_support). -type json_term() :: [{binary() | atom(), json_term()}] | [{},...] | [json_term()] | [] | {with_tail, json_term(), binary()} @@ -62,7 +49,6 @@ | integer() | float() | binary() | atom() | calendar:datetime(). --endif. -type json_text() :: binary(). @@ -183,17 +169,6 @@ resume(Term, {decoder, State, Handler, Acc, Stack}, Config) -> resume(Term, {parser, State, Handler, Stack}, Config) -> jsx_parser:resume(Term, State, Handler, Stack, jsx_config:parse_config(Config)). - --spec maps_support() -> boolean(). - --ifndef(maps_support). -maps_support() -> false. --endif. --ifdef(maps_support). -maps_support() -> true. --endif. - - -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). diff --git a/src/jsx_consult.erl b/src/jsx_consult.erl index b1a4424..d25306a 100644 --- a/src/jsx_consult.erl +++ b/src/jsx_consult.erl @@ -35,18 +35,6 @@ -type config() :: list(). -export_type([config/0]). --ifndef(maps_support). --type json_value() :: list(json_value()) - | list({binary() | atom(), json_value()}) - | true - | false - | null - | integer() - | float() - | binary(). --endif. - --ifdef(maps_support). -type json_value() :: list(json_value()) | map() | true @@ -55,15 +43,8 @@ | integer() | float() | binary(). --endif. - --ifdef(maps_always). opts(Opts) -> [return_maps, multi_term] ++ Opts. --endif. --ifndef(maps_always). -opts(Opts) -> [multi_term] ++ Opts. --endif. -spec consult(File::file:name_all(), Config::config()) -> [json_value()]. diff --git a/src/jsx_decoder.erl b/src/jsx_decoder.erl index 9e0d205..7d92c0d 100644 --- a/src/jsx_decoder.erl +++ b/src/jsx_decoder.erl @@ -946,15 +946,8 @@ exp(_, N) -> {finish_float, N}. finish_number(Rest, Handler, Acc, Stack, Config) -> maybe_done(Rest, handle_event(format_number(Acc), Handler, Config), Stack, Config). - --ifndef(no_binary_to_whatever). format_number({integer, Acc}) -> {integer, binary_to_integer(Acc)}; format_number({float, Acc}) -> {float, binary_to_float(Acc)}. --else. -format_number({integer, Acc}) -> {integer, list_to_integer(unicode:characters_to_list(Acc))}; -format_number({float, Acc}) -> {float, list_to_float(unicode:characters_to_list(Acc))}. --endif. - true(<<$r, $u, $e, Rest/binary>>, Handler, Stack, Config) -> maybe_done(Rest, handle_event({literal, true}, Handler, Config), Stack, Config); @@ -1882,26 +1875,26 @@ custom_incomplete_handler_test_() -> return_tail_test_() -> [ {"return_tail with tail", ?_assertEqual( - {with_tail,[{}],<<"3">>}, + {with_tail,#{},<<"3">>}, jsx:decode(<<"{} 3">>, [return_tail]) )}, {"return_tail without tail", ?_assertEqual( - {with_tail,[{}],<<"">>}, + {with_tail,#{},<<"">>}, jsx:decode(<<"{}">>, [return_tail]) )}, {"return_tail with trimmed whitespace", ?_assertEqual( - {with_tail,[{}],<<"">>}, + {with_tail,#{},<<"">>}, jsx:decode(<<"{} ">>, [return_tail]) )}, {"return_tail and streaming", ?_assertEqual( - {with_tail,[{}],<<"3">>}, + {with_tail,#{},<<"3">>}, begin {incomplete, F} = jsx:decode(<<"{">>, [return_tail, stream]), F(<<"} 3">>) end )}, {"return_tail and streaming", ?_assertEqual( - {with_tail,[{}],<<"">>}, + {with_tail,#{},<<"">>}, begin %% In case of infinite stream of objects a user does not know %% when to call F(end_stream). diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index 39140d8..c6db8ef 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -39,17 +39,11 @@ encode(Term) -> encode(Term, ?MODULE). -spec encode(Term::any(), EntryPoint::module()) -> any(). --ifndef(maps_support). -encode(Term, EntryPoint) -> encode_(Term, EntryPoint). --endif. - --ifdef(maps_support). encode(Map, _EntryPoint) when is_map(Map), map_size(Map) < 1 -> [start_object, end_object]; encode(Term, EntryPoint) when is_map(Term) -> [start_object] ++ unpack(Term, EntryPoint); encode(Term, EntryPoint) -> encode_(Term, EntryPoint). --endif. encode_([], _EntryPoint) -> [start_array, end_array]; encode_([{}], _EntryPoint) -> [start_object, end_object]; @@ -75,16 +69,11 @@ unhitch([V|Rest], EntryPoint) -> EntryPoint:encode(V, EntryPoint) ++ unhitch(Rest, EntryPoint); unhitch([], _) -> [end_array]. - --ifdef(maps_support). unpack(Map, EntryPoint) -> unpack(Map, maps:keys(Map), EntryPoint). unpack(Map, [K|Rest], EntryPoint) when is_integer(K); is_binary(K); is_atom(K) -> [K] ++ EntryPoint:encode(maps:get(K, Map), EntryPoint) ++ unpack(Map, Rest, EntryPoint); unpack(_, [], _) -> [end_object]. --endif. - - -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). diff --git a/src/jsx_to_term.erl b/src/jsx_to_term.erl index 2f06913..e3c99e7 100644 --- a/src/jsx_to_term.erl +++ b/src/jsx_to_term.erl @@ -44,19 +44,6 @@ -type config() :: list(). -export_type([config/0]). --ifndef(maps_support). --type json_value() :: list(json_value()) - | list({binary() | atom(), json_value()}) | [{},...] - | {with_tail, json_value(), binary()} - | true - | false - | null - | integer() - | float() - | binary(). --endif. - --ifdef(maps_support). -type json_value() :: list(json_value()) | list({binary() | atom(), json_value()}) | [{},...] | {with_tail, json_value(), binary()} @@ -67,19 +54,11 @@ | integer() | float() | binary(). --endif. - -spec to_term(Source::binary(), Config::config()) -> json_value(). --ifdef(maps_always). to_term(Source, Config) when is_list(Config) -> (jsx:decoder(?MODULE, [return_maps] ++ Config, jsx_config:extract_config(Config)))(Source). --endif. --ifndef(maps_always). -to_term(Source, Config) when is_list(Config) -> - (jsx:decoder(?MODULE, Config, jsx_config:extract_config(Config)))(Source). --endif. parse_config(Config) -> parse_config(Config, #config{}). @@ -166,41 +145,6 @@ format_key(Key, Config) -> start_term(Config) when is_list(Config) -> {[], parse_config(Config)}. - --ifndef(maps_support). -%% allocate a new object on top of the stack -start_object({Stack, Config}) -> {[{object, []}] ++ Stack, Config}. - - -%% allocate a new array on top of the stack -start_array({Stack, Config}) -> {[{array, []}] ++ Stack, Config}. - - -%% finish an object or array and insert it into the parent object if it exists or -%% return it if it is the root object -finish({[{object, []}], Config}) -> {[{}], Config}; -finish({[{object, []}|Rest], Config}) -> insert([{}], {Rest, Config}); -finish({[{object, Pairs}], Config}) -> {lists:reverse(Pairs), Config}; -finish({[{object, Pairs}|Rest], Config}) -> insert(lists:reverse(Pairs), {Rest, Config}); -finish({[{array, Values}], Config}) -> {lists:reverse(Values), Config}; -finish({[{array, Values}|Rest], Config}) -> insert(lists:reverse(Values), {Rest, Config}); -finish(_) -> erlang:error(badarg). - - -%% insert a value when there's no parent object or array -insert(Value, {[], Config}) -> {Value, Config}; -%% insert a key or value into an object or array, autodetects the 'right' thing -insert(Key, {[{object, Pairs}|Rest], Config}) -> - {[{object, Key, Pairs}] ++ Rest, Config}; -insert(Value, {[{object, Key, Pairs}|Rest], Config}) -> - {[{object, [{Key, Value}] ++ Pairs}] ++ Rest, Config}; -insert(Value, {[{array, Values}|Rest], Config}) -> - {[{array, [Value] ++ Values}] ++ Rest, Config}; -insert(_, _) -> erlang:error(badarg). --endif. - - --ifdef(maps_support). %% allocate a new object on top of the stack start_object({Stack, Config=#config{return_maps=true}}) -> {[{object, #{}}] ++ Stack, Config}; @@ -239,8 +183,6 @@ insert(Value, {[{object, Key, Pairs}|Rest], Config}) -> insert(Value, {[{array, Values}|Rest], Config}) -> {[{array, [Value] ++ Values}] ++ Rest, Config}; insert(_, _) -> erlang:error(badarg). --endif. - get_key({[{object, Key, _}|_], _}) -> Key; get_key(_) -> erlang:error(badarg). @@ -368,7 +310,6 @@ rep_manipulation_test_() -> ]. --ifdef(maps_support). rep_manipulation_with_maps_test_() -> [ {"allocate a new object on an empty stack", ?_assertEqual( @@ -420,10 +361,10 @@ return_maps_test_() -> [ {"an empty map", ?_assertEqual( #{}, - jsx:decode(<<"{}">>, [return_maps]) + jsx:decode(<<"{}">>, []) )}, {"an empty map", ?_assertEqual( - [{}], + #{}, jsx:decode(<<"{}">>, []) )}, {"an empty map", ?_assertEqual( @@ -432,18 +373,17 @@ return_maps_test_() -> )}, {"a small map", ?_assertEqual( #{<<"awesome">> => true, <<"library">> => <<"jsx">>}, - jsx:decode(<<"{\"library\": \"jsx\", \"awesome\": true}">>, [return_maps]) + jsx:decode(<<"{\"library\": \"jsx\", \"awesome\": true}">>, []) )}, {"a recursive map", ?_assertEqual( #{<<"key">> => #{<<"key">> => true}}, - jsx:decode(<<"{\"key\": {\"key\": true}}">>, [return_maps]) + jsx:decode(<<"{\"key\": {\"key\": true}}">>, []) )}, {"a map inside a list", ?_assertEqual( [#{}], - jsx:decode(<<"[{}]">>, [return_maps]) + jsx:decode(<<"[{}]">>, []) )} ]. --endif. handle_event_test_() ->