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