encoding.rb 8.8 KB
Newer Older
1 2 3
require 'active_support/core_ext/object/to_json'
require 'active_support/core_ext/module/delegation'
require 'active_support/json/variable'
4
require 'active_support/ordered_hash'
5

6
require 'bigdecimal'
7
require 'active_support/core_ext/big_decimal/conversions' # for #to_s
8
require 'active_support/core_ext/array/wrap'
J
Jeremy Kemper 已提交
9 10
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/slice'
11
require 'active_support/core_ext/object/instance_variables'
12
require 'time'
13
require 'active_support/core_ext/time/conversions'
14 15
require 'active_support/core_ext/date_time/conversions'
require 'active_support/core_ext/date/conversions'
16
require 'set'
J
Joshua Peek 已提交
17

18
module ActiveSupport
19 20 21 22 23 24
  class << self
    delegate :use_standard_json_time_format, :use_standard_json_time_format=,
      :escape_html_entities_in_json, :escape_html_entities_in_json=,
      :to => :'ActiveSupport::JSON::Encoding'
  end

25
  module JSON
26
    # matches YAML-formatted dates
27
    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})?))$/
28 29 30 31

    # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
    def self.encode(value, options = nil)
      Encoding::Encoder.new(options).encode(value)
32 33
    end

34 35 36 37 38 39 40
    module Encoding #:nodoc:
      class CircularReferenceError < StandardError; end

      class Encoder
        attr_reader :options

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

45
        def encode(value, use_options = true)
46
          check_for_circular_references(value) do
47 48 49 50 51 52
            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
53
        def as_json(value, use_options = true)
54
          check_for_circular_references(value) do
55
            use_options ? value.as_json(options_for(value)) : value.as_json
56 57 58 59 60 61
          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
62
            options.merge(:encoder => self)
63 64
          else
            options
65 66 67 68 69 70 71 72 73
          end
        end

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

        private
          def check_for_circular_references(value)
74
            unless @seen.add?(value.__id__)
75 76 77 78
              raise CircularReferenceError, 'object references itself'
            end
            yield
          ensure
79
            @seen.delete(value.__id__)
80 81 82 83 84
          end
      end


      ESCAPED_CHARS = {
85 86 87 88 89 90 91 92 93
        "\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',
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
        "\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

        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
115
              /[\x00-\x1F"\\><&]/
116
            else
117
              /[\x00-\x1F"\\]/
118 119 120 121
            end
        end

        def escape(string)
122
          if string.respond_to?(:force_encoding)
123
            string = string.encode(::Encoding::UTF_8, :undef => :replace).force_encoding(::Encoding::BINARY)
124
          end
125 126 127
          json = string.
            gsub(escape_regex) { |s| ESCAPED_CHARS[s] }.
            gsub(/([\xC0-\xDF][\x80-\xBF]|
128 129
                   [\xE0-\xEF][\x80-\xBF]{2}|
                   [\xF0-\xF7][\x80-\xBF]{3})+/nx) { |s|
130 131
            s.unpack("U*").pack("n*").unpack("H*")[0].gsub(/.{4}/n, '\\\\u\&')
          }
132 133 134
          json = %("#{json}")
          json.force_encoding(::Encoding::UTF_8) if json.respond_to?(:force_encoding)
          json
135 136 137
        end
      end

138 139
      self.use_standard_json_time_format = true
      self.escape_html_entities_in_json  = false
140 141 142 143
    end
  end
end

144
class Object
145 146 147 148 149 150 151
  def as_json(options = nil) #:nodoc:
    if respond_to?(:to_hash)
      to_hash
    else
      instance_values
    end
  end
152 153
end

A
Alexey Nayden 已提交
154 155 156 157 158 159
class Struct
  def as_json(options = nil) #:nodoc:
    Hash[members.zip(values)]
  end
end

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
class TrueClass
  AS_JSON = ActiveSupport::JSON::Variable.new('true').freeze
  def as_json(options = nil) AS_JSON end #:nodoc:
end

class FalseClass
  AS_JSON = ActiveSupport::JSON::Variable.new('false').freeze
  def as_json(options = nil) AS_JSON end #:nodoc:
end

class NilClass
  AS_JSON = ActiveSupport::JSON::Variable.new('null').freeze
  def as_json(options = nil) AS_JSON end #:nodoc:
end

class String
  def as_json(options = nil) self end #:nodoc:
  def encode_json(encoder) encoder.escape(self) end #:nodoc:
end

class Symbol
  def as_json(options = nil) to_s end #:nodoc:
end

class Numeric
  def as_json(options = nil) self end #:nodoc:
  def encode_json(encoder) to_s end #:nodoc:
end

189
class BigDecimal
190 191 192 193 194 195 196 197
  # 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.
198 199 200
  def as_json(options = nil) to_s end #:nodoc:
end

201
class Regexp
202
  def as_json(options = nil) to_s end #:nodoc:
203 204 205
end

module Enumerable
206 207 208
  def as_json(options = nil) #:nodoc:
    to_a.as_json(options)
  end
209 210 211
end

class Array
212 213 214
  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)
215
    map { |v| encoder.as_json(v, options) }
216 217 218 219 220 221
  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
222 223 224 225
end

class Hash
  def as_json(options = nil) #:nodoc:
226 227
    # create a subset of the hash by applying :only or :except
    subset = if options
228 229 230 231 232 233
      if attrs = options[:only]
        slice(*Array.wrap(attrs))
      elsif attrs = options[:except]
        except(*Array.wrap(attrs))
      else
        self
234
      end
235 236 237
    else
      self
    end
238 239 240

    # 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)
E
Emilio Tagua 已提交
241
    result = self.is_a?(ActiveSupport::OrderedHash) ? ActiveSupport::OrderedHash : Hash
242
    result[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }]
243 244 245
  end

  def encode_json(encoder)
246 247 248 249 250 251 252
    # 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)}" } * ','}}"
253 254 255 256 257 258 259 260
  end
end

class Time
  def as_json(options = nil) #:nodoc:
    if ActiveSupport.use_standard_json_time_format
      xmlschema
    else
261
      %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
262 263 264
    end
  end
end
265

266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
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