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() :: proplists:proplist().
45
46-spec to_term(Source::binary(), Config::jsx_config:options()) -> jsx:json_term() | {incomplete, jsx:decoder()}.
47
48to_term(Source, Config) when is_list(Config) ->
49    (jsx:decoder(?MODULE, [return_maps] ++ Config, jsx_config:extract_config(Config)))(Source).
50
51parse_config(Config) -> parse_config(Config, #config{}).
52
53parse_config([{labels, Val}|Rest], Config)
54        when Val == binary; Val == atom; Val == existing_atom; Val == attempt_atom ->
55    parse_config(Rest, Config#config{labels = Val});
56parse_config([labels|Rest], Config) ->
57    parse_config(Rest, Config#config{labels = binary});
58parse_config([{return_maps, Val}|Rest], Config)
59        when Val == true; Val == false ->
60    parse_config(Rest, Config#config{return_maps = Val});
61parse_config([return_maps|Rest], Config) ->
62    parse_config(Rest, Config#config{return_maps = true});
63parse_config([{K, _}|Rest] = Options, Config) ->
64    case lists:member(K, jsx_config:valid_flags()) of
65        true -> parse_config(Rest, Config)
66        ; false -> erlang:error(badarg, [Options, Config])
67    end;
68parse_config([K|Rest] = Options, Config) ->
69    case lists:member(K, jsx_config:valid_flags()) of
70        true -> parse_config(Rest, Config)
71        ; false -> erlang:error(badarg, [Options, Config])
72    end;
73parse_config([], Config) ->
74    Config.
75
76
77-type state() :: {list(), #config{}}.
78-spec init(Config::config()) -> state().
79
80init(Config) -> start_term(Config).
81
82-spec handle_event(Event::any(), State::state()) -> state().
83
84handle_event(end_json, State) -> get_value(State);
85
86handle_event(start_object, State) -> start_object(State);
87handle_event(end_object, State) -> finish(State);
88
89handle_event(start_array, State) -> start_array(State);
90handle_event(end_array, State) -> finish(State);
91
92handle_event({key, Key}, {_, Config} = State) -> insert(format_key(Key, Config), State);
93
94handle_event({_, Event}, State) -> insert(Event, State).
95
96
97format_key(Key, Config) ->
98    case Config#config.labels of
99        binary -> Key
100        ; atom -> binary_to_atom(Key, utf8)
101        ; existing_atom -> binary_to_existing_atom(Key, utf8)
102        ; attempt_atom ->
103            try binary_to_existing_atom(Key, utf8) of
104                Result -> Result
105            catch
106                error:badarg -> Key
107            end
108    end.
109
110
111%% internal state is a stack and a config object
112%%  `{Stack, Config}`
113%% the stack is a list of in progress objects/arrays
114%%  `[Current, Parent, Grandparent,...OriginalAncestor]`
115%% an object has the representation on the stack of
116%%  `{object, [
117%%    {NthKey, NthValue},
118%%    {NMinus1Key, NthMinus1Value},
119%%    ...,
120%%    {FirstKey, FirstValue}
121%%  ]}`
122%% or if returning maps
123%%  `{object, #{
124%%    FirstKey => FirstValue,
125%%    SecondKey => SecondValue,
126%%    ...,
127%%    NthKey => NthValue
128%%  }}`
129%% or if there's a key with a yet to be matched value
130%%  `{object, Key, ...}`
131%% an array looks like
132%%  `{array, [NthValue, NthMinus1Value,...FirstValue]}`
133
134start_term(Config) when is_list(Config) -> {[], parse_config(Config)}.
135
136%% allocate a new object on top of the stack
137start_object({Stack, Config=#config{return_maps=true}}) ->
138    {[{object, #{}}] ++ Stack, Config};
139start_object({Stack, Config}) ->
140    {[{object, []}] ++ Stack, Config}.
141
142
143%% allocate a new array on top of the stack
144start_array({Stack, Config}) -> {[{array, []}] ++ Stack, Config}.
145
146
147%% finish an object or array and insert it into the parent object if it exists or
148%% return it if it is the root object
149finish({[{object, Map}], Config=#config{return_maps=true}}) -> {Map, Config};
150finish({[{object, Map}|Rest], Config=#config{return_maps=true}}) -> insert(Map, {Rest, Config});
151finish({[{object, []}], Config}) -> {[{}], Config};
152finish({[{object, []}|Rest], Config}) -> insert([{}], {Rest, Config});
153finish({[{object, Pairs}], Config}) -> {lists:reverse(Pairs), Config};
154finish({[{object, Pairs}|Rest], Config}) -> insert(lists:reverse(Pairs), {Rest, Config});
155finish({[{array, Values}], Config}) -> {lists:reverse(Values), Config};
156finish({[{array, Values}|Rest], Config}) -> insert(lists:reverse(Values), {Rest, Config});
157finish(_) -> erlang:error(badarg).
158
159
160%% insert a value when there's no parent object or array
161insert(Value, {[], Config}) -> {Value, Config};
162%% insert a key or value into an object or array, autodetects the 'right' thing
163insert(Key, {[{object, Map}|Rest], Config=#config{return_maps=true}}) ->
164    {[{object, Key, Map}] ++ Rest, Config};
165insert(Key, {[{object, Pairs}|Rest], Config}) ->
166    {[{object, Key, Pairs}] ++ Rest, Config};
167insert(Value, {[{object, Key, Map}|Rest], Config=#config{return_maps=true}}) ->
168    {[{object, maps:put(Key, Value, Map)}] ++ Rest, Config};
169insert(Value, {[{object, Key, Pairs}|Rest], Config}) ->
170    {[{object, [{Key, Value}] ++ Pairs}] ++ Rest, Config};
171insert(Value, {[{array, Values}|Rest], Config}) ->
172    {[{array, [Value] ++ Values}] ++ Rest, Config};
173insert(_, _) -> erlang:error(badarg).
174
175get_key({[{object, Key, _}|_], _}) -> Key;
176get_key(_) -> erlang:error(badarg).
177
178
179get_value({Value, _Config}) -> Value;
180get_value(_) -> erlang:error(badarg).
181
182
183
184%% eunit tests
185
186-ifdef(TEST).
187-include_lib("eunit/include/eunit.hrl").
188
189
190config_test_() ->
191    [
192        {"empty config", ?_assertEqual(#config{}, parse_config([]))},
193        {"implicit binary labels", ?_assertEqual(#config{}, parse_config([labels]))},
194        {"binary labels", ?_assertEqual(#config{}, parse_config([{labels, binary}]))},
195        {"atom labels", ?_assertEqual(#config{labels=atom}, parse_config([{labels, atom}]))},
196        {"existing atom labels", ?_assertEqual(
197            #config{labels=existing_atom},
198            parse_config([{labels, existing_atom}])
199        )},
200        {"return_maps true", ?_assertEqual(
201            #config{return_maps=true},
202            parse_config([return_maps])
203        )},
204        {"invalid opt flag", ?_assertError(badarg, parse_config([error]))},
205        {"invalid opt tuple", ?_assertError(badarg, parse_config([{error, true}]))}
206    ].
207
208
209format_key_test_() ->
210    [
211        {"binary key", ?_assertEqual(<<"key">>, format_key(<<"key">>, #config{labels=binary}))},
212        {"atom key", ?_assertEqual(key, format_key(<<"key">>, #config{labels=atom}))},
213        {"existing atom key", ?_assertEqual(
214            key,
215            format_key(<<"key">>, #config{labels=existing_atom})
216        )},
217        {"nonexisting atom key", ?_assertError(
218            badarg,
219            format_key(<<"nonexistentatom">>, #config{labels=existing_atom})
220        )},
221        {"sloppy existing atom key", ?_assertEqual(
222            key,
223            format_key(<<"key">>, #config{labels=attempt_atom})
224        )},
225        {"nonexisting atom key", ?_assertEqual(
226            <<"nonexistentatom">>,
227            format_key(<<"nonexistentatom">>, #config{labels=attempt_atom})
228        )}
229    ].
230
231
232rep_manipulation_test_() ->
233    [
234        {"allocate a new context with option", ?_assertEqual(
235            {[], #config{labels=atom}},
236            start_term([{labels, atom}])
237        )},
238        {"allocate a new object on an empty stack", ?_assertEqual(
239            {[{object, []}], #config{}},
240            start_object({[], #config{}})
241        )},
242        {"allocate a new object on a stack", ?_assertEqual(
243            {[{object, []}, {object, []}], #config{}},
244            start_object({[{object, []}], #config{}})
245        )},
246        {"allocate a new array on an empty stack", ?_assertEqual(
247            {[{array, []}], #config{}},
248            start_array({[], #config{}})
249        )},
250        {"allocate a new array on a stack", ?_assertEqual(
251            {[{array, []}, {object, []}], #config{}},
252            start_array({[{object, []}], #config{}})
253        )},
254        {"insert a key into an object", ?_assertEqual(
255            {[{object, key, []}, junk], #config{}},
256            insert(key, {[{object, []}, junk], #config{}})
257        )},
258        {"get current key", ?_assertEqual(
259            key,
260            get_key({[{object, key, []}], #config{}})
261        )},
262        {"try to get non-key from object", ?_assertError(
263            badarg,
264            get_key({[{object, []}], #config{}})
265        )},
266        {"try to get key from array", ?_assertError(
267            badarg,
268            get_key({[{array, []}], #config{}})
269        )},
270        {"insert a value into an object", ?_assertEqual(
271            {[{object, [{key, value}]}, junk], #config{}},
272            insert(value, {[{object, key, []}, junk], #config{}})
273        )},
274        {"insert a value into an array", ?_assertEqual(
275            {[{array, [value]}, junk], #config{}},
276            insert(value, {[{array, []}, junk], #config{}})
277        )},
278        {"finish an object with no ancestor", ?_assertEqual(
279            {[{a, b}, {x, y}], #config{}},
280            finish({[{object, [{x, y}, {a, b}]}], #config{}})
281        )},
282        {"finish an empty object", ?_assertEqual(
283            {[{}], #config{}},
284            finish({[{object, []}], #config{}})
285        )},
286        {"finish an object with an ancestor", ?_assertEqual(
287            {[{object, [{key, [{a, b}, {x, y}]}, {foo, bar}]}], #config{}},
288            finish({[{object, [{x, y}, {a, b}]}, {object, key, [{foo, bar}]}], #config{}})
289        )},
290        {"finish an array with no ancestor", ?_assertEqual(
291            {[a, b, c], #config{}},
292            finish({[{array, [c, b, a]}], #config{}})
293        )},
294        {"finish an array with an ancestor", ?_assertEqual(
295            {[{array, [[a, b, c], d, e, f]}], #config{}},
296            finish({[{array, [c, b, a]}, {array, [d, e, f]}], #config{}})
297        )}
298    ].
299
300
301rep_manipulation_with_maps_test_() ->
302    [
303        {"allocate a new object on an empty stack", ?_assertEqual(
304            {[{object, #{}}], #config{return_maps=true}},
305            start_object({[], #config{return_maps=true}})
306        )},
307        {"allocate a new object on a stack", ?_assertEqual(
308            {[{object, #{}}, {object, #{}}], #config{return_maps=true}},
309            start_object({[{object, #{}}], #config{return_maps=true}})
310        )},
311        {"insert a key into an object", ?_assertEqual(
312            {[{object, key, #{}}, junk], #config{return_maps=true}},
313            insert(key, {[{object, #{}}, junk], #config{return_maps=true}})
314        )},
315        {"get current key", ?_assertEqual(
316            key,
317            get_key({[{object, key, #{}}], #config{return_maps=true}})
318        )},
319        {"try to get non-key from object", ?_assertError(
320            badarg,
321            get_key({[{object, #{}}], #config{return_maps=true}})
322        )},
323        {"insert a value into an object", ?_assertEqual(
324            {[{object, #{key => value}}, junk], #config{return_maps=true}},
325            insert(value, {[{object, key, #{}}, junk], #config{return_maps=true}})
326        )},
327        {"finish an object with no ancestor", ?_assertEqual(
328            {#{a => b, x => y}, #config{return_maps=true}},
329            finish({[{object, #{x => y, a => b}}], #config{return_maps=true}})
330        )},
331        {"finish an empty object", ?_assertEqual(
332            {#{}, #config{return_maps=true}},
333            finish({[{object, #{}}], #config{return_maps=true}})
334        )},
335        {"finish an object with an ancestor", ?_assertEqual(
336            {
337                [{object, #{key => #{a => b, x => y}, foo => bar}}],
338                #config{return_maps=true}
339            },
340            finish({
341                [{object, #{x => y, a => b}}, {object, key, #{foo => bar}}],
342                #config{return_maps=true}
343            })
344        )}
345    ].
346
347
348return_maps_test_() ->
349    [
350        {"an empty map", ?_assertEqual(
351            #{},
352            jsx:decode(<<"{}">>, [])
353        )},
354        {"an empty map", ?_assertEqual(
355            #{},
356            jsx:decode(<<"{}">>, [])
357        )},
358        {"an empty map", ?_assertEqual(
359            [{}],
360            jsx:decode(<<"{}">>, [{return_maps, false}])
361        )},
362        {"a small map", ?_assertEqual(
363            #{<<"awesome">> => true, <<"library">> => <<"jsx">>},
364            jsx:decode(<<"{\"library\": \"jsx\", \"awesome\": true}">>, [])
365        )},
366        {"a recursive map", ?_assertEqual(
367            #{<<"key">> => #{<<"key">> => true}},
368            jsx:decode(<<"{\"key\": {\"key\": true}}">>, [])
369        )},
370        {"a map inside a list", ?_assertEqual(
371            [#{}],
372            jsx:decode(<<"[{}]">>, [])
373        )}
374    ].
375
376
377handle_event_test_() ->
378    Data = jsx:test_cases(),
379    [
380        {
381            Title, ?_assertEqual(
382                Term,
383                lists:foldl(fun handle_event/2, init([]), Events ++ [end_json])
384            )
385        } || {Title, _, Term, Events} <- Data
386    ].
387
388
389-endif.
390