Skip to content
This repository
Browse code

Add ActiveSupport::JSON and Object#to_json for converting Ruby object…

…s to JSON strings

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@3356 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit e567a5eb1afe1ac38f1da37f1c1e3922bbf79d2a 1 parent 0b55ce7
Sam Stephenson authored
2  activesupport/CHANGELOG
... ...
@@ -1,5 +1,7 @@
1 1
 *SVN*
2 2
 
  3
+* Add ActiveSupport::JSON and Object#to_json for converting Ruby objects to JSON strings. [Sam Stephenson]
  4
+
3 5
 * Add Object#with_options for DRYing up multiple calls to methods having shared options. [Sam Stephenson]  Example:
4 6
 
5 7
   ActionController::Routing::Routes.draw do |map|
4  activesupport/lib/active_support.rb
@@ -34,4 +34,6 @@
34 34
 require 'active_support/ordered_options'
35 35
 require 'active_support/option_merger'
36 36
 
37  
-require 'active_support/values/time_zone'
  37
+require 'active_support/values/time_zone'
  38
+
  39
+require 'active_support/json'
11  activesupport/lib/active_support/core_ext/object_and_class.rb
@@ -54,6 +54,17 @@ def suppress(*exception_classes)
54 54
   def with_options(options)
55 55
     yield ActiveSupport::OptionMerger.new(self, options)
56 56
   end
  57
+  
  58
+  def instance_values
  59
+    instance_variables.inject({}) do |values, name|
  60
+      values[name[1..-1]] = instance_variable_get(name)
  61
+      values
  62
+    end
  63
+  end
  64
+  
  65
+  def to_json
  66
+    ActiveSupport::JSON.encode(self)
  67
+  end
57 68
 end
58 69
 
59 70
 class Class #:nodoc:
2  activesupport/lib/active_support/core_ext/string.rb
@@ -2,10 +2,12 @@
2 2
 require File.dirname(__FILE__) + '/string/conversions'
3 3
 require File.dirname(__FILE__) + '/string/access'
4 4
 require File.dirname(__FILE__) + '/string/starts_ends_with'
  5
+require File.dirname(__FILE__) + '/string/iterators'
5 6
 
6 7
 class String #:nodoc:
7 8
   include ActiveSupport::CoreExtensions::String::Access
8 9
   include ActiveSupport::CoreExtensions::String::Conversions
9 10
   include ActiveSupport::CoreExtensions::String::Inflections
10 11
   include ActiveSupport::CoreExtensions::String::StartsEndsWith
  12
+  include ActiveSupport::CoreExtensions::String::Iterators
11 13
 end
17  activesupport/lib/active_support/core_ext/string/iterators.rb
... ...
@@ -0,0 +1,17 @@
  1
+require 'strscan'
  2
+
  3
+module ActiveSupport #:nodoc:
  4
+  module CoreExtensions #:nodoc:
  5
+    module String #:nodoc:
  6
+      # Custom string iterators
  7
+      module Iterators
  8
+        # Yields a single-character string for each character in the string.
  9
+        # When $KCODE = 'UTF8', multi-byte characters are yielded appropriately.
  10
+        def each_char
  11
+          scanner, char = StringScanner.new(self), /./mu
  12
+          loop { yield(scanner.scan(char) || break) }
  13
+        end
  14
+      end
  15
+    end
  16
+  end
  17
+end
28  activesupport/lib/active_support/json.rb
... ...
@@ -0,0 +1,28 @@
  1
+require 'active_support/json/encoders'
  2
+
  3
+module ActiveSupport
  4
+  module JSON #:nodoc:
  5
+    class CircularReferenceError < StandardError; end
  6
+      
  7
+    class << self
  8
+      REFERENCE_STACK_VARIABLE = :json_reference_stack
  9
+      
  10
+      def encode(value)
  11
+        raise_on_circular_reference(value) do
  12
+          Encoders[value.class].call(value)
  13
+        end
  14
+      end
  15
+      
  16
+      protected
  17
+        def raise_on_circular_reference(value)
  18
+          stack = Thread.current[REFERENCE_STACK_VARIABLE] ||= []
  19
+          raise CircularReferenceError, 'object references itself' if
  20
+            stack.include? value
  21
+          stack << value
  22
+          yield
  23
+        ensure
  24
+          stack.pop
  25
+        end
  26
+    end
  27
+  end
  28
+end
25  activesupport/lib/active_support/json/encoders.rb
... ...
@@ -0,0 +1,25 @@
  1
+module ActiveSupport
  2
+  module JSON #:nodoc:
  3
+    module Encoders
  4
+      mattr_accessor :encoders
  5
+      @@encoders = {}
  6
+
  7
+      class << self        
  8
+        def define_encoder(klass, &block)
  9
+          encoders[klass] = block
  10
+        end
  11
+        
  12
+        def [](klass)
  13
+          klass.ancestors.each do |k|
  14
+            encoder = encoders[k]
  15
+            return encoder if encoder
  16
+          end
  17
+        end
  18
+      end
  19
+    end
  20
+  end
  21
+end
  22
+
  23
+Dir[File.dirname(__FILE__) + '/encoders/*.rb'].each do |file|
  24
+  require file[0..-4]
  25
+end
61  activesupport/lib/active_support/json/encoders/core.rb
... ...
@@ -0,0 +1,61 @@
  1
+module ActiveSupport
  2
+  module JSON #:nodoc:
  3
+    module Encoders
  4
+      define_encoder Object do |object|
  5
+        object.instance_values.to_json
  6
+      end
  7
+      
  8
+      define_encoder TrueClass do
  9
+        'true'
  10
+      end
  11
+      
  12
+      define_encoder FalseClass do
  13
