aboutsummaryrefslogblamecommitdiffstats
path: root/activesupport/lib/active_support/json/encoding.rb
blob: 389df58ec485247cd0ff13fa590fefd8299cf4b2 (plain) (tree)
1
2
3
4
5
6
7
8
9

                                                   
 
                    
                                                                     

                                             
                                                           
              
                                                  

                                                       
             
 
                    


                                                                             
                                                                    


                                             
             
                                  
                                                                                                                                    
 



                                                                                         

                                                  

       






                                                       
                                  
                         

           
                                             
                                                 





                                                                                       
                                              
                                                 
                                                                           





                                                                                                               
                                           

                   








                                                  
                                           

                                                                      

                 
                                      




                       








                                                                   














                                                                                                                     


                                                                                                 





                                                    
                                 
                
                              



                          
                                                                                                          


                                                        

                                                        

                                                                              
                             
                                                
              


           
                                               
                                               
                                               
       


     
            






                                     

   

                            



                             
               






                                     


                






                                     


              






                                     


            






                                     


            


                                     


             






                                     

   
           
                                                                              
                                                                                


                                     

   
                







                                                                                 





                                                                                            
         

       

   
            


                                     


                 


                                     

   
           


                                     

   
           


                                                                                                 
                                           





                                                                                                                      



                                     

                                                              
                               
                            
                                    
                             

            
         


          


                                                                                                             
                                                                     

     
                                   






                                                                                                                    







                                                  
                                                                    


       
 

















                                                  
   
require 'active_support/core_ext/object/to_json'
require 'active_support/core_ext/module/delegation'

require 'bigdecimal'
require 'active_support/core_ext/big_decimal/conversions' # for #to_s
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/object/instance_variables'
require 'time'
require 'active_support/core_ext/time/conversions'
require 'active_support/core_ext/date_time/conversions'
require 'active_support/core_ext/date/conversions'
require 'set'

module ActiveSupport
  class << self
    delegate :use_standard_json_time_format, :use_standard_json_time_format=,
      :escape_html_entities_in_json, :escape_html_entities_in_json=,
      :encode_big_decimal_as_string, :encode_big_decimal_as_string=,
      :to => :'ActiveSupport::JSON::Encoding'
  end

  module JSON
    # matches YAML-formatted dates
    DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/

    # Dumps objects in JSON (JavaScript Object Notation). See www.json.org for more info.
    #
    #   ActiveSupport::JSON.encode({team: 'rails', players: '36'})
    #   # => "{\"team\":\"rails\",\"players\":\"36\"}"
    def self.encode(value, options = nil)
      Encoding::Encoder.new(options).encode(value)
    end

    module Encoding #:nodoc:
      class CircularReferenceError < StandardError; end

      class Encoder
        attr_reader :options

        def initialize(options = nil)
          @options = options || {}
          @seen = Set.new
        end

        def encode(value, use_options = true)
          check_for_circular_references(value) do
            jsonified = use_options ? value.as_json(options_for(value)) : value.as_json
            jsonified.encode_json(self)
          end
        end

        # like encode, but only calls as_json, without encoding to string
        def as_json(value, use_options = true)
          check_for_circular_references(value) do
            use_options ? value.as_json(options_for(value)) : value.as_json
          end
        end

        def options_for(value)
          if value.is_a?(Array) || value.is_a?(Hash)
            # hashes and arrays need to get encoder in the options, so that they can detect circular references
            options.merge(:encoder => self)
          else
            options
          end
        end

        def escape(string)
          Encoding.escape(string)
        end

        private
          def check_for_circular_references(value)
            unless @seen.add?(value.__id__)
              raise CircularReferenceError, 'object references itself'
            end
            yield
          ensure
            @seen.delete(value.__id__)
          end
      end


      ESCAPED_CHARS = {
        "\x00" => '\u0000', "\x01" => '\u0001', "\x02" => '\u0002',
        "\x03" => '\u0003', "\x04" => '\u0004', "\x05" => '\u0005',
        "\x06" => '\u0006', "\x07" => '\u0007', "\x0B" => '\u000B',
        "\x0E" => '\u000E', "\x0F" => '\u000F', "\x10" => '\u0010',
        "\x11" => '\u0011', "\x12" => '\u0012', "\x13" => '\u0013',
        "\x14" => '\u0014', "\x15" => '\u0015', "\x16" => '\u0016',
        "\x17" => '\u0017', "\x18" => '\u0018', "\x19" => '\u0019',
        "\x1A" => '\u001A', "\x1B" => '\u001B', "\x1C" => '\u001C',
        "\x1D" => '\u001D', "\x1E" => '\u001E', "\x1F" => '\u001F',
        "\010" =>  '\b',
        "\f"   =>  '\f',
        "\n"   =>  '\n',
        "\r"   =>  '\r',
        "\t"   =>  '\t',
        '"'    =>  '\"',
        '\\'   =>  '\\\\',
        '>'    =>  '\u003E',
        '<'    =>  '\u003C',
        '&'    =>  '\u0026' }

      class << self
        # If true, use ISO 8601 format for dates and times. Otherwise, fall back to the Active Support legacy format.
        attr_accessor :use_standard_json_time_format

        # If false, serializes BigDecimal objects as numeric instead of wrapping them in a string
        attr_accessor :encode_big_decimal_as_string

        attr_accessor :escape_regex
        attr_reader :escape_html_entities_in_json

        def escape_html_entities_in_json=(value)
          self.escape_regex = \
            if @escape_html_entities_in_json = value
              /[\x00-\x1F"\\><&]/
            else
              /[\x00-\x1F"\\]/
            end
        end

        def escape(string)
          string = string.encode(::Encoding::UTF_8, :undef => :replace).force_encoding(::Encoding::BINARY)
          json = string.
            gsub(escape_regex) { |s| ESCAPED_CHARS[s] }.
            gsub(/([\xC0-\xDF][\x80-\xBF]|
                   [\xE0-\xEF][\x80-\xBF]{2}|
                   [\xF0-\xF7][\x80-\xBF]{3})+/nx) { |s|
            s.unpack("U*").pack("n*").unpack("H*")[0].gsub(/.{4}/n, '\\\\u\&')
          }
          json = %("#{json}")
          json.force_encoding(::Encoding::UTF_8)
          json
        end
      end

      self.use_standard_json_time_format = true
      self.escape_html_entities_in_json  = true
      self.encode_big_decimal_as_string  = true
    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 ActiveSupport.use_standard_json_big_decimal_format = true to override this behaviour
  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