require "../spec_helper" include Parcom # NOTE: WORK IN PROGRESS alias JSONType = Nil \ | Bool \ | Int64 \ | Float64 \ | String \ | Array(JSONValue) \ | Hash(String, JSONValue) struct JSONValue property data : JSONType def initialize(@data) end end describe "example: JSON parsing" 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 << 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) p_json_array = pointerof(json_array) #p_json_object = pointerof(json_object) p_json_value = pointerof(json_value) ws = Parser(Char, Char).satisfy(&.whitespace?).many a_delim = 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 elements = non_empty_elements.recover([] of JSONValue) p = (a_front >> elements << a_back).map { |es| JSONValue.new(es) } p.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, ]).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\"]"}.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_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 end end