1%% The MIT License
2
3%% Copyright (c) 2010-2013 Alisdair Sullivan <alisdairsullivan@yahoo.ca>
4
5%% Permission is hereby granted, free of charge, to any person obtaining a copy
6%% of this software and associated documentation files (the "Software"), to deal
7%% in the Software without restriction, including without limitation the rights
8%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9%% copies of the Software, and to permit persons to whom the Software is
10%% furnished to do so, subject to the following conditions:
11
12%% The above copyright notice and this permission notice shall be included in
13%% all copies or substantial portions of the Software.
14
15%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21%% THE SOFTWARE.
22
23
24-module(jsx_to_term).
25
26-export([to_term/2]).
27-export([init/1, handle_event/2]).
28-export([
29    start_term/1,
30    start_object/1,
31    start_array/1,
32    finish/1,
33    insert/2,
34    get_key/1,
35    get_value/1
36]).
37
38
39-record(config, {
40    labels = binary,
41    return_maps = false
42}).
43
44-type config() :: list().
45-export_type([config/0]).
46
47-ifndef(maps_support).
48-type json_value() :: list(json_value())
49    | list({binary() | atom(), json_value()}) | [{},...]
50    | true
51    | false
52    | null
53    | integer()
54    | float()
55    | binary().
56-endif.
57
58-ifdef(maps_support).
59-type json_value() :: list(json_value())
60    | list({binary() | atom(), json_value()}) | [{},...]
61    | map()
62    | true
63    | false
64    | null
65    | integer()
66    | float()
67    | binary().
68-endif.
69
70
71-spec to_term(Source::binary(), Config::config()) -> json_value().
72
73-ifdef(maps_always).
74to_term(Source, Config) when is_list(Config) ->
75    (jsx:decoder(?MODULE, [return_maps] ++ Config, jsx_config:extract_config(Config)))(Source).
76-endif.
77-ifndef(maps_always).
78to_term(Source, Config) when is_list(Config) ->
79    (jsx:decoder(?MODULE, Config, jsx_config:extract_config(Config)))(Source).
80-endif.
81
82parse_config(Config) -> parse_config(Config, #config{}).
83
84parse_config([{labels, Val}|Rest], Config)
85        when Val == binary; Val == atom; Val == existing_atom; Val == attempt_atom ->
86    parse_config(Rest, Config#config{labels = Val});
87parse_config([labels|Rest], Config) ->
88    parse_config(Rest, Config#config{labels = binary});
89parse_config([{return_maps, Val}|Rest], Config)
90        when Val == true; Val == false ->
91    parse_config(Rest, Config#config{return_maps = Val});
92parse_config([return_maps|Rest], Config) ->
93    parse_config(Rest, Config#config{return_maps = true});
94parse_config([{K, _}|Rest] = Options, Config) ->
95    case lists:member(K, jsx_config:valid_flags()) of
96        true -> parse_config(Rest, Config)
97        ; false -> erlang:error(badarg, [Options, Config])
98    end;
99parse_config([K|Rest] = Options, Config) ->
100    case lists:member(K, jsx_config:valid_flags()) of
101        true -> parse_config(Rest, Config)
102        ; false -> erlang:error(badarg, [Options, Config])
103    end;
104parse_config([], Config) ->
105    Config.
106
107
108-type state() :: {list(), #config{}}.
109-spec init(Config::proplists:proplist()) -> state().
110
111init(Config) -> start_term(Config).
112
113-spec handle_event(Event::any(), State::state()) -> state().
114
115handle_event(end_json, State) -> get_value(State);
116
117handle_event(start_object, State) -> start_object(State);
118handle_event(end_object, State) -> finish(State);
119
120handle_event(start_array, State) -> start_array(State);
121handle_event(end_array, State) -> finish(State);
122
123handle_event({key, Key}, {_, Config} = State) -> insert(format_key(Key, Config), State);
124
125handle_event({_, Event}, State) -> insert(Event, State).
126
127
128format_key(Key, Config) ->
129    case Config#config.labels of
130        binary -> Key
131        ; atom -> binary_to_atom(Key, utf8)
132        ; existing_atom -> binary_to_existing_atom(Key, utf8)
133        ; attempt_atom ->
134            try binary_to_existing_atom(Key, utf8) of
135                Result -> Result
136            catch
137                error:badarg -> Key
138            end
139    end.
140
141
142%% internal state is a stack and a config object
143%%  `{Stack, Config}`
144%% the stack is a list of in progress objects/arrays
145%%  `[Current, Parent, Grandparent,...OriginalAncestor]`
146%% an object has the representation on the stack of
147%%  `{object, [
148%%    {NthKey, NthValue},
149%%    {NMinus1Key, NthMinus1Value},
150%%    ...,
151%%    {FirstKey, FirstValue}
152%%  ]}`
153%% or if returning maps
154%%  `{object, #{
155%%    FirstKey => FirstValue,
156%%    SecondKey => SecondValue,
157%%    ...,
158%%    NthKey => NthValue
159%%  }}`
160%% or if there's a key with a yet to be matched value
161%%  `{object, Key, ...}`
162%% an array looks like
163%%  `{array, [NthValue, NthMinus1Value,...FirstValue]}`
164
165start_term(Config) when is_list(Config) -> {[], parse_config(Config)}.
166
167
168-ifndef(maps_support).
169%% allocate a new object on top of the stack
170start_object({Stack, Config}) -> {[{object, []}] ++ Stack, Config}.
171
172
173%% allocate a new array on top of the stack
174start_array({Stack, Config}) -> {[{array, []}] ++ Stack, Config}.
175
176
177%% finish an object or array and insert it into the parent object if it exists or
178%% return it if it is the root object
179finish({[{object, []}], Config}) -> {[{}], Config};
180finish({[{object, []}|Rest], Config}) -> insert([{}], {Rest, Config});
181finish({[{object, Pairs}], Config}) -> {lists:reverse(Pairs), Config};
182finish({[{object, Pairs}|Rest], Config}) -> insert(lists:reverse(Pairs), {Rest, Config});
183finish({[{array, Values}], Config}) -> {lists:reverse(Values), Config};
184finish({[{array, Values}|Rest], Config}) -> insert(lists:reverse(Values), {Rest, Config});
185finish(_) -> erlang:error(badarg).
186
187
188%% insert a value when there's no parent object or array
189insert(Value, {[], Config}) -> {Value, Config};
190%% insert a key or value into an object or array, autodetects the 'right' thing
191insert(Key, {[{object, Pairs}|Rest], Config}) ->
192    {[{object, Key, Pairs}] ++ Rest, Config};
193insert(Value, {[{object, Key, Pairs}|Rest], Config}) ->
194    {[{object, [{Key, Value}] ++ Pairs}] ++ Rest, Config};
195insert(Value, {[{array, Values}|Rest], Config}) ->
196    {[{array, [Value] ++ Values}] ++ Rest, Config};
197insert(_, _) -> erlang:error(badarg).
198-endif.
199
200
201-ifdef(maps_support).
202%% allocate a new object on top of the stack
203start_object({Stack, Config=#config{return_maps=true}}) ->
204    {[{object, #{}}] ++ Stack, Config};
205start_object({Stack, Config}) ->
206    {[{object, []}] ++ Stack, Config}.
207
208
209%% allocate a new array on top of the stack
210start_array({Stack, Config}) -> {[{array, []}] ++ Stack, Config}.
211
212
213%% finish an object or array and insert it into the parent object if it exists or
214%% return it if it is the root object
215finish({[{object, Map}], Config=#config{return_maps=true}}) -> {Map, Config};
216finish({[{object, Map}|Rest], Config=#config{return_maps=true}}) -> insert(Map, {Rest, Config});
217finish({[{object, []}], Config}) -> {[{}], Config};
218finish({[{object, []}|Rest], Config}) -> insert([{}], {Rest, Config});
219finish({[{object, Pairs}], Config}) -> {lists:reverse(Pairs), Config};
220finish({[{object, Pairs}|Rest], Config}) -> insert(lists:reverse(Pairs), {Rest, Config});
221finish({[{array, Values}], Config}) -> {lists:reverse(Values), Config};
222finish({[{array, Values}|Rest], Config}) -> insert(lists:reverse(Values), {Rest, Config});
223finish(_) -> erlang:error(badarg).
224
225
226%% insert a value when there's no parent object or array
227insert(Value, {[], Config}) -> {Value, Config};
228%% insert a key or value into an object or array, autodetects the 'right' thing
229insert(Key, {[{object, Map}|Rest], Config=#config{return_maps=true}}) ->
230    {[{object, Key, Map}] ++ Rest, Config};
231insert(Key, {[{object, Pairs}|Rest], Config}) ->
232    {[{object, Key, Pairs}] ++ Rest, Config};
233insert(Value, {[{object, Key, Map}|Rest], Config=#config{return_maps=true}}) ->
234    {[{object, maps:put(Key, Value, Map)}] ++ Rest, Config};
235insert(Value, {[{object, Key, Pairs}|Rest], Config}) ->
236    {[{object, [{Key, Value}] ++ Pairs}] ++ Rest, Config};
237insert(Value, {[{array, Values}|Rest], Config}) ->
238    {[{array, [Value] ++ Values}] ++ Rest, Config};
239insert(_, _) -> erlang:error(badarg).
240-endif.
241
242
243get_key({[{object, Key, _}|_], _}) -> Key;
244get_key(_) -> erlang:error(badarg).
245
246
247get_value({Value, _Config}) -> Value;
248get_value(_) -> erlang:error(badarg).
249
250
251
252%% eunit tests
253
254-ifdef(TEST).
255-include_lib("eunit/include/eunit.hrl").
256
257
258config_test_() ->
259    [
260        {"empty config", ?_assertEqual(#config{}, parse_config([]))},
261        {"implicit binary labels", ?_assertEqual(#config{}, parse_config([labels]))},
262        {"binary labels", ?_assertEqual(#config{}, parse_config([{labels, binary}]))},
263        {"atom labels", ?_assertEqual(#config{labels=atom}, parse_config([{labels, atom}]))},
264        {"existing atom labels", ?_assertEqual(
265            #config{labels=existing_atom},
266            parse_config([{labels, existing_atom}])
267        )},
268        {"return_maps true", ?_assertEqual(
269            #config{return_maps=true},
270            parse_config([return_maps])
271        )},
272        {"invalid opt flag", ?_assertError(badarg, parse_config([error]))},
273        {"invalid opt tuple", ?_assertError(badarg, parse_config([{error, true}]))}
274    ].
275
276
277format_key_test_() ->
278    [
279        {"binary key", ?_assertEqual(<<"key">>, format_key(<<"key">>, #config{labels=binary}))},
280        {"atom key", ?_assertEqual(key, format_key(<<"key">>, #config{labels=atom}))},
281        {"existing atom key", ?_assertEqual(
282            key,
283            format_key(<<"key">>, #config{labels=existing_atom})
284        )},
285        {"nonexisting atom key", ?_assertError(
286            badarg,
287            format_key(<<"nonexistentatom">>, #config{labels=existing_atom})
288        )},
289        {"sloppy existing atom key", ?_assertEqual(
290            key,
291            format_key(<<"key">>, #config{labels=attempt_atom})
292        )},
293        {"nonexisting atom key", ?_assertEqual(
294            <<"nonexistentatom">>,
295            format_key(<<"nonexistentatom">>, #config{labels=attempt_atom})
296        )}
297    ].
298
299
300rep_manipulation_test_() ->
301    [
302        {"allocate a new context with option", ?_assertEqual(
303            {[], #config{labels=atom}},
304            start_term([{labels, atom}])
305        )},
306        {"allocate a new object on an empty stack", ?_assertEqual(
307            {[{object, []}], #config{}},
308            start_object({[], #config{}})
309        )},
310        {"allocate a new object on a stack", ?_assertEqual(
311            {[{object, []}, {object, []}], #config{}},
312            start_object({[{object, []}], #config{}})
313        )},
314        {"allocate a new array on an empty stack", ?_assertEqual(
315            {[{array, []}], #config{}},
316            start_array({[], #config{}})
317        )},
318        {"allocate a new array on a stack", ?_assertEqual(
319            {[{array, []}, {object, []}], #config{}},
320            start_array({[{object, []}], #config{}})
321        )},
322        {"insert a key into an object", ?_assertEqual(
323            {[{object, key, []}, junk], #config{}},
324            insert(key, {[{object, []}, junk], #config{}})
325        )},
326        {"get current key", ?_assertEqual(
327            key,
328            get_key({[{object, key, []}], #config{}})
329        )},
330        {"try to get non-key from object", ?_assertError(
331            badarg,
332            get_key({[{object, []}], #config{}})
333        )},
334        {"try to get key from array", ?_assertError(
335            badarg,
336            get_key({[{array, []}], #config{}})
337        )},
338        {"insert a value into an object", ?_assertEqual(
339            {[{object, [{key, value}]}, junk], #config{}},
340            insert(value, {[{object, key, []}, junk], #config{}})
341        )},
342        {"insert a value into an array", ?_assertEqual(
343            {[{array, [value]}, junk], #config{}},
344            insert(value, {[{array, []}, junk], #config{}})
345        )},
346        {"finish an object with no ancestor", ?_assertEqual(
347            {[{a, b}, {x, y}], #config{}},
348            finish({[{object, [{x, y}, {a, b}]}], #config{}})
349        )},
350        {"finish an empty object", ?_assertEqual(
351            {[{}], #config{}},
352            finish({[{object, []}], #config{}})
353        )},
354        {"finish an object with an ancestor", ?_assertEqual(
355            {[{object, [{key, [{a, b}, {x, y}]}, {foo, bar}]}], #config{}},
356            finish({[{object, [{x, y}, {a, b}]}, {object, key, [{foo, bar}]}], #config{}})
357        )},
358        {"finish an array with no ancestor", ?_assertEqual(
359            {[a, b, c], #config{}},
360            finish({[{array, [c, b, a]}], #config{}})
361        )},
362        {"finish an array with an ancestor", ?_assertEqual(
363            {[{array, [[a, b, c], d, e, f]}], #config{}},
364            finish({[{array, [c, b, a]}, {array, [d, e, f]}], #config{}})
365        )}
366    ].
367
368
369-ifdef(maps_support).
370rep_manipulation_with_maps_test_() ->
371    [
372        {"allocate a new object on an empty stack", ?_assertEqual(
373            {[{object, #{}}], #config{return_maps=true}},
374            start_object({[], #config{return_maps=true}})
375        )},
376        {"allocate a new object on a stack", ?_assertEqual(
377            {[{object, #{}}, {object, #{}}], #config{return_maps=true}},
378            start_object({[{object, #{}}], #config{return_maps=true}})
379        )},
380        {"insert a key into an object", ?_assertEqual(
381            {[{object, key, #{}}, junk], #config{return_maps=true}},
382            insert(key, {[{object, #{}}, junk], #config{return_maps=true}})
383        )},
384        {"get current key", ?_assertEqual(
385            key,
386            get_key({[{object, key, #{}}], #config{return_maps=true}})
387        )},
388        {"try to get non-key from object", ?_assertError(
389            badarg,
390            get_key({[{object, #{}}], #config{return_maps=true}})
391        )},
392        {"insert a value into an object", ?_assertEqual(
393            {[{object, #{key => value}}, junk], #config{return_maps=true}},
394            insert(value, {[{object, key, #{}}, junk], #config{return_maps=true}})
395        )},
396        {"finish an object with no ancestor", ?_assertEqual(
397            {#{a => b, x => y}, #config{return_maps=true}},
398            finish({[{object, #{x => y, a => b}}], #config{return_maps=true}})
399        )},
400        {"finish an empty object", ?_assertEqual(
401            {#{}, #config{return_maps=true}},
402            finish({[{object, #{}}], #config{return_maps=true}})
403        )},
404        {"finish an object with an ancestor", ?_assertEqual(
405            {
406                [{object, #{key => #{a => b, x => y}, foo => bar}}],
407                #config{return_maps=true}
408            },
409            finish({
410                [{object, #{x => y, a => b}}, {object, key, #{foo => bar}}],
411                #config{return_maps=true}
412            })
413        )}
414    ].
415
416
417return_maps_test_() ->
418    [
419        {"an empty map", ?_assertEqual(
420            #{},
421            jsx:decode(<<"{}">>, [return_maps])
422        )},
423        {"an empty map", ?_assertEqual(
424            [{}],
425            jsx:decode(<<"{}">>, [])
426        )},
427        {"an empty map", ?_assertEqual(
428            [{}],
429            jsx:decode(<<"{}">>, [{return_maps, false}])
430        )},
431        {"a small map", ?_assertEqual(
432            #{<<"awesome">> => true, <<"library">> => <<"jsx">>},
433            jsx:decode(<<"{\"library\": \"jsx\", \"awesome\": true}">>, [return_maps])
434        )},
435        {"a recursive map", ?_assertEqual(
436            #{<<"key">> => #{<<"key">> => true}},
437            jsx:decode(<<"{\"key\": {\"key\": true}}">>, [return_maps])
438        )},
439        {"a map inside a list", ?_assertEqual(
440            [#{}],
441            jsx:decode(<<"[{}]">>, [return_maps])
442        )}
443    ].
444-endif.
445
446
447handle_event_test_() ->
448    Data = jsx:test_cases(),
449    [
450        {
451            Title, ?_assertEqual(
452                Term,
453                lists:foldl(fun handle_event/2, init([]), Events ++ [end_json])
454            )
455        } || {Title, _, Term, Events} <- Data
456    ].
457
458
459-endif.
460