Here's my own recursive descent parser (based on the not-quite-correct
quiz tests):
#!/usr/bin/env ruby -wKU
require "strscan"
# http://json.org/
class JSONParser
AST = Struct.new(:value)
def parse(input)
@input = StringScanner.new(input.strip)
parse_value.value
end
private
def parse_value
parse_object or
parse_array or
parse_string or
parse_number or
parse_keyword or
error("Illegal JSON value")
end
def parse_object
if @input.scan(/\{\s*/)
object = Hash.new
while key = parse_string
@input.scan(/\s*:\s*/) or error("Expecting object separator")
object[key.value] = parse_value.value
@input.scan(/\s*,\s*/) or break
end
@input.scan(/\s*\}\s*/) or error("Unclosed object")
AST.new(object)
else
false
end
end
def parse_array
if @input.scan(/\[\s*/)
array = Array.new
while contents = parse_value rescue nil
array << contents.value
@input.scan(/\s*,\s*/) or break
end
@input.scan(/\s*\]\s*/) or error("Unclosed array")
AST.new(array)
else
false
end
end
def parse_string
if @input.scan(/"/)
string = String.new
while contents = parse_string_content || parse_string_escape
string << contents.value
end
@input.scan(/"\s*/) or error("Unclosed string")
AST.new(string)
else
false
end
end
def parse_string_content
@input.scan(/[^\\"]+/) and AST.new(@input.matched)
end
def parse_string_escape
if @input.scan(%r{\\["\\/]})
AST.new(@input.matched[-1])
elsif @input.scan(/\\[bfnrt]/)
AST.new(eval(%Q{"#{@input.matched}"}))
elsif @input.scan(/\\u[0-9a-fA-F]{4}/)
AST.new([Integer("0x#{@input.matched[2..-1]}")].pack("U"))
else
false
end
end
def parse_number
@input.scan(/-?(?:0|[1-9]\d*)(?:\.\d+(?:[eE][+-]?\d+)?)?\b/) and
AST.new(eval(@input.matched))
end
def parse_keyword
@input.scan(/\b(?:true|false|null)\b/) and
AST.new(eval(@input.matched.sub("null", "nil")))
end
def error(message)
if @input.eos?
raise "Unexpected end of input."
else
raise "#{message}: #{@input.peek(@input.string.length)}"
end
end
end
__END__
James Edward Gray II