+        'false'
  14
+      end
  15
+      
  16
+      define_encoder NilClass do
  17
+        'null'
  18
+      end
  19
+      
  20
+      define_encoder String do |string|
  21
+        returning value = '"' do
  22
+          string.each_char do |char|
  23
+            value << case
  24
+            when char == "\010":  '\b'
  25
+            when char == "\f":    '\f'
  26
+            when char == "\n":    '\n'
  27
+            when char == "\r":    '\r'
  28
+            when char == "\t":    '\t'
  29
+            when char == '"':     '\"'
  30
+            when char == '\\':    '\\\\'  
  31
+            when char.length > 1: "\\u#{'%04x' % char.unpack('U').first}"
  32
+            else;                 char
  33
+            end
  34
+          end
  35
+          value << '"'
  36
+        end
  37
+      end
  38
+      
  39
+      define_encoder Numeric do |numeric|
  40
+        numeric.to_s
  41
+      end
  42
+      
  43
+      define_encoder Symbol do |symbol|
  44
+        symbol.to_s.to_json
  45
+      end
  46
+
  47
+      define_encoder Enumerable do |enumerable|
  48
+        "[#{enumerable.map { |value| value.to_json } * ', '}]"
  49
+      end
  50
+      
  51
+      define_encoder Hash do |hash|
  52
+        returning result = '{' do
  53
+          result << hash.map do |pair|
  54
+            pair.map { |value| value.to_json } * ': '
  55
+          end * ', '
  56
+          result << '}'
  57
+        end
  58
+      end
  59
+    end
  60
+  end
  61
+end
7  activesupport/test/core_ext/object_and_class_ext_test.rb
@@ -104,4 +104,11 @@ def protected_instance_variables
104 104
     assert !@dest.instance_variables.include?('@quux')
105 105
     assert_equal 'baz', @dest.instance_variable_get('@baz')
106 106
   end
  107
+  
  108
+  def test_instance_values
  109
+    object = Object.new
  110
+    object.instance_variable_set :@a, 1
  111
+    object.instance_variable_set :@b, 2
  112
+    assert_equal({'a' => 1, 'b' => 2}, object.instance_values)
  113
+  end
107 114
 end
10  activesupport/test/core_ext/string_ext_test.rb
@@ -89,4 +89,14 @@ def test_starts_ends_with
89 89
     assert s.ends_with?('lo')    
90 90
     assert !s.ends_with?('el')  
91 91
   end
  92
+
  93
+  def test_each_char_with_utf8_string_when_kcode_is_utf8
  94
+    old_kcode, $KCODE = $KCODE, 'UTF8'
  95
+    '€2.99'.each_char do |char|
  96
+      assert_not_equal 1, char.length
  97
+      break
  98
+    end
  99
+  ensure
  100
+    $KCODE = old_kcode
  101
+  end
92 102
 end
53  activesupport/test/json.rb
... ...
@@ -0,0 +1,53 @@
  1
+$:.unshift File.dirname(__FILE__) + '/../lib'
  2
+require 'active_support'
  3
+require 'test/unit'
  4
+
  5
+class Foo
  6
+  def initialize(a, b)
  7
+    @a, @b = a, b
  8
+  end
  9
+end
  10
+
  11
+class TestJSONEmitters < Test::Unit::TestCase
  12
+  TrueTests    = [[ true,  %(true)  ]]
  13
+  FalseTests   = [[ false, %(false) ]]
  14
+  NilTests     = [[ nil,   %(null)  ]]
  15
+  NumericTests = [[ 1,     %(1)     ],
  16
+                  [ 2.5,   %(2.5)   ]]
  17
+                    
  18
+  StringTests  = [[ 'this is the string',     %("this is the string")         ],
  19
+                  [ 'a "string" with quotes', %("a \\"string\\" with quotes") ]]
  20
+                 
  21
+  ArrayTests   = [[ ['a', 'b', 'c'],          %([\"a\", \"b\", \"c\"])          ],
  22
+                  [ [1, 'a', :b, nil, false], %([1, \"a\", \"b\", null, false]) ]]
  23
+  
  24
+  HashTests    = [[ {:a => :b, :c => :d}, %({\"c\": \"d\", \"a\": \"b\"}) ]]
  25
+                  
  26
+  SymbolTests  = [[ :a,     %("a")    ],
  27
+                  [ :this,  %("this") ],
  28
+                  [ :"a b", %("a b")  ]]
  29
+
  30
+  ObjectTests  = [[ Foo.new(1, 2), %({\"a\": 1, \"b\": 2}) ]]
  31
+  
  32
+  constants.grep(/Tests$/).each do |class_tests|
  33
+    define_method("test_#{class_tests[0..-6].downcase}") do
  34
+      self.class.const_get(class_tests).each do |pair|
  35
+        assert_equal pair.last, pair.first.to_json
  36
+      end
  37
+    end
  38
+  end
  39
+  
  40
+  def test_utf8_string_encoded_properly_when_kcode_is_utf8
  41
+    old_kcode, $KCODE = $KCODE, 'UTF8'
  42
+    assert_equal '"\\u20ac2.99"', '€2.99'.to_json
  43
+    assert_equal '"\\u270e\\u263a"', '✎☺'.to_json
  44
+  ensure
  45
+    $KCODE = old_kcode
  46
+  end
  47
+  
  48
+  def test_exception_raised_when_encoding_circular_reference
  49
+    a = [1]
  50
+    a << a
  51
+    assert_raises(ActiveSupport::JSON::CircularReferenceError) { a.to_json }
  52
+  end
  53
+end

0 notes on commit e567a5e

Please sign in to comment.
Something went wrong with that request. Please try again.