# = json.rb # # == Copyright (c) 2006 Florian Frank # # GNU General Public License # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or (at # your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # == Author(s) # # * Florian Frank # # == Additional Information # # The latest version of this library can be downloaded at: # * http://rubyforge.org/frs?group_id=953 # # Online Documentation should be located at: # * http://json.rubyforge.org # Author:: Florian Frank # Copyright:: Copyright (c) 2006 Florian Frank # License:: GNU General Public License (GPL) # = JSON library for Ruby # # Ruby support of Javascript Object Notation. # # == Examples # # To create a JSON string from a ruby data structure, you # can call JSON.unparse like that: # # json = JSON.unparse [1, 2, {"a"=>3.141}, false, true, nil, 4..10] # # => "[1,2,{\"a\":3.141},false,true,null,\"4..10\"]" # # It's also possible to call the #to_json method directly. # # json = [1, 2, {"a"=>3.141}, false, true, nil, 4..10].to_json # # => "[1,2,{\"a\":3.141},false,true,null,\"4..10\"]" # # To get back a ruby data structure, you have to call # JSON.parse on the JSON string: # # JSON.parse json # # => [1, 2, {"a"=>3.141}, false, true, nil, "4..10"] # # Note, that the range from the original data structure is a simple # string now. The reason for this is, that JSON doesn't support ranges # or arbitrary classes. In this case the json library falls back to call # Object#to_json, which is the same as #to_s.to_json. # # It's possible to extend JSON to support serialization of arbitray classes by # simply implementing a more specialized version of the #to_json method, that # should return a JSON object (a hash converted to JSON with #to_json) # like this (don't forget the *a for all the arguments): # # class Range # def to_json(*a) # { # 'json_class' => self.class.name, # 'data' => [ first, last, exclude_end? ] # }.to_json(*a) # end # end # # The hash key 'json_class' is the class, that will be asked to deserialize the # JSON representation later. In this case it's 'Range', but any namespace of # the form 'A::B' or '::A::B' will do. All other keys are arbitrary and can be # used to store the necessary data to configure the object to be deserialized. # # If a the key 'json_class' is found in a JSON object, the JSON parser checks # if the given class responds to the json_create class method. If so, it is # called with the JSON object converted to a Ruby hash. So a range can # be deserialized by implementing Range.json_create like this: # # class Range # def self.json_create(o) # new(*o['data']) # end # end # # Now it possible to serialize/deserialize ranges as well: # # json = JSON.unparse [1, 2, {"a"=>3.141}, false, true, nil, 4..10] # # => "[1,2,{\"a\":3.141},false,true,null,{\"json_class\":\"Range\",\"data\":[4,10,false]}]" # JSON.parse json # # => [1, 2, {"a"=>3.141}, false, true, nil, 4..10] # # JSON.unparse always creates the shortes possible string representation of a # ruby data structure in one line. This good for data storage or network # protocols, but not so good for humans to read. Fortunately there's # also JSON.pretty_unparse that creates a more readable output: # # puts JSON.pretty_unparse([1, 2, {"a"=>3.141}, false, true, nil, 4..10]) # [ # 1, # 2, # { # "a": 3.141 # }, # false, # true, # null, # { # "json_class": "Range", # "data": [ # 4, # 10, # false # ] # } # ] # # There are also the methods Kernel#j for unparse, and Kernel#jj for # pretty_unparse output to the console, that work analogous to Kernel#p and # Kernel#pp. # require 'strscan' # This module is the namespace for all the JSON related classes. It also # defines some module functions to expose a nicer API to users, instead # of using the parser and other classes directly. module JSON # The base exception for JSON errors. JSONError = Class.new StandardError # This exception is raise, if a parser error occurs. ParserError = Class.new JSONError # This exception is raise, if a unparser error occurs. UnparserError = Class.new JSONError # If a circular data structure is encountered while unparsing # this exception is raised. CircularDatastructure = Class.new UnparserError class << self # Switches on Unicode support, if _enable_ is _true_. Otherwise switches # Unicode support off. def support_unicode=(enable) @support_unicode = enable end # Returns _true_ if JSON supports unicode, otherwise _false_ is returned. def support_unicode? !!@support_unicode end end JSON.support_unicode = true # default, hower it's possible to switch off full # unicode support, if non-ascii bytes should be # just passed through. begin require 'iconv' # An iconv instance to convert from UTF8 to UTF16 Big Endian. UTF16toUTF8 = Iconv.new('utf-8', 'utf-16be') # An iconv instance to convert from UTF16 Big Endian to UTF8. UTF8toUTF16 = Iconv.new('utf-16be', 'utf-8'); UTF8toUTF16.iconv('no bom') rescue LoadError JSON.support_unicode = false # enforce disabling of unicode support end # This class implements the JSON parser that is used to parse a JSON string # into a Ruby data structure. class Parser < StringScanner STRING = /"((?:[^"\\]|\\.)*)"/ INTEGER = /-?\d+/ FLOAT = /-?\d+\.(\d*)(?i:e[+-]?\d+)?/ OBJECT_OPEN = /\{/ OBJECT_CLOSE = /\}/ ARRAY_OPEN = /\[/ ARRAY_CLOSE = /\]/ PAIR_DELIMITER = /:/ COLLECTION_DELIMITER = /,/ TRUE = /true/ FALSE = /false/ NULL = /null/ IGNORE = %r( (?: //[^\n\r]*[\n\r]| # line comments /\* # c-style comments (?: [^*/]| # normal chars /[^*]| # slashes that do not start a nested comment \*[^/]| # asterisks that do not end this comment /(?=\*/) # single slash before this comment's end )* \*/ # the end of this comment |\s+ # whitespaces )+ )mx UNPARSED = Object.new # Parses the current JSON string and returns the complete data structure # as a result. def parse reset until eos? case when scan(ARRAY_OPEN) return parse_array when scan(OBJECT_OPEN) return parse_object when skip(IGNORE) ; when !((value = parse_value).equal? 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 if JSON.support_unicode? and $KCODE == 'UTF8' JSON.utf16_to_utf8($~[1]) else # if utf8 mode is switched off or unicode not supported, try to # transform unicode \u-notation to bytes directly: $~[1].to_i(16).chr end 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 skip(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 skip(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 skip(IGNORE) unless scan(PAIR_DELIMITER) raise ParserError, "expected ':' in object at '#{peek(20)}'!" end skip(IGNORE) unless (value = parse_value).equal? UNPARSED result[string] = value skip(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) if klassname = result['json_class'] klass = klassname.sub(/^:+/, '').split(/::/).inject(Object) do |p,k| p.const_get(k) rescue nil end break unless klass and klass.json_creatable? result = klass.json_create(result) end break when skip(IGNORE) ; else raise ParserError, "unexpected token in object at '#{peek(20)}'!" end end result end end # This class is used to create State instances, that are use to hold data # while unparsing a Ruby data structure into a JSON string. class State # Creates a State object from _opts_, which ought to be Hash to create a # new State instance configured by opts, something else to create an # unconfigured instance. If _opts_ is a State object, it is just returned. def self.from_state(opts) case opts when self opts when Hash new(opts) else new end end # Instantiates a new State object, configured by _opts_. def initialize(opts = {}) @indent = opts[:indent] || '' @space = opts[:space] || '' @object_nl = opts[:object_nl] || '' @array_nl = opts[:array_nl] || '' @seen = {} end # This string is used to indent levels in the JSON string. attr_accessor :indent # This string is used to include a space between the tokens in a JSON # string. attr_accessor :space # This string is put at the end of a line that holds a JSON object (or # Hash). attr_accessor :object_nl # This string is put at the end of a line that holds a JSON array. attr_accessor :array_nl # Returns _true_, if _object_ was already seen during this Unparsing run. def seen?(object) @seen.key?(object.__id__) end # Remember _object_, to find out if it was already encountered (to find out # if a cyclic data structure is unparsed). def remember(object) @seen[object.__id__] = true end # Forget _object_ for this Unparsing run. def forget(object) @seen.delete object.__id__ end end module_function # Convert _string_ from UTF8 encoding to UTF16 (big endian) encoding and # return it. def utf8_to_utf16(string) JSON::UTF8toUTF16.iconv(string).unpack('H*')[0] end # Convert _string_ from UTF16 (big endian) encoding to UTF8 encoding and # return it. def utf16_to_utf8(string) bytes = '' << string[0, 2].to_i(16) << string[2, 2].to_i(16) JSON::UTF16toUTF8.iconv(bytes) end # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with # UTF16 big endian characters as \u????, and return it. def utf8_to_json(string) i, n, result = 0, string.size, '' while i < n char = string[i] case when char == ?\b then result << '\b' when char == ?\t then result << '\t' when char == ?\n then result << '\n' when char == ?\f then result << '\f' when char == ?\r then result << '\r' when char == ?" then result << '\"' when char == ?\\ then result << '\\\\' when char.between?(0x0, 0x1f) then result << "\\u%04x" % char when char.between?(0x20, 0x7f) then result << char when !(JSON.support_unicode? && $KCODE == 'UTF8') # if utf8 mode is switched off or unicode not supported, just pass # bytes through: result << char when char & 0xe0 == 0xc0 result << '\u' << utf8_to_utf16(string[i, 2]) i += 1 when char & 0xf0 == 0xe0 result << '\u' << utf8_to_utf16(string[i, 3]) i += 2 when char & 0xf8 == 0xf0 result << '\u' << utf8_to_utf16(string[i, 4]) i += 3 when char & 0xfc == 0xf8 result << '\u' << utf8_to_utf16(string[i, 5]) i += 4 when char & 0xfe == 0xfc result << '\u' << utf8_to_utf16(string[i, 6]) i += 5 else raise JSON::UnparserError, "Encountered unknown UTF-8 byte: %x!" % char end i += 1 end result end # Parse the JSON string _source_ into a Ruby data structure and return it. def parse(source) Parser.new(source).parse end # Unparse the Ruby data structure _obj_ into a single line JSON string and # return it. _state_ is a JSON::State object, that can be used to configure # the output further. def unparse(obj, state = nil) obj.to_json(JSON::State.from_state(state)) end # Unparse the Ruby data structure _obj_ into a JSON string and return it. # The returned string is a prettier form of the string returned by #unparse. def pretty_unparse(obj) state = JSON::State.new( :indent => ' ', :space => ' ', :object_nl => "\n", :array_nl => "\n" ) obj.to_json(state) end end class Object # Converts this object to a string (calling #to_s), converts # it to a JSON string, and returns the result. This is a fallback, if no # special method #to_json was defined for some object. # _state_ is a JSON::State object, that can also be used # to configure the produced JSON string output further. def to_json(*) to_s.to_json end end class Hash # Returns a JSON string containing a JSON object, that is unparsed from # this Hash instance. # _state_ is a JSON::State object, that can also be used to configure the # produced JSON string output further. # _depth_ is used to find out nesting depth, to indent accordingly. 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 data structures 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 << state.object_nl if state result << map { |key,value| json_shift(state, depth + 1) << key.to_s.to_json(state, depth + 1) << ':' << state.space << 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 # Returns a JSON string containing a JSON array, that is unparsed from # this Array instance. # _state_ is a JSON::State object, that can also be used to configure the # produced JSON string output further. # _depth_ is used to find out nesting depth, to indent accordingly. 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 data structures 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 << 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 Integer #:nodoc: # Returns a JSON string representation for this Integer number. def to_json(*) to_s end end class Float #:nodoc: # Returns a JSON string representation for this Float number. def to_json(*) to_s end end class String # This string should be encoded with UTF-8 (if JSON unicode support is # enabled). A call to this method returns a JSON string # encoded with UTF16 big endian characters as \u????. If # JSON.support_unicode? is false only control characters are encoded this # way, all 8-bit bytes are just passed through. def to_json(*) '"' << JSON::utf8_to_json(self) << '"' end # Raw Strings are JSON Objects (the raw bytes are stored in an array for the # key "raw"). The Ruby String can be created by this class method. def self.json_create(o) o['raw'].pack('C*') end # This method creates a raw object, that can be nested into other data # structures and will be unparsed as a raw string. def to_json_raw_object { 'json_class' => self.class.name, 'raw' => self.unpack('C*'), } end # This method should be used, if you want to convert raw strings to JSON # instead of UTF-8 strings, e. g. binary data (and JSON Unicode support is # enabled). def to_json_raw(*args) to_json_raw_object.to_json(*args) end end class TrueClass #:nodoc: # Returns a JSON string for true: 'true'. def to_json(*) to_s end end class FalseClass #:nodoc: # Returns a JSON string for false: 'false'. def to_json(*) to_s end end class NilClass #:nodoc: # Returns a JSON string for nil: 'null'. def to_json(*) 'null' end end module Kernel # Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in # one line. def j(*objs) objs.each do |obj| puts JSON::unparse(obj) end nil end # Ouputs _objs_ to STDOUT as JSON strings in a pretty format, with # indentation and over many lines. def jj(*objs) objs.each do |obj| puts JSON::pretty_unparse(obj) end nil end end class Class # Returns true, if this class can be used to create an instance # from a serialised JSON string. The class has to implement a class # method _json_create_ that expects a hash as first parameter, which includes # the required data. def json_creatable? respond_to?(:json_create) end end # vim: set et sw=2 ts=2: # _____ _ # |_ _|__ ___| |_ # | |/ _ \/ __| __| # | | __/\__ \ |_ # |_|\___||___/\__| # =begin test require 'test/unit' class TC_JSON < Test::Unit::TestCase include JSON class A def initialize(a) @a = a end attr_reader :a def ==(other) a == other.a end def self.json_create(object) new(*object['args']) end def to_json(*args) { 'json_class' => self.class, 'args' => [ @a ], }.to_json(*args) end end def setup $KCODE = 'UTF8' @ary = [1, "foo", 3.14, 4711.0, 2.718, nil, [1,-2,3], false, true] @ary_to_parse = ["1", '"foo"', "3.14", "4711.0", "2.718", "null", "[1,-2,3]", "false", "true"] @hash = { 'a' => 2, 'b' => 3.141, 'c' => 'c', 'd' => [ 1, "b", 3.14 ], 'e' => { 'foo' => 'bar' }, 'g' => "\"\0\037", 'h' => 1000.0, 'i' => 0.001 } @json = '{"a":2,"b":3.141,"c":"c","d":[1,"b",3.14],"e":{"foo":"bar"},' + '"g":"\\"\\u0000\\u001f","h":1.0E3,"i":1.0E-3}' @json2 = '{"a":2,"b":3.141,"c":"c","d":[1,"b",3.14],"e":{"foo":"bar"},' + '"g":"\\"\\u0000\\u001f","h":1000.0,"i":0.001}' end def test_parse_value assert_equal("", parse('""')) assert_equal("\\", parse('"\\\\"')) assert_equal('"', parse('"\""')) assert_equal('\\"\\', parse('"\\\\\\"\\\\"')) assert_equal("\\a\"\b\f\n\r\t\0\037", parse('"\\a\"\b\f\n\r\t\u0000\u001f"')) for i in 0 ... @ary.size assert_equal(@ary[i], parse(@ary_to_parse[i])) end end def test_parse_array assert_equal([], parse('[]')) assert_equal([], parse(' [ ] ')) assert_equal([1], parse('[1]')) assert_equal([1], parse(' [ 1 ] ')) assert_equal(@ary, parse('[1,"foo",3.14,47.11e+2,2718.E-3,null,[1,-2,3],false,true]')) assert_equal(@ary, parse(%Q{ [ 1 , "foo" , 3.14 \t , 47.11e+2 , 2718.E-3 ,\n null , [1, -2, 3 ], false , true\n ] })) end def test_parse_object assert_equal({}, parse('{}')) assert_equal({}, parse(' { } ')) assert_equal({'foo'=>'bar'}, parse('{"foo":"bar"}')) assert_equal({'foo'=>'bar'}, parse(' { "foo" : "bar" } ')) end def test_unparse json = unparse(@hash) assert_equal(@json2, json) parsed_json = parse(json) assert_equal(@hash, parsed_json) json = unparse({1=>2}) assert_equal('{"1":2}', json) parsed_json = parse(json) assert_equal({"1"=>2}, parsed_json) end def test_parser_reset parser = Parser.new(@json) assert_equal(@hash, parser.parse) assert_equal(@hash, parser.parse) end def test_unicode assert_equal '""', ''.to_json assert_equal '"\\b"', "\b".to_json assert_equal '"\u0001"', 0x1.chr.to_json assert_equal '"\u001f"', 0x1f.chr.to_json assert_equal '" "', ' '.to_json assert_equal "\"#{0x7f.chr}\"", 0x7f.chr.to_json utf8 = '© ≠ €!' json = '"\u00a9 \u2260 \u20ac!"' assert_equal json, utf8.to_json assert_equal utf8, parse(json) utf8 = "\343\201\202\343\201\204\343\201\206\343\201\210\343\201\212" json = '"\u3042\u3044\u3046\u3048\u304a"' assert_equal json, utf8.to_json assert_equal utf8, parse(json) utf8 = 'საქართველო' json = '"\u10e1\u10d0\u10e5\u10d0\u10e0\u10d7\u10d5\u10d4\u10da\u10dd"' assert_equal json, utf8.to_json assert_equal utf8, parse(json) end def test_comments json = < "value1", "key2" => "value2", "key3" => "value3" }, parse(json)) json = < "value1" }, parse(json)) end def test_extended_json a = A.new(666) json = a.to_json a_again = JSON.parse(json) assert_kind_of a.class, a_again assert_equal a, a_again end def test_raw_strings raw = '' raw_array = [] for i in 0..255 raw << i raw_array << i end json = raw.to_json_raw json_raw_object = raw.to_json_raw_object hash = { 'json_class' => 'String', 'raw'=> raw_array } assert_equal hash, json_raw_object json_raw = <