aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--spec/practical/json_spec.cr128
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"))