aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/json.rb26
-rw-r--r--lib/json/add/core.rb120
-rw-r--r--lib/json/add/rails.rb58
-rw-r--r--test/json/test_json_rails.rb113
4 files changed, 305 insertions, 12 deletions
diff --git a/lib/json.rb b/lib/json.rb
index bb711c1724..3b0b711550 100644
--- a/lib/json.rb
+++ b/lib/json.rb
@@ -62,6 +62,13 @@ require 'json/common'
# you
# require 'json/add/core'
#
+# After requiring this you can, e. g., serialise/deserialise Ruby ranges:
+#
+# JSON JSON(1..10) # => 1..10
+#
+# To find out how to add JSON support to other or your own classes, read the
+# Examples section below.
+#
# To get the best compatibility to rails' JSON implementation, you can
# require 'json/add/rails'
#
@@ -125,11 +132,6 @@ require 'json/common'
# json = JSON.generate [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 create a valid JSON text you have to make sure, that the output is
# embedded in either a JSON array [] or a JSON object {}. The easiest way to do
# this, is by putting your values in a Ruby Array or Hash instance.
@@ -145,10 +147,10 @@ require 'json/common'
# 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 arbitrary classes by
+# It's possible to add JSON support serialization to arbitrary 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):
+# 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)
@@ -159,15 +161,15 @@ require 'json/common'
# end
# end
#
-# The hash key 'json_class' is the class, that will be asked to deserialize the
+# The hash key 'json_class' is the class, that will be asked to deserialise 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.
+# used to store the necessary data to configure the object to be deserialised.
#
# 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:
+# be deserialised by implementing Range.json_create like this:
#
# class Range
# def self.json_create(o)
@@ -175,7 +177,7 @@ require 'json/common'
# end
# end
#
-# Now it possible to serialize/deserialize ranges as well:
+# Now it possible to serialise/deserialise ranges as well:
#
# json = JSON.generate [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]}]"
diff --git a/lib/json/add/core.rb b/lib/json/add/core.rb
new file mode 100644
index 0000000000..630ed6e4b7
--- /dev/null
+++ b/lib/json/add/core.rb
@@ -0,0 +1,120 @@
+# This file contains implementations of ruby core's custom objects for
+# serialisation/deserialisation.
+
+unless Object.const_defined?(:JSON) and ::JSON.const_defined?(:JSON_LOADED) and
+ ::JSON::JSON_LOADED
+ require 'json'
+end
+require 'date'
+
+class Time
+ def self.json_create(object)
+ at(*object.values_at('s', 'u'))
+ end
+
+ def to_json(*args)
+ {
+ 'json_class' => self.class.name.to_s,
+ 's' => tv_sec,
+ 'u' => tv_usec,
+ }.to_json(*args)
+ end
+end
+
+class Date
+ def self.json_create(object)
+ civil(*object.values_at('y', 'm', 'd', 'sg'))
+ end
+
+ def to_json(*args)
+ {
+ 'json_class' => self.class.name.to_s,
+ 'y' => year,
+ 'm' => month,
+ 'd' => day,
+ 'sg' => @sg,
+ }.to_json(*args)
+ end
+end
+
+class DateTime
+ def self.json_create(object)
+ args = object.values_at('y', 'm', 'd', 'H', 'M', 'S')
+ of_a, of_b = object['of'].split('/')
+ args << Rational(of_a.to_i, of_b.to_i)
+ args << object['sg']
+ civil(*args)
+ end
+
+ def to_json(*args)
+ {
+ 'json_class' => self.class.name.to_s,
+ 'y' => year,
+ 'm' => month,
+ 'd' => day,
+ 'H' => hour,
+ 'M' => min,
+ 'S' => sec,
+ 'of' => offset.to_s,
+ 'sg' => @sg,
+ }.to_json(*args)
+ end
+end
+
+class Range
+ def self.json_create(object)
+ new(*object['a'])
+ end
+
+ def to_json(*args)
+ {
+ 'json_class' => self.class.name.to_s,
+ 'a' => [ first, last, exclude_end? ]
+ }.to_json(*args)
+ end
+end
+
+class Struct
+ def self.json_create(object)
+ new(*object['v'])
+ end
+
+ def to_json(*args)
+ klass = self.class.name.to_s
+ klass.empty? and raise JSON::JSONError, "Only named structs are supported!"
+ {
+ 'json_class' => klass,
+ 'v' => values,
+ }.to_json(*args)
+ end
+end
+
+class Exception
+ def self.json_create(object)
+ result = new(object['m'])
+ result.set_backtrace object['b']
+ result
+ end
+
+ def to_json(*args)
+ {
+ 'json_class' => self.class.name.to_s,
+ 'm' => message,
+ 'b' => backtrace,
+ }.to_json(*args)
+ end
+end
+
+class Regexp
+ def self.json_create(object)
+ new(object['s'], object['o'])
+ end
+
+ def to_json(*)
+ {
+ 'json_class' => self.class.name.to_s,
+ 'o' => options,
+ 's' => source,
+ }.to_json
+ end
+end
diff --git a/lib/json/add/rails.rb b/lib/json/add/rails.rb
new file mode 100644
index 0000000000..e86ed1aab9
--- /dev/null
+++ b/lib/json/add/rails.rb
@@ -0,0 +1,58 @@
+# This file contains implementations of rails custom objects for
+# serialisation/deserialisation.
+
+unless Object.const_defined?(:JSON) and ::JSON.const_defined?(:JSON_LOADED) and
+ ::JSON::JSON_LOADED
+ require 'json'
+end
+
+class Object
+ def self.json_create(object)
+ obj = new
+ for key, value in object
+ next if key == 'json_class'
+ instance_variable_set "@#{key}", value
+ end
+ obj
+ end
+
+ def to_json(*a)
+ result = {
+ 'json_class' => self.class.name
+ }
+ instance_variables.inject(result) do |r, name|
+ r[name[1..-1]] = instance_variable_get name
+ r
+ end
+ result.to_json(*a)
+ end
+end
+
+class Symbol
+ def to_json(*a)
+ to_s.to_json(*a)
+ end
+end
+
+module Enumerable
+ def to_json(*a)
+ to_a.to_json(*a)
+ end
+end
+
+# class Regexp
+# def to_json(*)
+# inspect
+# end
+# end
+#
+# The above rails definition has some problems:
+#
+# 1. { 'foo' => /bar/ }.to_json # => "{foo: /bar/}"
+# This isn't valid JSON, because the regular expression syntax is not
+# defined in RFC 4627. (And unquoted strings are disallowed there, too.)
+# Though it is valid Javascript.
+#
+# 2. { 'foo' => /bar/mix }.to_json # => "{foo: /bar/mix}"
+# This isn't even valid Javascript.
+
diff --git a/test/json/test_json_rails.rb b/test/json/test_json_rails.rb
new file mode 100644
index 0000000000..e44ea606e3
--- /dev/null
+++ b/test/json/test_json_rails.rb
@@ -0,0 +1,113 @@
+#!/usr/bin/env ruby
+
+require 'test/unit'
+require 'json/add/rails'
+require 'date'
+
+class TC_JSONRails < 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.name,
+ 'args' => [ @a ],
+ }.to_json(*args)
+ end
+ end
+
+ class B
+ def to_json(*args)
+ {
+ 'json_class' => self.class.name,
+ }.to_json(*args)
+ end
+ end
+
+ class C
+ def to_json(*args)
+ {
+ 'json_class' => 'TC_JSONRails::Nix',
+ }.to_json(*args)
+ end
+ end
+
+ def setup
+ end
+
+ def test_extended_json
+ a = A.new(666)
+ assert A.json_creatable?
+ json = generate(a)
+ a_again = JSON.parse(json)
+ assert_kind_of a.class, a_again
+ assert_equal a, a_again
+ end
+
+ def test_extended_json_disabled
+ a = A.new(666)
+ assert A.json_creatable?
+ json = generate(a)
+ a_again = JSON.parse(json, :create_additions => true)
+ assert_kind_of a.class, a_again
+ assert_equal a, a_again
+ a_hash = JSON.parse(json, :create_additions => false)
+ assert_kind_of Hash, a_hash
+ assert_equal(
+ {"args"=>[666], "json_class"=>"TC_JSONRails::A"}.sort_by { |k,| k },
+ a_hash.sort_by { |k,| k }
+ )
+ end
+
+ def test_extended_json_fail
+ b = B.new
+ assert !B.json_creatable?
+ json = generate(b)
+ assert_equal({ 'json_class' => B.name }, JSON.parse(json))
+ end
+
+ def test_extended_json_fail
+ c = C.new # with rails addition all objects are theoretically creatable
+ assert C.json_creatable?
+ json = generate(c)
+ assert_raises(ArgumentError) { JSON.parse(json) }
+ 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 = <<EOT.chomp
+{\"json_class\":\"String\",\"raw\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255]}
+EOT
+# "
+ assert_equal json_raw, json
+ raw_again = JSON.parse(json)
+ assert_equal raw, raw_again
+ end
+
+ def test_symbol
+ assert_equal '"foo"', JSON(:foo) # we don't want an object here
+ end
+end