diff options
| author | Matthew Hall <hallmatthew314@gmail.com> | 2023-03-27 23:41:37 +1300 |
|---|---|---|
| committer | Matthew Hall <hallmatthew314@gmail.com> | 2023-03-27 23:41:37 +1300 |
| commit | b6d48036b6ffa643eaaa6ce275e54c44aae342bb (patch) | |
| tree | eb719844de16ff2cacb45406085df7aff395692c /spec/practical | |
| parent | 7ce8225660994f0b353d57d994819efd04463c35 (diff) | |
Implement json_object
Diffstat (limited to 'spec/practical')
| -rw-r--r-- | spec/practical/json_spec.cr | 128 |
1 files changed, 114 insertions, 14 deletions
diff --git a/spec/practical/json_spec.cr b/spec/practical/json_spec.cr index fe31ea9..7a2875f 100644 --- a/spec/practical/json_spec.cr +++ b/spec/practical/json_spec.cr @@ -19,6 +19,14 @@ struct JSONValue end end +def assoc_to_hash(arr : Array({K, V})) : Hash(K, V) forall K, V + h = {} of K => V + arr.each do |k, v| + h[k] = v + end + h +end + describe "example: JSON parsing" do json_null = Parser.token_sequence("null".chars).map_const(JSONValue.new(nil)) @@ -60,30 +68,39 @@ describe "example: JSON parsing" do Parser.token_sequence(['\\', '"']).map_const('"'), Parser(Char, Char).satisfy(&.!=('"')), ]) - string_literal = (q >> s_char.many << q) - json_string = string_literal.map { |cs| JSONValue.new(cs.join) } - - json_array = uninitialized Parser(Char, JSONValue) - #json_object = uninitialized Parser(Char, JSONValue) - json_value = uninitialized Parser(Char, JSONValue) + string_literal = (q >> s_char.many.map(&.join) << q) + json_string = string_literal.map { |s| JSONValue.new(s) } - p_json_array = pointerof(json_array) - #p_json_object = pointerof(json_object) - p_json_value = pointerof(json_value) + # Forward-declared here to allow for mutual recursion + json_value = uninitialized Parser(Char, JSONValue) + p_json_value = pointerof(json_value) ws = Parser(Char, Char).satisfy(&.whitespace?).many - a_delim = ws >> Parser.token(',') >> ws + a_sep = ws >> Parser.token(',') >> ws a_front = Parser.token('[') >> ws a_back = ws >> Parser.token(']') json_array = Parser(Char, JSONValue).new("json_array") do |tokens| - # Has to be created and re-created here due to mutual recursion - # `a_delim.optional` matches a trailing comma - non_empty_elements = p_json_value.value.sep_by(a_delim) << a_delim.optional + # Has to be created and re-created here to allow mutual recursion + # `a_sep.optional` matches a trailing comma in this expression + non_empty_elements = p_json_value.value.sep_by(a_sep) << a_sep.optional elements = non_empty_elements.recover([] of JSONValue) p = (a_front >> elements << a_back).map { |es| JSONValue.new(es) } p.parse(tokens) end + kv_sep = ws >> Parser.token(':') >> ws + o_sep = ws >> Parser.token(',') >> ws + o_front = Parser.token('{') >> ws + o_back = ws >> Parser.token('}') + json_object = Parser(Char, JSONValue).new("json_object") do |tokens| + # Has to be created and re-created here to allow mutual recursion + kv_pair = string_literal + (kv_sep >> p_json_value.value) + # `a_sep.optional` matches a trailing comma in this expression + attributes = kv_pair.sep_by(o_sep) << o_sep.optional + p = o_front >> attributes.recover([] of {String, JSONValue}) << o_back + p.map { |pairs| JSONValue.new(assoc_to_hash(pairs)) }.parse(tokens) + end + json_value = Parser(Char, JSONValue).new("json_value") do |tokens| Parser(Char, JSONValue).first_of([ json_null, @@ -91,6 +108,7 @@ describe "example: JSON parsing" do json_number, json_string, json_array, + json_object, ]).parse(tokens) end @@ -212,7 +230,15 @@ describe "example: JSON parsing" do end it "can parse arrays with one element" do - {"[null]", "[ \n1]", "[0.123 \n]", "[true,]", "[false , ]", "[\"a string\"]"}.each do |s| + { + "[null]", + "[ \n1]", + "[0.123 \n]", + "[true,]", + "[false , ]", + "[\"a string\"]", + "[{\"hsrths\": 78.9}]" + }.each do |s| tokens = Tokens.from_string(s) result = json_array.parse(tokens) result.value.data.should be_a(Array(JSONValue)) @@ -258,6 +284,80 @@ describe "example: JSON parsing" do end end + describe "json_object" do + it "can parse empty objects" do + empty = JSONValue.new({} of String => JSONValue) + {"{}", "{ }", "{ \n\t\r}"}.each do |s| + tokens = Tokens.from_string(s) + result = json_object.parse(tokens) + result.value.should eq(empty) + result.tokens.empty?.should be_true + end + end + + it "can parse objects with one attribute" do + { + "{\"foo\":null}", + "{\"foo\": true,}", + "{\"foo\" :1}", + "{\"foo\" : 2.3 , }", + "{\"foo\" : \"bar\"}", + "{\"foo\"\t:\n[1], }", + }.each do |s| + tokens = Tokens.from_string(s) + result = json_object.parse(tokens) + result.value.data.should be_a(Hash(String, JSONValue)) + result.value.data.as(Hash(String, JSONValue)).size.should eq(1) + result.tokens.empty?.should be_true + end + end + + it "can parse objects with more than one attribute" do + { + "{\"foo\":null,\"bar\":false}", + "{ \"foo\" : 3.4 , \"bar\" : 1 , }", + "{\"foo\": \"some string\", \"bar\": [1, 2, 3] }", + }.each do |s| + tokens = Tokens.from_string(s) + result = json_object.parse(tokens) + result.value.data.should be_a(Hash(String, JSONValue)) + result.value.data.as(Hash(String, JSONValue)).size.>(1).should be_true + result.tokens.empty?.should be_true + end + end + + it "can parse nested objects" do + expected = JSONValue.new({ + "one" => JSONValue.new({"foo" => JSONValue.new(8_i64)}), + "two" => JSONValue.new(nil), + "three" => JSONValue.new({ + "foo" => JSONValue.new(7_i64), + "bar" => JSONValue.new([ + JSONValue.new(9_i64), + JSONValue.new(8_i64), + JSONValue.new(7_i64), + JSONValue.new(nil), + ]), + }), + }) + s = "{ + \"one\": {\"foo\": 8,}, + \"two\": null, + \"three\": {\"foo\": 7, \"bar\" : [9,8,7,null]}, +}" + tokens = Tokens.from_string(s) + result = json_object.parse(tokens) + result.value.should eq(expected) + result.tokens.empty?.should be_true + end + + it "does not allow trailing commas in empty objects" do + {"{,}", "{ , }"}.each do |s| + expect_raises(ParserFail) { json_object.parse(Tokens.from_string(s)) } + end + end + end + describe "json_value" do it "parses null" do result = json_value.parse(Tokens.from_string("null")) |
