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