diff options
-rw-r--r-- | activesupport/lib/active_support/core_ext/object.rb | 2 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/object/json.rb | 216 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/object/to_json.rb | 30 | ||||
-rw-r--r-- | activesupport/lib/active_support/json/encoding.rb | 196 | ||||
-rw-r--r-- | activesupport/test/core_ext/object/json_test.rb | 9 | ||||
-rw-r--r-- | activesupport/test/json/encoding_test.rb | 8 | ||||
-rw-r--r-- | activesupport/test/ordered_hash_test.rb | 2 | ||||
-rw-r--r-- | guides/source/active_support_core_extensions.md | 6 |
8 files changed, 242 insertions, 227 deletions
diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb index ec2157221f..f4f9152d6a 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -8,7 +8,7 @@ require 'active_support/core_ext/object/inclusion' require 'active_support/core_ext/object/conversions' require 'active_support/core_ext/object/instance_variables' -require 'active_support/core_ext/object/to_json' +require 'active_support/core_ext/object/json' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/object/with_options' diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb new file mode 100644 index 0000000000..1cbc935c30 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -0,0 +1,216 @@ +# Hack to load json gem first so we can overwrite its to_json. +require 'json' + +# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting +# their default behavior. That said, we need to define the basic to_json method in all of them, +# otherwise they will always use to_json gem implementation, which is backwards incompatible in +# several cases (for instance, the JSON implementation for Hash does not work) with inheritance +# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. +[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| + klass.class_eval do + # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info. + def to_json(options = nil) + ActiveSupport::JSON.encode(self, options) + end + end +end + +class Object + def as_json(options = nil) #:nodoc: + if respond_to?(:to_hash) + to_hash + else + instance_values + end + end +end + +class Struct #:nodoc: + def as_json(options = nil) + Hash[members.zip(values)] + end +end + +class TrueClass + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + to_s + end +end + +class FalseClass + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + to_s + end +end + +class NilClass + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + 'null' + end +end + +class String + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + encoder.escape(self) + end +end + +class Symbol + def as_json(options = nil) #:nodoc: + to_s + end +end + +class Numeric + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + to_s + end +end + +class Float + # Encoding Infinity or NaN to JSON should return "null". The default returns + # "Infinity" or "NaN" which breaks parsing the JSON. E.g. JSON.parse('[NaN]'). + def as_json(options = nil) #:nodoc: + finite? ? self : nil + end +end + +class BigDecimal + # A BigDecimal would be naturally represented as a JSON number. Most libraries, + # however, parse non-integer JSON numbers directly as floats. Clients using + # those libraries would get in general a wrong number and no way to recover + # other than manually inspecting the string with the JSON code itself. + # + # That's why a JSON string is returned. The JSON literal is not numeric, but + # if the other end knows by contract that the data is supposed to be a + # BigDecimal, it still has the chance to post-process the string and get the + # real value. + # + # Use <tt>ActiveSupport.use_standard_json_big_decimal_format = true</tt> to + # override this behavior. + def as_json(options = nil) #:nodoc: + if finite? + ActiveSupport.encode_big_decimal_as_string ? to_s : self + else + nil + end + end +end + +class Regexp + def as_json(options = nil) #:nodoc: + to_s + end +end + +module Enumerable + def as_json(options = nil) #:nodoc: + to_a.as_json(options) + end +end + +class Range + def as_json(options = nil) #:nodoc: + to_s + end +end + +class Array + def as_json(options = nil) #:nodoc: + # use encoder as a proxy to call as_json on all elements, to protect from circular references + encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options) + map { |v| encoder.as_json(v, options) } + end + + def encode_json(encoder) #:nodoc: + # we assume here that the encoder has already run as_json on self and the elements, so we run encode_json directly + "[#{map { |v| v.encode_json(encoder) } * ','}]" + end +end + +class Hash + def as_json(options = nil) #:nodoc: + # create a subset of the hash by applying :only or :except + subset = if options + if attrs = options[:only] + slice(*Array(attrs)) + elsif attrs = options[:except] + except(*Array(attrs)) + else + self + end + else + self + end + + # use encoder as a proxy to call as_json on all values in the subset, to protect from circular references + encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options) + Hash[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }] + end + + def encode_json(encoder) #:nodoc: + # values are encoded with use_options = false, because we don't want hash representations from ActiveModel to be + # processed once again with as_json with options, as this could cause unexpected results (i.e. missing fields); + + # on the other hand, we need to run as_json on the elements, because the model representation may contain fields + # like Time/Date in their original (not jsonified) form, etc. + + "{#{map { |k,v| "#{encoder.encode(k.to_s)}:#{encoder.encode(v, false)}" } * ','}}" + end +end + +class Time + def as_json(options = nil) #:nodoc: + if ActiveSupport.use_standard_json_time_format + xmlschema + else + %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) + end + end +end + +class Date + def as_json(options = nil) #:nodoc: + if ActiveSupport.use_standard_json_time_format + strftime("%Y-%m-%d") + else + strftime("%Y/%m/%d") + end + end +end + +class DateTime + def as_json(options = nil) #:nodoc: + if ActiveSupport.use_standard_json_time_format + xmlschema + else + strftime('%Y/%m/%d %H:%M:%S %z') + end + end +end + +class Process::Status + def as_json(options = nil) + { :exitstatus => exitstatus, :pid => pid } + end +end diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb index 83cc8066e7..3dcae6fc7f 100644 --- a/activesupport/lib/active_support/core_ext/object/to_json.rb +++ b/activesupport/lib/active_support/core_ext/object/to_json.rb @@ -1,27 +1,5 @@ -# Hack to load json gem first so we can overwrite its to_json. -begin - require 'json' -rescue LoadError -end +ActiveSupport::Deprecation.warn 'You have required `active_support/core_ext/object/to_json`. ' \ + 'This file will be removed in Rails 4.2. You should require `active_support/core_ext/object/json` ' \ + 'instead.' -# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting -# their default behavior. That said, we need to define the basic to_json method in all of them, -# otherwise they will always use to_json gem implementation, which is backwards incompatible in -# several cases (for instance, the JSON implementation for Hash does not work) with inheritance -# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. -[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| - klass.class_eval do - # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info. - def to_json(options = nil) - ActiveSupport::JSON.encode(self, options) - end - end -end - -module Process - class Status - def as_json(options = nil) - { :exitstatus => exitstatus, :pid => pid } - end - end -end +require 'active_support/core_ext/object/json' diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 77b5d8d227..9a89dac449 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -1,6 +1,6 @@ #encoding: us-ascii -require 'active_support/core_ext/object/to_json' +require 'active_support/core_ext/object/json' require 'active_support/core_ext/module/delegation' require 'bigdecimal' @@ -148,197 +148,3 @@ module ActiveSupport end end end - -class Object - def as_json(options = nil) #:nodoc: - if respond_to?(:to_hash) - to_hash - else - instance_values - end - end -end - -class Struct #:nodoc: - def as_json(options = nil) - Hash[members.zip(values)] - end -end - -class TrueClass - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - to_s - end -end - -class FalseClass - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - to_s - end -end - -class NilClass - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - 'null' - end -end - -class String - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - encoder.escape(self) - end -end - -class Symbol - def as_json(options = nil) #:nodoc: - to_s - end -end - -class Numeric - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - to_s - end -end - -class Float - # Encoding Infinity or NaN to JSON should return "null". The default returns - # "Infinity" or "NaN" which breaks parsing the JSON. E.g. JSON.parse('[NaN]'). - def as_json(options = nil) #:nodoc: - finite? ? self : nil - end -end - -class BigDecimal - # A BigDecimal would be naturally represented as a JSON number. Most libraries, - # however, parse non-integer JSON numbers directly as floats. Clients using - # those libraries would get in general a wrong number and no way to recover - # other than manually inspecting the string with the JSON code itself. - # - # That's why a JSON string is returned. The JSON literal is not numeric, but - # if the other end knows by contract that the data is supposed to be a - # BigDecimal, it still has the chance to post-process the string and get the - # real value. - # - # Use <tt>ActiveSupport.use_standard_json_big_decimal_format = true</tt> to - # override this behavior. - def as_json(options = nil) #:nodoc: - if finite? - ActiveSupport.encode_big_decimal_as_string ? to_s : self - else - nil - end - end -end - -class Regexp - def as_json(options = nil) #:nodoc: - to_s - end -end - -module Enumerable - def as_json(options = nil) #:nodoc: - to_a.as_json(options) - end -end - -class Range - def as_json(options = nil) #:nodoc: - to_s - end -end - -class Array - def as_json(options = nil) #:nodoc: - # use encoder as a proxy to call as_json on all elements, to protect from circular references - encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options) - map { |v| encoder.as_json(v, options) } - end - - def encode_json(encoder) #:nodoc: - # we assume here that the encoder has already run as_json on self and the elements, so we run encode_json directly - "[#{map { |v| v.encode_json(encoder) } * ','}]" - end -end - -class Hash - def as_json(options = nil) #:nodoc: - # create a subset of the hash by applying :only or :except - subset = if options - if attrs = options[:only] - slice(*Array(attrs)) - elsif attrs = options[:except] - except(*Array(attrs)) - else - self - end - else - self - end - - # use encoder as a proxy to call as_json on all values in the subset, to protect from circular references - encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options) - Hash[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }] - end - - def encode_json(encoder) #:nodoc: - # values are encoded with use_options = false, because we don't want hash representations from ActiveModel to be - # processed once again with as_json with options, as this could cause unexpected results (i.e. missing fields); - - # on the other hand, we need to run as_json on the elements, because the model representation may contain fields - # like Time/Date in their original (not jsonified) form, etc. - - "{#{map { |k,v| "#{encoder.encode(k.to_s)}:#{encoder.encode(v, false)}" } * ','}}" - end -end - -class Time - def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format - xmlschema - else - %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) - end - end -end - -class Date - def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format - strftime("%Y-%m-%d") - else - strftime("%Y/%m/%d") - end - end -end - -class DateTime - def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format - xmlschema - else - strftime('%Y/%m/%d %H:%M:%S %z') - end - end -end diff --git a/activesupport/test/core_ext/object/json_test.rb b/activesupport/test/core_ext/object/json_test.rb new file mode 100644 index 0000000000..d3d31530df --- /dev/null +++ b/activesupport/test/core_ext/object/json_test.rb @@ -0,0 +1,9 @@ +require 'abstract_unit' + +class JsonTest < ActiveSupport::TestCase + # See activesupport/test/json/encoding_test.rb for JSON encoding tests + + def test_deprecated_require_to_json_rb + assert_deprecated { require 'active_support/core_ext/object/to_json' } + end +end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index ed1326705c..d549113ff4 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -1,4 +1,5 @@ # encoding: utf-8 +require 'securerandom' require 'abstract_unit' require 'active_support/core_ext/string/inflections' require 'active_support/json' @@ -96,6 +97,13 @@ class TestJSONEncoding < ActiveSupport::TestCase end end + def test_process_status + # There doesn't seem to be a good way to get a handle on a Process::Status object without actually + # creating a child process, hence this to populate $? + system("not_a_real_program_#{SecureRandom.hex}") + assert_equal %({"exitstatus":#{$?.exitstatus},"pid":#{$?.pid}}), ActiveSupport::JSON.encode($?) + end + def test_hash_encoding assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(:a => :b) assert_equal %({\"a\":1}), ActiveSupport::JSON.encode('a' => 1) diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb index c3fe89de4b..0b54026c64 100644 --- a/activesupport/test/ordered_hash_test.rb +++ b/activesupport/test/ordered_hash_test.rb @@ -1,6 +1,6 @@ require 'abstract_unit' require 'active_support/json' -require 'active_support/core_ext/object/to_json' +require 'active_support/core_ext/object/json' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/array/extract_options' diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index d3f49b19fa..74dfd23811 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -421,11 +421,9 @@ NOTE: Defined in `active_support/core_ext/object/with_options.rb`. ### JSON support -Active Support provides a better implementation of `to_json` than the +json+ gem ordinarily provides for Ruby objects. This is because some classes, like +Hash+ and +OrderedHash+ needs special handling in order to provide a proper JSON representation. +Active Support provides a better implementation of `to_json` than the +json+ gem ordinarily provides for Ruby objects. This is because some classes, like +Hash+, +OrderedHash+ and +Process::Status+ needs special handling in order to provide a proper JSON representation. -Active Support also provides an implementation of `as_json` for the <tt>Process::Status</tt> class. - -NOTE: Defined in `active_support/core_ext/object/to_json.rb`. +NOTE: Defined in `active_support/core_ext/object/json.rb`. ### Instance Variables |