rpanachi/core_ext

View on GitHub
lib/core_ext/object/json.rb

Summary

Maintainability
A
55 mins
Test Coverage
# Hack to load json gem first so we can overwrite its to_json.
require 'json'
require 'time'
require 'bigdecimal'
require 'core_ext/big_decimal/conversions' # for #to_s
require 'core_ext/hash/except'
require 'core_ext/hash/slice'
require 'core_ext/object/instance_variables'
require 'core_ext/time/conversions'
require 'core_ext/date_time/conversions'
require 'core_ext/date/conversions'

# 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 CoreExt::OrderedHash cannot be serialized to json.
#
# On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the
# JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always
# passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the
# calls to the original to_json method.
#
# It should be noted that when using ::JSON.{generate,dump} directly, CoreExt's encoder is
# bypassed completely. This means that as_json won't be invoked and the JSON gem will simply
# ignore any options it does not natively understand. This also means that ::JSON.{generate,dump}
# should give exactly the same results with or without active support.

module CoreExt
  module ToJsonWithCoreExtEncoder # :nodoc:
    def to_json(options = nil)
      if options.is_a?(::JSON::State)
        # Called from JSON.{generate,dump}, forward it to JSON gem's to_json
        super(options)
      else
        # to_json is being invoked directly, use CoreExt's encoder
        CoreExt::JSON.encode(self, options)
      end
    end
  end
end

[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].reverse_each do |klass|
  klass.prepend(CoreExt::ToJsonWithCoreExtEncoder)
end

class Object
  def as_json(options = nil) #:nodoc:
    if respond_to?(:to_hash)
      to_hash.as_json(options)
    else
      instance_values.as_json(options)
    end
  end
end

class Struct #:nodoc:
  def as_json(options = nil)
    Hash[members.zip(values)].as_json(options)
  end
end

class TrueClass
  def as_json(options = nil) #:nodoc:
    self
  end
end

class FalseClass
  def as_json(options = nil) #:nodoc:
    self
  end
end

class NilClass
  def as_json(options = nil) #:nodoc:
    self
  end
end

class String
  def as_json(options = nil) #:nodoc:
    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
end

class Float
  # Encoding Infinity or NaN to JSON should return "null". The default returns
  # "Infinity" or "NaN" which are not valid JSON.
  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.
  def as_json(options = nil) #:nodoc:
    finite? ? to_s : nil
  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:
    map { |v| options ? v.as_json(options.dup) : v.as_json }
  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

    Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }]
  end
end

class Time
  def as_json(options = nil) #:nodoc:
    if CoreExt::JSON::Encoding.use_standard_json_time_format
      xmlschema(CoreExt::JSON::Encoding.time_precision)
    else
      %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
    end
  end
end

class Date
  def as_json(options = nil) #:nodoc:
    if CoreExt::JSON::Encoding.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 CoreExt::JSON::Encoding.use_standard_json_time_format
      xmlschema(CoreExt::JSON::Encoding.time_precision)
    else
      strftime('%Y/%m/%d %H:%M:%S %z')
    end
  end
end

class Process::Status #:nodoc:
  def as_json(options = nil)
    { :exitstatus => exitstatus, :pid => pid }
  end
end