Parsing JSON in IronRuby
@str.
split(":".to_clr_string).
join("=>".to_clr_string).
to_clr_string
end
Yep, that's right ... replace all occurrences of ":" with "=>" .. eek! That'll work fine until you have time values that look like "2007/03/24 09:00:00". =) Then what do I do with this transformed string?
ScriptRuntime.
create.get_engine("rb").
create_script_source_from_string(json_str).
execute
end
That's right, I eval it! (Why did I write my own eval? Because we don't have eval working in IronRuby today, so this is how you'd use the Hosting APIs of the DLR to implement it). No parsing/making sure the string doesn't reformat my drive or anything. =P
So, you write crappy code ... got a fix?Yeah, thanks genius ... that's the point of this spheel. Anyway, the solution is to write a real JSON parser in Ruby. There are JSON libraries for Ruby already (gem install json), but I wanted a simple stand-alone parser to include in a Silverlight application. Luckily, that's not a extremely hard task as the JSON semantics are pretty straight-forward. Though, I'm a lazy bastard, so I'm not going to write my own!
Ruby Quiz #155 talks about parsing JSON, and shows a hand-rolled recursive decent parser. It didn't work as-advertised in IronRuby (to be expected though, IronRuby isn't finished yet), so I adapted it to work in IronRuby. This parser not only gets the semantics right, but makes sure I'm not eval-ing random stuff (it makes sure it parses as JSON before eval-ing). Anyway, here's all 154 lines of glory:
include Microsoft::Scripting::Hosting
# TODO: remove when eval is implemented
ScriptRuntime.
create.
get_engine("rb").
create_script_source_from_string(str).
execute
end
# TODO: remove this when Struct is working properly
@value = value
end
@value
end
end
@input = StringScanner.new(input)
if top_level = parse_object || parse_array
top_level.value
else
error("Illegal top-level JSON object")
end
end
private
trim_space
parse_object or
parse_array or
parse_string or
parse_number or
parse_keyword or
error("Illegal JSON value")
ensure
trim_space
end
if @input.scan(/\{\s*/)
object = Hash.new
more_pairs = false
while true
key = parse_string
break if key == false
@input.scan(/\s*:\s*/) or error("Expecting object separator")
object[key.value] = parse_value.value
more_pairs = @input.scan(/\s*,\s*/) or break
end
error("Missing object pair") if more_pairs
@input.scan(/\s*\}/) or error("Unclosed object")
AST.new(object)
else
false
end
end
if @input.scan(/\[\s*/)
array = Array.new
more_values = false
while true
contents = begin
parse_value
rescue
nil
end
break if contents == false or contents.nil?
array << contents.value
more_values = @input.scan(/\s*,\s*/) or break
end
error("Missing value") if more_values
@input.scan(/\s*\]/) or error("Unclosed array")
AST.new(array)
else
false
end
end
if @input.scan(/"/)
string = String.new
while true
contents = parse_string_content
contents = parse_string_escape if contents == false
break if contents == false
string << contents.value
end
@input.scan(/"/) or error("Unclosed string")
AST.new(string)
else
false
end
end
if @input.scan(/[^\\"]+/)
AST.new(@input.matched)
else
false
end
end
if @input.scan(%r{\\["\\/]})
AST.new(@input.matched[-1])
elsif @input.scan(/\\[bfnrt]/)
AST.new(evaluate(%Q{" "}))
elsif @input.scan(/\\u[0-9a-fA-F]{4}/)
AST.new([Integer("0x")].pack("U"))
else
false
end
end
if @input.scan(/-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?\b/)
AST.new(evaluate(@input.matched))
else
false
end
end
if @input.scan(/\b(?:true|false|null)\b/)
matched = @input.matched == "null" ? "nil" : @input.matched
val = evaluate(matched)
AST.new(val)
else
false
end
end
@input.scan(/\s+/)
end
if @input.eos?
raise "Unexpected end of input."
else
raise " : "
end
end
end
Usage:
p = JSONParser.new
r = p.parse YOUR_JSON_DATA
Enjoy!