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 describe "example: JSON parsing", tags: "example" do json_null = Parser.token_sequence("null".chars).map_const(JSONValue.new(nil)) 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) } digits = Parser(Char, Char).satisfy(&.ascii_number?).many sign = Parser.token('-').map_const(-1).recover(1) point = Parser.token('.').optional json_number = sign.and_then do |s| digits.and_then do |front| point.and_then do |p| digits.and_then do |back| if front.empty? && back.empty? Parser(Char, JSONValue).flunk else x = case when front.empty? && !p.nil? # {[], '.', _} # 0 <= x < 1 "0.#{back.join}".to_f64 when back.empty? #{_, _, []} # whole number front.join.to_i64 else #when !front.empty? && !p.nil? && !back.empty? # some other float "#{front.join}.#{back.join}".to_f64 #else # raise "json_number, Should be unreachable: #{front} #{p} #{back}" end x = x.to_i64 if x.is_a?(Float64) && (x % 1).zero? Parser(Char, JSONValue).pure(JSONValue.new(x * s)) end end end end end 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) } # 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(']') json_array = Parser(Char, JSONValue).new("json_array") do |tokens| # 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, 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