Florian Frank
1/27/2005 9:58:00 PM
ruby talk wrote:
>Mixed in with the current churn about JavaScript and XmlHttpRequest
>stuff is JSON [0] and JSON-RPC [1]. I've done some assorted RPC stuff
>with XmlHttpRequest, but the JSON part looks interesting because it
>transfers fewer bytes and may make it easier to build "standardized"
>browser services in Ruby. (JSON is JavaScript object notation; like
>YAML with C curly-braces instead of Python indentation, and JSON-RPC
>is like XML-RPC, but with JSON.)
>
>
I've recently written a JSON parser based on Ruby's StringScanner class,
that doesn't support the unicode stuff of the specifcation fully yet. A
few small examples how it can be used come here:
> puts
JSON.pretty_unparse([1,{"foo"=>2,"bar"=>[true,false,nil]},"baz",3.14])
[
1,
{
"foo":2,
"bar":[
true,
false,
null
]
},
"baz",
3.14
]
> p JSON.parse([1,{"foo"=>2,"bar"=>[true,false,nil]},"baz",3.14].to_json)
#=> [1, {"foo"=>2, "bar"=>[true, false, nil]}, "baz", 3.14]
I have attached a copy of the core class to this mail, but I wonder, if
I should do a real release (with tests and stuff) on Rubyforge, if you
or other people want to build something bigger with it.
--
Florian Frank
require 'strscan'
module JSON
JSONError = Class.new StandardError
ParserError = Class.new JSONError
CircularDatastructure = Class.new JSONError
class Parser < StringScanner
STRING = /"((?:\\"|[^"])*)"/
INTEGER = /\d+/
FLOAT = /-?\d+\.(\d*)(?:e[+-]?\d+)?/i
OBJECT_OPEN = /\{/
OBJECT_CLOSE = /\}/
ARRAY_OPEN = /\[/
ARRAY_CLOSE = /\]/
PAIR_DELIMITER = /:/
COLLECTION_DELIMITER = /,/
TRUE = /true/
FALSE = /false/
NULL = /null/
IGNORE = /\s+/
UNPARSED = Object.new
def parse
reset
until eos?
case
when scan(ARRAY_OPEN)
return parse_array
when scan(OBJECT_OPEN)
return parse_object
when scan(IGNORE)
;
when (value = parse_value) != UNPARSED
return value
else
raise ParserError, "source '#{peek(20)}' not in JSON!"
end
end
end
private
def parse_string
if scan(STRING)
return '' if self[1].empty?
self[1].gsub(/\\(?:[\\bfnrt"]|u([A-Fa-f\d]{4}))/) do
case $~[0]
when '\\\\' then '\\'
when '\\b' then "\b"
when '\\f' then "\f"
when '\\n' then "\n"
when '\\r' then "\r"
when '\\t' then "\t"
when '\"' then '"'
else $~[1].to_i(16).chr # TODO atm only unicode characters <= 0x127
end
end
else
UNPARSED
end
end
def parse_value
case
when scan(FLOAT)
Float(self[0])
when scan(INTEGER)
Integer(self[0])
when scan(TRUE)
true
when scan(FALSE)
false
when scan(NULL)
nil
when (string = parse_string) != UNPARSED
string
when scan(ARRAY_OPEN)
parse_array
when scan(OBJECT_OPEN)
parse_object
else
UNPARSED
end
end
def parse_array
result = []
until eos?
case
when (value = parse_value) != UNPARSED
result << value
scan(IGNORE)
unless scan(COLLECTION_DELIMITER) or match?(ARRAY_CLOSE)
raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!"
end
when scan(ARRAY_CLOSE)
break
when scan(IGNORE)
;
else
raise ParserError, "unexpected token in array at '#{peek(20)}'!"
end
end
result
end
def parse_object
result = {}
until eos?
case
when (string = parse_string) != UNPARSED
scan(IGNORE)
unless scan(PAIR_DELIMITER)
raise ParserError, "expected ':' in object at '#{peek(20)}'!"
end
scan(IGNORE)
if (value = parse_value) != UNPARSED
result[string] = value
scan(IGNORE)
unless scan(COLLECTION_DELIMITER) or match?(OBJECT_CLOSE)
raise ParserError,
"expected ',' or '}' in object at '#{peek(20)}'!"
end
else
raise ParserError, "expected value in object at '#{peek(20)}'!"
end
when scan(OBJECT_CLOSE)
break
when scan(IGNORE)
;
else
raise ParserError, "unexpected token in object at '#{peek(20)}'!"
end
end
result
end
end
class State
def self.from_state(opts)
case opts
when self
opts
when Hash
new(opts)
else
new
end
end
def initialize(opts = {})
@indent = opts[:indent] || ''
@object_nl = opts[:object_nl] || ''
@array_nl = opts[:array_nl] || ''
@seen = {}
end
attr_accessor :indent
attr_accessor :object_nl
attr_accessor :array_nl
def seen?(object)
@seen.key?(object.__id__)
end
def remember(object)
@seen[object.__id__] = true
end
def forget(object)
@seen.delete object.__id__
end
end
module_function
def parse(source)
Parser.new(source).parse
end
def unparse(obj, state = nil)
state = JSON::State.from_state(state)
obj.to_json(state)
end
def pretty_unparse(obj)
state = JSON::State.new(
:indent => ' ',
:object_nl => "\n",
:array_nl => "\n"
)
obj.to_json(state)
end
end
class Object
def to_json(*args)
to_s.to_json
end
end
class Hash
def to_json(state = nil, depth = 0)
state = JSON::State.from_state(state)
json_check_circular(state) { json_transform(state, depth) }
end
private
def json_check_circular(state)
if state
state.seen?(self) and raise JSON::CircularDatastructure,
"circular datastructures not supported!"
state.remember self
end
yield
ensure
state and state.forget self
end
def json_shift(state, depth)
state and not state.object_nl.empty? or return ''
state.indent * depth
end
def json_transform(state, depth)
delim = ','
delim << state.object_nl if state
result = ''
result << '{'
result << state.object_nl if state
result << map { |string,value|
json_shift(state, depth + 1) <<
string.to_json(state, depth + 1) <<
':' << value.to_json(state, depth + 1)
}.join(delim)
result << state.object_nl if state
result << json_shift(state, depth)
result << '}'
result
end
end
class Array
def to_json(state = nil, depth = 0)
state = JSON::State.from_state(state)
json_check_circular(state) { json_transform(state, depth) }
end
private
def json_check_circular(state)
if state
state.seen?(self) and raise JSON::CircularDatastructure,
"circular datastructures not supported!"
state.remember self
end
yield
ensure
state and state.forget self
end
def json_shift(state, depth)
state and not state.array_nl.empty? or return ''
state.indent * depth
end
def json_transform(state, depth)
delim = ','
delim << state.array_nl if state
result = ''
result = '['
result << state.array_nl if state
result << map { |value|
json_shift(state, depth + 1) << value.to_json(state, depth + 1)
}.join(delim)
result << state.array_nl if state
result << json_shift(state, depth)
result << ']'
result
end
end
class Fixnum
def to_json(*args) to_s end
end
class Float
def to_json(*args) to_s end
end
class String
def to_json(*args)
'"' << gsub(/["\\\0-\037]/) do
case $~[0]
when '"' then '\\"'
when '\\' then '\\\\'
when "\b" then '\b'
when "\f" then '\f'
when "\n" then '\n'
when "\r" then '\r'
when "\t" then '\t'
else "\\u\%04x" % $~[0][0]
end
end << '"'
end
end
class TrueClass
def to_json(*args) to_s end
end
class FalseClass
def to_json(*args) to_s end
end
class NilClass
def to_json(*args) 'null' end
end
module Kernel
def j(*objs)
objs.each do |obj|
puts JSON::unparse(obj)
end
nil
end
def jj(*objs)
objs.each do |obj|
puts JSON::pretty_unparse(obj)
end
nil
end
end
# vim: set et sw=2 ts=2: