require "../spec_helper" include Parcom alias JSONType = Nil \ | Bool \ | Int64 \ | Float64 \ | String \ | Array(JSONValue) \ | Hash(String, JSONValue) struct JSONValue property data : JSONType def initialize(@data) 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 def float_or_int(x : Float64) : Float64 | Int64 (x % 1).zero? ? x.to_i64 : x end describe "example: JSON parsing", tags: "example" do json_null = Parser.token_sequence("null".chars) .map_const(JSONValue.new(nil)) .named("json_null") t = Parser.token_sequence("true".chars).map_const(true) f = Parser.token_sequence("false".chars).map_const(false) json_bool = (t | f).map { |b| JSONValue.new(b) }.named("json_bool") digits = Parser(Char, Char).satisfy(&.number?).many sign = Parser.token('-').map_const(-1).recover(1) point = Parser.token('.').optional json_number = parser_chain Char, JSONValue, "json_number", {s, sign}, {front, digits}, {p, point}, {back, digits}, make: case {front.empty?, p.nil?, back.empty?} when {true, false, false} Parser(Char, Float64).pure("0.#{back.join}".to_f64) when {false, _, true} Parser(Char, Float64).pure(front.join.to_f64) when {false, false, false} Parser(Char, Float64).pure("#{front.join}.#{back.join}".to_f64) else Parser(Char, Float64).flunk("Could not parse '#{front.join}#{p}#{back.join}' as a number") end.map { |x| JSONValue.new(float_or_int(x) * s) } q = Parser.token('"') s_char = Parser(Char, Char).first_of([ Parser.token_sequence(['\\', '"']).map_const('"'), Parser(Char, Char).satisfy(&.!=('"')), ]) string_literal = (q >> s_char.many.map(&.join) << q) json_string = string_literal.map { |s| JSONValue.new(s) }.named("json_string") # 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_sep = ws >> Parser.token(',') >> ws a_front = Parser.token('[') >> ws a_back = ws >> Parser.token(']') # Has to be created this way to allow for mutual recursion non_empty_elements = Parser(Char, Array(JSONValue)).new("non_empty_elements") do |tokens| # `a_sep.optional` matches a trailing comma in this expression (p_json_value.value.sep_by(a_sep) << a_sep.optional).parse(tokens) end elements = non_empty_elements.recover([] of JSONValue) json_array = (a_front >> elements << a_back).map { |es| JSONValue.new(es) } .named("json_value") kv_sep = ws >> Parser.token(':') >> ws # Has to be created this way to allow for mutual recursion kv_pair = Parser(Char, {String, JSONValue}).new("kv_pair") do |tokens| (string_literal + (kv_sep >> p_json_value.value)).parse(tokens) end o_sep = ws >> Parser.token(',') >> ws o_front = Parser.token('{') >> ws o_back = ws >> Parser.token('}') # `a_sep.optional` matches a trailing comma in this expression non_empty_attributes = kv_pair.sep_by(o_sep) << o_sep.optional o_attributes = non_empty_attributes.recover([] of {String, JSONValue}) json_object = (o_front >> o_attributes << o_back).map do |pairs| JSONValue.new(assoc_to_hash(pairs)) end.named("json_object") json_value = Parser(Char, JSONValue).new("json_value") do |tokens| Parser(Char, JSONValue).first_of([ json_null, json_bool, json_number, json_string, json_array, json_object, ]).parse(tokens) end describe "json_null" do it "parses the string 'null' and returns nil when successful" do result = json_null.parse(Tokens.from_string("null")) result.value.data.should be_nil result.tokens.empty?.should be_true expect_raises(ParserFail) { json_null.parse(Tokens.from_string("")) } end end describe "json_bool" do it "parses the strings 'true' or 'false' and returns bool" do result = json_bool.parse(Tokens.from_string("true")) result.value.data.should be_true result.tokens.empty?.should be_true result = json_bool.parse(Tokens.from_string("false")) result.value.data.should be_false result.tokens.empty?.should be_true expect_raises(ParserFail) { json_bool.parse(Tokens.from_string("")) } end end describe "json_number" do it "parses positive and negative integers" do {"42", "42.", "42.000000"}.each do |s| result = json_number.parse(Tokens.from_string(s)) result.value.data.should eq(42) result.value.data.should be_a(Int64) result.tokens.empty?.should be_true result = json_number.parse(Tokens.from_string("-#{s}")) result.value.data.should eq(-42) result.value.data.should be_a(Int64) result.tokens.empty?.should be_true end end it "parses positive and negative floats between -1 and 1" do {"0.1234", ".1234", ".12340000"}.each do |s| result = json_number.parse(Tokens.from_string(s)) result.value.data.should eq(0.1234) result.value.data.should be_a(Float64) result.tokens.empty?.should be_true result = json_number.parse(Tokens.from_string("-#{s}")) result.value.data.should eq(-0.1234) result.value.data.should be_a(Float64) result.tokens.empty?.should be_true end end it "parses positive and negative floats with a whole-number component" do {"12.34", "0012.3400"}.each do |s| result = json_number.parse(Tokens.from_string(s)) result.value.data.should eq(12.34) result.value.data.should be_a(Float64) result.tokens.empty?.should be_true result = json_number.parse(Tokens.from_string("-#{s}")) result.value.data.should eq(-12.34) result.value.data.should be_a(Float64) result.tokens.empty?.should be_true end end it "parses 0 as an int in various forms" do {"0", "0.0", "0000", ".000", "000.", "-0", "-0.", "-.0", "-0.0"}.each do |s| result = json_number.parse(Tokens.from_string(s)) result.value.data.should eq(0) result.value.data.should be_a(Int64) result.tokens.empty?.should be_true end end it "does not parse '.' or '-.' as 0" do expect_raises(ParserFail) { json_number.parse(Tokens.from_string(".")) } end it "fails when input if not a number" do expect_raises(ParserFail) { json_number.parse(Tokens.from_string("")) } expect_raises(ParserFail) { json_number.parse(Tokens.from_string("foo")) } end end describe "json_string" do it "can parse non-empty strings" do result = json_string.parse(Tokens.from_string("\"foo\"")) result.value.data.should eq("foo") result.tokens.empty?.should be_true end it "can parse empty strings" do result = json_string.parse(Tokens.from_string("\"\"")) result.value.data.should eq("") result.tokens.empty?.should be_true end it "can parse escaped quotes" do result = json_string.parse(Tokens.from_string("\"escaped \\\"\\\" quotes\"")) result.value.data.should eq("escaped \"\" quotes") result.tokens.empty?.should be_true end end describe "json_array" do it "can parse empty arrays" do empty = JSONValue.new([] of JSONValue) {"[]", "[ \t\n\n ]"}.each do |s| tokens = Tokens.from_string(s) result = json_array.parse(tokens) result.value.should eq(empty) result.tokens.empty?.should be_true end end it "can parse arrays with one element" do { "[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)) result.value.data.as(Array(JSONValue)).size.should eq(1) result.tokens.empty?.should be_true end end it "can parse arrays with many elements" do {"[\"one\" , \"two\"]", "[1,2,3,4,]", "[ null\n, false\n, true\n]"}.each do |s| tokens = Tokens.from_string(s) # TODO: come up with better test format result = json_array.parse(tokens) result.value.data.as(Array(JSONValue)).size.>(1).should be_true result.tokens.empty?.should be_true end end it "can parse nested arrays" do expected = JSONValue.new([ JSONValue.new([] of JSONValue), JSONValue.new([ JSONValue.new(nil), JSONValue.new(false), JSONValue.new("two"), JSONValue.new(3_i64), JSONValue.new(4.1_f64), JSONValue.new([] of JSONValue), ]), JSONValue.new([JSONValue.new(5_i64)]), ]) s = "[ [], [null, false, \"two\", 3, 4.1, [],] ,[5]]" tokens = Tokens.from_string(s) result = json_array.parse(tokens) result.value.should eq(expected) result.tokens.empty?.should be_true end it "does not allow trailing commas in empty arrays" do {"[,]", "[ , ]"}.each do |s| expect_raises(ParserFail) { json_array.parse(Tokens.from_string(s)) } end 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")) result.value.data.should be_nil result.tokens.empty?.should be_true end it "parses bools" do result = json_value.parse(Tokens.from_string("true")) result.value.data.should be_true result.tokens.empty?.should be_true result = json_value.parse(Tokens.from_string("false")) result.value.data.should be_false result.tokens.empty?.should be_true end it "parses ints" do result = json_value.parse(Tokens.from_string("42")) result.value.data.should eq(42) result.value.data.should be_a(Int64) result.tokens.empty?.should be_true result = json_value.parse(Tokens.from_string("-42")) result.value.data.should eq(-42) result.value.data.should be_a(Int64) result.tokens.empty?.should be_true end it "parses floats" do result = json_value.parse(Tokens.from_string("12.34")) result.value.data.should eq(12.34) result.value.data.should be_a(Float64) result.tokens.empty?.should be_true result = json_value.parse(Tokens.from_string("-12.34")) result.value.data.should eq(-12.34) result.value.data.should be_a(Float64) result.tokens.empty?.should be_true end it "parses strings" do result = json_value.parse(Tokens.from_string("\"foo\"")) result.value.data.should eq("foo") result.tokens.empty?.should be_true end it "parses arrays" do {"[]", "[ null, ]", "[1, 2, 3, 4.1, 5]"}.each do |s| result = json_value.parse(Tokens.from_string(s)) result.value.data.should be_a(Array(JSONValue)) result.tokens.empty?.should be_true end end it "parses objects" do {"{}", "{\"foo\": 1}"}.each do |s| result = json_value.parse(Tokens.from_string(s)) result.value.data.should be_a(Hash(String, JSONValue)) result.tokens.empty?.should be_true end end end end