Skip to content

Instantly share code, notes, and snippets.

@statianzo
Created May 17, 2011 23:57
Show Gist options
  • Save statianzo/977699 to your computer and use it in GitHub Desktop.
Save statianzo/977699 to your computer and use it in GitHub Desktop.
A json parser
require "strscan"
class JSONParser
AST = Struct.new(:value)
def parse(input)
@input = StringScanner.new(input)
parse_value.value
ensure
@input.eos? or error("Unexpected data")
end
private
#comment is back
def parse_value
trim_space
parse_object or
parse_array or
parse_string or
parse_number or
parse_keyword or
error("Invalid Data")
ensure
trim_space
end
#Parses colon separated object hashes
def parse_object
if @input.scan(/\s*\{/)
obj = Hash.new
more_parts = false
while key = parse_string
@input.scan(/\s*:\s*/) or error("Expected : separator")
obj[key.value] = parse_value.value
more_parts = @input.scan(/\s*,\s*/) or break
end
error("Missing object pair") if more_parts
@input.scan(/\s*}/) or error("Unclosed object")
AST.new(obj)
else
false
end
end
def parse_array
if @input.scan(/\s*\[/) #Arrays start with [
a = Array.new
more_items = false
while current = parse_value
a << current.value
more_items = @input.scan(/\s*,\s*/) or break
end
error("Missing value") if more_items
@input.scan(/s*\]/) or error("Unclosed array")
AST.new(a)
end
end
#Parses string objects
def parse_string
if @input.scan(/"/)
s = String.new
while current = parse_string_content || parse_string_escape
s << current.value
end
@input.scan(/"/) or error('Unclosed String')
AST.new(s)
else
false
end
end
def parse_string_content
@input.scan(/[^\\"]+/) and AST.new(@input.matched)
end
def parse_string_escape
if @input.scan(%r{\\["\\/]}) #slashes and quotations
AST.new(@input.matched[-1])
elsif @input.scan(/\\[bfnrt]/) #newlines, tabs, etc
AST.new(eval(%Q{"#{@input.matched}"}))
elsif @input.scan(/\\u[0-9a-fA-F]{4}/) #Hex integers
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 trim_space
@input.scan(/\s+/)
end
def error(message)
raise "#{message}: #{@input.peek(@input.string.length)}"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment