* Add pluggable JSON backends with support for the JSON gem. [rick]

Example: ActiveSupport::JSON.backend = "JSONGem"

  All internal Rails JSON encoding is now handled by ActiveSupport::JSON.encode().  Use of #to_json is not recommended, as it may clash with other libraries that overwrite it.  However, you can recover Rails specific functionality
  if you really want to use #to_json.

    gem 'json'
    ActiveSupport::JSON.backend = "JSONGem"

    class ActiveRecord::Base
      alias to_json rails_to_json
    end
This commit is contained in:
rick 2009-04-23 00:08:40 -07:00
parent bab2bfa692
commit 3c4c6bd0df
32 changed files with 384 additions and 239 deletions

@ -254,7 +254,7 @@ def render(options = nil, extra_options = {}, &block) #:doc:
render_for_text(js)
elsif json = options[:json]
json = json.to_json unless json.is_a?(String)
json = ActiveSupport::JSON.encode(json) unless json.is_a?(String)
json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
response.content_type ||= Mime::JSON
render_for_text(json)

@ -973,7 +973,7 @@ def drop_receiving(id, options = {})
def loop_on_multiple_args(method, ids)
record(ids.size>1 ?
"#{javascript_object_for(ids)}.each(#{method})" :
"#{method}(#{ids.first.to_json})")
"#{method}(#{ActiveSupport::JSON.encode(ids.first)})")
end
def page
@ -997,7 +997,7 @@ def render(*options_for_render)
end
def javascript_object_for(object)
object.respond_to?(:to_json) ? object.to_json : object.inspect
ActiveSupport::JSON.encode(object)
end
def arguments_for_call(arguments, block = nil)
@ -1139,7 +1139,7 @@ def append_to_function_chain!(call)
class JavaScriptElementProxy < JavaScriptProxy #:nodoc:
def initialize(generator, id)
@id = id
super(generator, "$(#{id.to_json})")
super(generator, "$(#{ActiveSupport::JSON.encode(id)})")
end
# Allows access of element attributes through +attribute+. Examples:
@ -1184,10 +1184,12 @@ def respond_to?(method)
true
end
def to_json(options = nil)
def rails_to_json(options = nil)
@variable
end
alias to_json rails_to_json
private
def append_to_function_chain!(call)
@generator << @variable if @empty
@ -1211,7 +1213,7 @@ def each_slice(variable, number, &block)
enumerate :eachSlice, :variable => variable, :method_args => [number], :yield_args => %w(value index), :return => true, &block
else
add_variable_assignment!(variable)
append_enumerable_function!("eachSlice(#{number.to_json});")
append_enumerable_function!("eachSlice(#{ActiveSupport::JSON.encode(number)});")
end
end
@ -1232,7 +1234,7 @@ def inject(variable, memo, &block)
def pluck(variable, property)
add_variable_assignment!(variable)
append_enumerable_function!("pluck(#{property.to_json});")
append_enumerable_function!("pluck(#{ActiveSupport::JSON.encode(property)});")
end
def zip(variable, *arguments, &block)
@ -1296,7 +1298,7 @@ def append_enumerable_function!(call)
class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\
def initialize(generator, pattern)
super(generator, "$$(#{pattern.to_json})")
super(generator, "$$(#{ActiveSupport::JSON.encode(pattern)})")
end
end
end

@ -43,7 +43,7 @@ module ScriptaculousHelper
# You can change the behaviour with various options, see
# http://script.aculo.us for more documentation.
def visual_effect(name, element_id = false, js_options = {})
element = element_id ? element_id.to_json : "element"
element = element_id ? ActiveSupport::JSON.encode(element_id) : "element"
js_options[:queue] = if js_options[:queue].is_a?(Hash)
'{' + js_options[:queue].map {|k, v| k == :limit ? "#{k}:#{v}" : "#{k}:'#{v}'" }.join(',') + '}'
@ -138,7 +138,7 @@ def sortable_element(element_id, options = {})
end
def sortable_element_js(element_id, options = {}) #:nodoc:
options[:with] ||= "Sortable.serialize(#{element_id.to_json})"
options[:with] ||= "Sortable.serialize(#{ActiveSupport::JSON.encode(element_id)})"
options[:onUpdate] ||= "function(){" + remote_function(options) + "}"
options.delete_if { |key, value| PrototypeHelper::AJAX_OPTIONS.include?(key) }
@ -149,7 +149,7 @@ def sortable_element_js(element_id, options = {}) #:nodoc:
options[:containment] = array_or_string_for_javascript(options[:containment]) if options[:containment]
options[:only] = array_or_string_for_javascript(options[:only]) if options[:only]
%(Sortable.create(#{element_id.to_json}, #{options_for_javascript(options)});)
%(Sortable.create(#{ActiveSupport::JSON.encode(element_id)}, #{options_for_javascript(options)});)
end
# Makes the element with the DOM ID specified by +element_id+ draggable.
@ -164,7 +164,7 @@ def draggable_element(element_id, options = {})
end
def draggable_element_js(element_id, options = {}) #:nodoc:
%(new Draggable(#{element_id.to_json}, #{options_for_javascript(options)});)
%(new Draggable(#{ActiveSupport::JSON.encode(element_id)}, #{options_for_javascript(options)});)
end
# Makes the element with the DOM ID specified by +element_id+ receive
@ -219,7 +219,7 @@ def drop_receiving_element_js(element_id, options = {}) #:nodoc:
# Confirmation happens during the onDrop callback, so it can be removed from the options
options.delete(:confirm) if options[:confirm]
%(Droppables.add(#{element_id.to_json}, #{options_for_javascript(options)});)
%(Droppables.add(#{ActiveSupport::JSON.encode(element_id)}, #{options_for_javascript(options)});)
end
end
end

@ -194,19 +194,19 @@ def accessing_controller_name_in_template
end
def render_json_hello_world
render :json => {:hello => 'world'}.to_json
render :json => ActiveSupport::JSON.encode(:hello => 'world')
end
def render_json_hello_world_with_callback
render :json => {:hello => 'world'}.to_json, :callback => 'alert'
render :json => ActiveSupport::JSON.encode(:hello => 'world'), :callback => 'alert'
end
def render_json_with_custom_content_type
render :json => {:hello => 'world'}.to_json, :content_type => 'text/javascript'
render :json => ActiveSupport::JSON.encode(:hello => 'world'), :content_type => 'text/javascript'
end
def render_symbol_json
render :json => {:hello => 'world'}.to_json
render :json => ActiveSupport::JSON.encode(:hello => 'world')
end
def render_json_with_render_to_string
@ -875,31 +875,31 @@ def test_render_file_from_template
def test_render_json
get :render_json_hello_world
assert_equal '{"hello": "world"}', @response.body
assert_equal '{"hello":"world"}', @response.body
assert_equal 'application/json', @response.content_type
end
def test_render_json_with_callback
get :render_json_hello_world_with_callback
assert_equal 'alert({"hello": "world"})', @response.body
assert_equal 'alert({"hello":"world"})', @response.body
assert_equal 'application/json', @response.content_type
end
def test_render_json_with_custom_content_type
get :render_json_with_custom_content_type
assert_equal '{"hello": "world"}', @response.body
assert_equal '{"hello":"world"}', @response.body
assert_equal 'text/javascript', @response.content_type
end
def test_render_symbol_json
get :render_symbol_json
assert_equal '{"hello": "world"}', @response.body
assert_equal '{"hello":"world"}', @response.body
assert_equal 'application/json', @response.content_type
end
def test_render_json_with_render_to_string
get :render_json_with_render_to_string
assert_equal '{"hello": "partial html"}', @response.body
assert_equal '{"hello":"partial html"}', @response.body
assert_equal 'application/json', @response.content_type
end

@ -328,28 +328,28 @@ def test_replace_element_with_string
def test_remove
assert_equal 'Element.remove("foo");',
@generator.remove('foo')
assert_equal '["foo", "bar", "baz"].each(Element.remove);',
assert_equal '["foo","bar","baz"].each(Element.remove);',
@generator.remove('foo', 'bar', 'baz')
end
def test_show
assert_equal 'Element.show("foo");',
@generator.show('foo')
assert_equal '["foo", "bar", "baz"].each(Element.show);',
assert_equal '["foo","bar","baz"].each(Element.show);',
@generator.show('foo', 'bar', 'baz')
end
def test_hide
assert_equal 'Element.hide("foo");',
@generator.hide('foo')
assert_equal '["foo", "bar", "baz"].each(Element.hide);',
assert_equal '["foo","bar","baz"].each(Element.hide);',
@generator.hide('foo', 'bar', 'baz')
end
def test_toggle
assert_equal 'Element.toggle("foo");',
@generator.toggle('foo')
assert_equal '["foo", "bar", "baz"].each(Element.toggle);',
assert_equal '["foo","bar","baz"].each(Element.toggle);',
@generator.toggle('foo', 'bar', 'baz')
end
@ -386,7 +386,7 @@ def test_to_s
assert_equal <<-EOS.chomp, @generator.to_s
Element.insert("element", { top: "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E" });
Element.insert("element", { bottom: "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E" });
["foo", "bar"].each(Element.remove);
["foo","bar"].each(Element.remove);
Element.update("baz", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E");
EOS
end
@ -555,8 +555,8 @@ def test_collection_proxy_with_zip
end
assert_equal <<-EOS.strip, @generator.to_s
var a = [1, 2, 3].zip([4, 5, 6], [7, 8, 9]);
var b = [1, 2, 3].zip([4, 5, 6], [7, 8, 9], function(array) {
var a = [1, 2, 3].zip([4,5,6], [7,8,9]);
var b = [1, 2, 3].zip([4,5,6], [7,8,9], function(array) {
return array.reverse();
});
EOS
@ -607,7 +607,7 @@ def test_debug_rjs
def test_literal
literal = @generator.literal("function() {}")
assert_equal "function() {}", literal.to_json
assert_equal "function() {}", ActiveSupport::JSON.encode(literal)
assert_equal "", @generator.to_s
end

@ -74,13 +74,17 @@ def self.included(base)
# {"comments": [{"body": "Don't think too hard"}],
# "title": "So I was thinking"}]}
def to_json(options = {})
json = JsonSerializer.new(self, options).to_s
if include_root_in_json
"{#{self.class.json_class_name}: #{JsonSerializer.new(self, options).to_s}}"
"{#{self.class.json_class_name}:#{json}}"
else
JsonSerializer.new(self, options).to_s
json
end
end
# For compatibility with ActiveSupport::JSON.encode
alias rails_to_json to_json
def from_json(json)
self.attributes = ActiveSupport::JSON.decode(json)
self
@ -88,7 +92,7 @@ def from_json(json)
class JsonSerializer < ActiveRecord::Serialization::Serializer #:nodoc:
def serialize
serializable_record.to_json
ActiveSupport::JSON.encode(serializable_record)
end
end

@ -26,19 +26,19 @@ def test_should_demodulize_root_in_json
NamespacedContact.include_root_in_json = true
@contact = NamespacedContact.new :name => 'whatever'
json = @contact.to_json
assert_match %r{^\{"namespaced_contact": \{}, json
assert_match %r{^\{"namespaced_contact":\{}, json
end
def test_should_include_root_in_json
Contact.include_root_in_json = true
json = @contact.to_json
assert_match %r{^\{"contact": \{}, json
assert_match %r{"name": "Konata Izumi"}, json
assert_match %r{"age": 16}, json
assert json.include?(%("created_at": #{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
assert_match %r{"awesome": true}, json
assert_match %r{"preferences": \{"shows": "anime"\}}, json
assert_match %r{^\{"contact":\{}, json
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
assert_match %r{"awesome":true}, json
assert_match %r{"preferences":\{"shows":"anime"\}}, json
ensure
Contact.include_root_in_json = false
end
@ -46,31 +46,31 @@ def test_should_include_root_in_json
def test_should_encode_all_encodable_attributes
json = @contact.to_json
assert_match %r{"name": "Konata Izumi"}, json
assert_match %r{"age": 16}, json
assert json.include?(%("created_at": #{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
assert_match %r{"awesome": true}, json
assert_match %r{"preferences": \{"shows": "anime"\}}, json
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
assert_match %r{"awesome":true}, json
assert_match %r{"preferences":\{"shows":"anime"\}}, json
end
def test_should_allow_attribute_filtering_with_only
json = @contact.to_json(:only => [:name, :age])
assert_match %r{"name": "Konata Izumi"}, json
assert_match %r{"age": 16}, json
assert_no_match %r{"awesome": true}, json
assert !json.include?(%("created_at": #{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
assert_no_match %r{"preferences": \{"shows": "anime"\}}, json
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
assert_no_match %r{"awesome":true}, json
assert !json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
assert_no_match %r{"preferences":\{"shows":"anime"\}}, json
end
def test_should_allow_attribute_filtering_with_except
json = @contact.to_json(:except => [:name, :age])
assert_no_match %r{"name": "Konata Izumi"}, json
assert_no_match %r{"age": 16}, json
assert_match %r{"awesome": true}, json
assert json.include?(%("created_at": #{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
assert_match %r{"preferences": \{"shows": "anime"\}}, json
assert_no_match %r{"name":"Konata Izumi"}, json
assert_no_match %r{"age":16}, json
assert_match %r{"awesome":true}, json
assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
assert_match %r{"preferences":\{"shows":"anime"\}}, json
end
def test_methods_are_called_on_object
@ -79,12 +79,12 @@ def @contact.label; "Has cheezburger"; end
def @contact.favorite_quote; "Constraints are liberating"; end
# Single method.
assert_match %r{"label": "Has cheezburger"}, @contact.to_json(:only => :name, :methods => :label)
assert_match %r{"label":"Has cheezburger"}, @contact.to_json(:only => :name, :methods => :label)
# Both methods.
methods_json = @contact.to_json(:only => :name, :methods => [:label, :favorite_quote])
assert_match %r{"label": "Has cheezburger"}, methods_json
assert_match %r{"favorite_quote": "Constraints are liberating"}, methods_json
assert_match %r{"label":"Has cheezburger"}, methods_json
assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json
end
end
@ -99,42 +99,42 @@ def setup
def test_includes_uses_association_name
json = @david.to_json(:include => :posts)
assert_match %r{"posts": \[}, json
assert_match %r{"posts":\[}, json
assert_match %r{"id": 1}, json
assert_match %r{"name": "David"}, json
assert_match %r{"id":1}, json
assert_match %r{"name":"David"}, json
assert_match %r{"author_id": 1}, json
assert_match %r{"title": "Welcome to the weblog"}, json
assert_match %r{"body": "Such a lovely day"}, json
assert_match %r{"author_id":1}, json
assert_match %r{"title":"Welcome to the weblog"}, json
assert_match %r{"body":"Such a lovely day"}, json
assert_match %r{"title": "So I was thinking"}, json
assert_match %r{"body": "Like I hopefully always am"}, json
assert_match %r{"title":"So I was thinking"}, json
assert_match %r{"body":"Like I hopefully always am"}, json
end
def test_includes_uses_association_name_and_applies_attribute_filters
json = @david.to_json(:include => { :posts => { :only => :title } })
assert_match %r{"name": "David"}, json
assert_match %r{"posts": \[}, json
assert_match %r{"name":"David"}, json
assert_match %r{"posts":\[}, json
assert_match %r{"title": "Welcome to the weblog"}, json
assert_no_match %r{"body": "Such a lovely day"}, json
assert_match %r{"title":"Welcome to the weblog"}, json
assert_no_match %r{"body":"Such a lovely day"}, json
assert_match %r{"title": "So I was thinking"}, json
assert_no_match %r{"body": "Like I hopefully always am"}, json
assert_match %r{"title":"So I was thinking"}, json
assert_no_match %r{"body":"Like I hopefully always am"}, json
end
def test_includes_fetches_second_level_associations
json = @david.to_json(:include => { :posts => { :include => { :comments => { :only => :body } } } })
assert_match %r{"name": "David"}, json
assert_match %r{"posts": \[}, json
assert_match %r{"name":"David"}, json
assert_match %r{"posts":\[}, json
assert_match %r{"comments": \[}, json
assert_match %r{\{"body": "Thank you again for the welcome"\}}, json
assert_match %r{\{"body": "Don't think too hard"\}}, json
assert_no_match %r{"post_id": }, json
assert_match %r{"comments":\[}, json
assert_match %r{\{"body":"Thank you again for the welcome"\}}, json
assert_match %r{\{"body":"Don't think too hard"\}}, json
assert_no_match %r{"post_id":}, json
end
def test_includes_fetches_nth_level_associations
@ -151,11 +151,11 @@ def test_includes_fetches_nth_level_associations
}
})
assert_match %r{"name": "David"}, json
assert_match %r{"posts": \[}, json
assert_match %r{"name":"David"}, json
assert_match %r{"posts":\[}, json
assert_match %r{"taggings": \[}, json
assert_match %r{"tag": \{"name": "General"\}}, json
assert_match %r{"taggings":\[}, json
assert_match %r{"tag":\{"name":"General"\}}, json
end
def test_should_not_call_methods_on_associations_that_dont_respond
@ -163,20 +163,20 @@ def @david.favorite_quote; "Constraints are liberating"; end
json = @david.to_json(:include => :posts, :methods => :favorite_quote)
assert !@david.posts.first.respond_to?(:favorite_quote)
assert_match %r{"favorite_quote": "Constraints are liberating"}, json
assert_equal %r{"favorite_quote": }.match(json).size, 1
assert_match %r{"favorite_quote":"Constraints are liberating"}, json
assert_equal %r{"favorite_quote":}.match(json).size, 1
end
def test_should_allow_only_option_for_list_of_authors
authors = [@david, @mary]
assert_equal %([{"name": "David"}, {"name": "Mary"}]), authors.to_json(:only => :name)
assert_equal %([{"name":"David"},{"name":"Mary"}]), authors.to_json(:only => :name)
end
def test_should_allow_except_option_for_list_of_authors
authors = [@david, @mary]
assert_equal %([{"id": 1}, {"id": 2}]), authors.to_json(:except => [:name, :author_address_id, :author_address_extra_id])
assert_equal %([{"id":1},{"id":2}]), authors.to_json(:except => [:name, :author_address_id, :author_address_extra_id])
end
def test_should_allow_includes_for_list_of_authors
@ -188,8 +188,8 @@ def test_should_allow_includes_for_list_of_authors
}
)
['"name": "David"', '"posts": [', '{"id": 1}', '{"id": 2}', '{"id": 4}',
'{"id": 5}', '{"id": 6}', '"name": "Mary"', '"posts": [{"id": 7}]'].each do |fragment|
['"name":"David"', '"posts":[', '{"id":1}', '{"id":2}', '{"id":4}',
'{"id":5}', '{"id":6}', '"name":"Mary"', '"posts":[{"id":7}]'].each do |fragment|
assert json.include?(fragment), json
end
end
@ -200,6 +200,6 @@ def test_should_allow_options_for_hash_of_authors
2 => @mary
}
assert_equal %({"1": {"name": "David"}}), authors_hash.to_json(:only => [1, :name])
assert_equal %({"1":{"name":"David"}}), authors_hash.to_json(:only => [1, :name])
end
end

@ -887,9 +887,12 @@ def to_xml(options={})
# person.to_json(:except => ["first_name"])
# # => {"last_name": "Smith"}
def to_json(options={})
attributes.to_json(options)
ActiveSupport::JSON.encode(attributes, options)
end
# For compatibility with ActiveSupport::JSON.encode
alias rails_to_json to_json
# Returns the serialized string representation of the resource in the configured
# serialization format specified in ActiveResource::Base.format. The options
# applicable depend on the configured encoding format.

@ -1,5 +1,20 @@
*Edge*
* Add ActiveSupport.parse_json_times to disable time parsing in JSON backends that don't support it or don't need it. [rick]
* Add pluggable JSON backends with support for the JSON gem. [rick]
Example: ActiveSupport::JSON.backend = "JSONGem"
All internal Rails JSON encoding is now handled by ActiveSupport::JSON.encode(). Use of #to_json is not recommended, as it may clash with other libraries that overwrite it. However, you can recover Rails specific functionality
if you really want to use #to_json.
gem 'json'
ActiveSupport::JSON.backend = "JSONGem"
class ActiveRecord::Base
alias to_json rails_to_json
end
* require 'active_support' no longer orders the whole menu of core extensions. Ask for just what you need: e.g. require 'active_support/core/time' to use timezones, durations, and stdlib date/time extensions. [Jeremy Kemper]
* Removed rarely-used DRb cache store. [Jeremy Kemper]

@ -1,8 +1,37 @@
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/module/attribute_accessors'
module ActiveSupport
# If true, use ISO 8601 format for dates and times. Otherwise, fall back to the Active Support legacy format.
mattr_accessor :use_standard_json_time_format
# Look for and parse json strings that look like ISO 8601 times.
mattr_accessor :parse_json_times
module JSON
# matches YAML-formatted dates
DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[ \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/
class << self
attr_reader :backend
delegate :decode, :to => :backend
def backend=(name)
if name.is_a?(Module)
@backend = name
else
require "active_support/json/backends/#{name.to_s.downcase}.rb"
@backend = ActiveSupport::JSON::Backends::const_get(name)
end
end
def with_backend(name)
old_backend, self.backend = backend, name
yield
ensure
self.backend = old_backend
end
end
end
class << self
def escape_html_entities_in_json
@ -19,7 +48,8 @@ def escape_html_entities_in_json=(value)
@escape_html_entities_in_json = value
end
end
JSON.backend = 'Yaml'
end
require 'active_support/json/encoding'
require 'active_support/json/decoding'
require 'active_support/json/encoding'

@ -0,0 +1,36 @@
module ActiveSupport
module JSON
ParseError = ::JSON::ParserError
module Backends
module JSONGem
extend self
# Converts a JSON string into a Ruby object.
def decode(json)
data = ::JSON.parse(json)
if ActiveSupport.parse_json_times
convert_dates_from(data)
else
data
end
end
private
def convert_dates_from(data)
case data
when DATE_REGEX
DateTime.parse(data)
when Array
data.map! { |d| convert_dates_from(d) }
when Hash
data.each do |key, value|
data[key] = convert_dates_from(value)
end
else data
end
end
end
end
end
end

@ -0,0 +1,83 @@
require 'active_support/core_ext/string/starts_ends_with'
module ActiveSupport
module JSON
class ParseError < StandardError
end
module Backends
module Yaml
extend self
# Converts a JSON string into a Ruby object.
def decode(json)
YAML.load(convert_json_to_yaml(json))
rescue ArgumentError => e
raise ParseError, "Invalid JSON string"
end
protected
# Ensure that ":" and "," are always followed by a space
def convert_json_to_yaml(json) #:nodoc:
require 'strscan' unless defined? ::StringScanner
scanner, quoting, marks, pos, times = ::StringScanner.new(json), false, [], nil, []
while scanner.scan_until(/(\\['"]|['":,\\]|\\.)/)
case char = scanner[1]
when '"', "'"
if !quoting
quoting = char
pos = scanner.pos
elsif quoting == char
if json[pos..scanner.pos-2] =~ DATE_REGEX
# found a date, track the exact positions of the quotes so we can remove them later.
# oh, and increment them for each current mark, each one is an extra padded space that bumps
# the position in the final YAML output
total_marks = marks.size
times << pos+total_marks << scanner.pos+total_marks
end
quoting = false
end
when ":",","
marks << scanner.pos - 1 unless quoting
end
end
if marks.empty?
json.gsub(/\\([\\\/]|u[[:xdigit:]]{4})/) do
ustr = $1
if ustr.start_with?('u')
[ustr[1..-1].to_i(16)].pack("U")
elsif ustr == '\\'
'\\\\'
else
ustr
end
end
else
left_pos = [-1].push(*marks)
right_pos = marks << scanner.pos + scanner.rest_size
output = []
left_pos.each_with_index do |left, i|
scanner.pos = left.succ
output << scanner.peek(right_pos[i] - scanner.pos + 1).gsub(/\\([\\\/]|u[[:xdigit:]]{4})/) do
ustr = $1
if ustr.start_with?('u')
[ustr[1..-1].to_i(16)].pack("U")
elsif ustr == '\\'
'\\\\'
else
ustr
end
end
end
output = output * " "
times.each { |i| output[i-1] = ' ' }
output.gsub!(/\\\//, '/')
output
end
end
end
end
end
end

@ -1,82 +0,0 @@
require 'active_support/core_ext/string/starts_ends_with'
module ActiveSupport
module JSON
class ParseError < StandardError
end
class << self
# Converts a JSON string into a Ruby object.
def decode(json)
YAML.load(convert_json_to_yaml(json))
rescue ArgumentError => e
raise ParseError, "Invalid JSON string"
end
protected
# matches YAML-formatted dates
DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[ \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?)?)$/
# Ensure that ":" and "," are always followed by a space
def convert_json_to_yaml(json) #:nodoc:
require 'strscan' unless defined? ::StringScanner
scanner, quoting, marks, pos, times = ::StringScanner.new(json), false, [], nil, []
while scanner.scan_until(/(\\['"]|['":,\\]|\\.)/)
case char = scanner[1]
when '"', "'"
if !quoting
quoting = char
pos = scanner.pos
elsif quoting == char
if json[pos..scanner.pos-2] =~ DATE_REGEX
# found a date, track the exact positions of the quotes so we can remove them later.
# oh, and increment them for each current mark, each one is an extra padded space that bumps
# the position in the final YAML output
total_marks = marks.size
times << pos+total_marks << scanner.pos+total_marks
end
quoting = false
end
when ":",","
marks << scanner.pos - 1 unless quoting
end
end
if marks.empty?
json.gsub(/\\([\\\/]|u[[:xdigit:]]{4})/) do
ustr = $1
if ustr.start_with?('u')
[ustr[1..-1].to_i(16)].pack("U")
elsif ustr == '\\'
'\\\\'
else
ustr
end
end
else
left_pos = [-1].push(*marks)
right_pos = marks << scanner.pos + scanner.rest_size
output = []
left_pos.each_with_index do |left, i|
scanner.pos = left.succ
output << scanner.peek(right_pos[i] - scanner.pos + 1).gsub(/\\([\\\/]|u[[:xdigit:]]{4})/) do
ustr = $1
if ustr.start_with?('u')
[ustr[1..-1].to_i(16)].pack("U")
elsif ustr == '\\'
'\\\\'
else
ustr
end
end
end
output = output * " "
times.each { |i| output[i-1] = ' ' }
output.gsub!(/\\\//, '/')
output
end
end
end
end
end

@ -11,11 +11,13 @@ class Date
# # With ActiveSupport.use_standard_json_time_format = false
# Date.new(2005,2,1).to_json
# # => "2005/02/01"
def to_json(options = nil)
def rails_to_json(options = nil)
if ActiveSupport.use_standard_json_time_format
%("#{strftime("%Y-%m-%d")}")
else
%("#{strftime("%Y/%m/%d")}")
end
end
alias to_json rails_to_json
end

@ -11,11 +11,13 @@ class DateTime
# # With ActiveSupport.use_standard_json_time_format = false
# DateTime.civil(2005,2,1,15,15,10).to_json
# # => "2005/02/01 15:15:10 +0000"
def to_json(options = nil)
def rails_to_json(options = nil)
if ActiveSupport.use_standard_json_time_format
xmlschema.inspect
else
strftime('"%Y/%m/%d %H:%M:%S %z"')
end
end
alias to_json rails_to_json
end

@ -6,7 +6,9 @@ module Enumerable
# # => users.to_json(:only => :name)
#
# will pass the <tt>:only => :name</tt> option to each user.
def to_json(options = {}) #:nodoc:
"[#{map { |value| ActiveSupport::JSON.encode(value, options) } * ', '}]"
def rails_to_json(options = {}) #:nodoc:
"[#{map { |value| ActiveSupport::JSON.encode(value, options) } * ','}]"
end
alias to_json rails_to_json
end

@ -1,5 +1,7 @@
class FalseClass
def to_json(options = nil) #:nodoc:
def rails_to_json(options = nil) #:nodoc:
'false'
end
alias to_json rails_to_json
end

@ -30,7 +30,7 @@ class Hash
# would pass the <tt>:include => :posts</tt> option to <tt>users</tt>,
# allowing the posts association in the User model to be converted to JSON
# as well.
def to_json(options = {}) #:nodoc:
def rails_to_json(options = {}) #:nodoc:
hash_keys = self.keys
if except = options[:except]
@ -41,8 +41,10 @@ def to_json(options = {}) #:nodoc:
result = '{'
result << hash_keys.map do |key|
"#{ActiveSupport::JSON.encode(key.to_s)}: #{ActiveSupport::JSON.encode(self[key], options)}"
end * ', '
"#{ActiveSupport::JSON.encode(key.to_s)}:#{ActiveSupport::JSON.encode(self[key], options)}"
end * ','
result << '}'
end
alias to_json rails_to_json
end

@ -1,5 +1,7 @@
class NilClass
def to_json(options = nil) #:nodoc:
def rails_to_json(options = nil) #:nodoc:
'null'
end
alias to_json rails_to_json
end

@ -1,5 +1,7 @@
class Numeric
def to_json(options = nil) #:nodoc:
def rails_to_json(options = nil) #:nodoc:
to_s
end
alias to_json rails_to_json
end

@ -2,7 +2,9 @@
class Object
# Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
def to_json(options = {})
def rails_to_json(options = {})
ActiveSupport::JSON.encode(instance_values, options)
end
alias to_json rails_to_json
end

@ -1,5 +1,7 @@
class Regexp
def to_json(options = nil) #:nodoc:
def rails_to_json(options = nil) #:nodoc:
inspect
end
alias to_json rails_to_json
end

@ -22,7 +22,7 @@ module Encoding
ActiveSupport.escape_html_entities_in_json = true
class String
def to_json(options = nil) #:nodoc:
def rails_to_json(options = nil) #:nodoc:
json = '"' + gsub(ActiveSupport::JSON::Encoding.escape_regex) { |s|
ActiveSupport::JSON::Encoding::ESCAPED_CHARS[s]
}
@ -33,4 +33,6 @@ def to_json(options = nil) #:nodoc:
s.unpack("U*").pack("n*").unpack("H*")[0].gsub(/.{4}/, '\\\\u\&')
} + '"'
end
alias to_json rails_to_json
end

@ -1,5 +1,7 @@
class Symbol
def to_json(options = {}) #:nodoc:
def rails_to_json(options = {}) #:nodoc:
ActiveSupport::JSON.encode(to_s, options)
end
alias to_json rails_to_json
end

@ -13,11 +13,13 @@ class Time
# # With ActiveSupport.use_standard_json_time_format = false
# Time.utc(2005,2,1,15,15,10).to_json
# # => "2005/02/01 15:15:10 +0000"
def to_json(options = nil)
def rails_to_json(options = nil)
if ActiveSupport.use_standard_json_time_format
xmlschema.inspect
else
%("#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}")
end
end
alias to_json rails_to_json
end

@ -1,5 +1,7 @@
class TrueClass
def to_json(options = nil) #:nodoc:
def rails_to_json(options = nil) #:nodoc:
'true'
end
alias to_json rails_to_json
end

@ -8,7 +8,7 @@ def self.encode(value, options = {})
seen = (options[:seen] ||= [])
raise CircularReferenceError, 'object references itself' if seen.include?(value)
seen << value
value.send(:to_json, options)
value.send(:rails_to_json, options)
ensure
seen.pop
end

@ -2,7 +2,7 @@ module ActiveSupport
module JSON
# A string that returns itself as its JSON-encoded form.
class Variable < String
def to_json(options=nil)
def rails_to_json(options=nil)
self
end
end

@ -130,7 +130,7 @@ def xmlschema(fraction_digits = 0)
# # With ActiveSupport.use_standard_json_time_format = false
# Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
# # => "2005/02/01 15:15:10 +0000"
def to_json(options = nil)
def rails_to_json(options = nil)
if !ActiveSupport.respond_to?(:use_standard_json_time_format) || ActiveSupport.use_standard_json_time_format
xmlschema.inspect
else
@ -138,6 +138,8 @@ def to_json(options = nil)
end
end
alias to_json rails_to_json
def to_yaml(options = {})
if options.kind_of?(YAML::Emitter)
utc.to_yaml(options)

@ -57,12 +57,12 @@ def test_zone
end
def test_to_json
assert_equal "\"1999/12/31 19:00:00 -0500\"", @twz.to_json
assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(@twz)
end
def test_to_json_with_use_standard_json_time_format_config_set_to_true
old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, true
assert_equal "\"1999-12-31T19:00:00-05:00\"", @twz.to_json
assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(@twz)
ensure
ActiveSupport.use_standard_json_time_format = old
end

@ -1,49 +1,75 @@
# encoding: UTF-8
require 'abstract_unit'
require 'active_support/json'
require 'active_support/core_ext/kernel/reporting'
class TestJSONDecoding < ActiveSupport::TestCase
TESTS = {
%q({"returnTo":{"\/categories":"\/"}}) => {"returnTo" => {"/categories" => "/"}},
%q({returnTo:{"\/categories":"\/"}}) => {"returnTo" => {"/categories" => "/"}},
%q({"return\\"To\\":":{"\/categories":"\/"}}) => {"return\"To\":" => {"/categories" => "/"}},
%q({"returnTo":{"\/categories":1}}) => {"returnTo" => {"/categories" => 1}},
%({"returnTo":[1,"a"]}) => {"returnTo" => [1, "a"]},
%({"returnTo":[1,"\\"a\\",", "b"]}) => {"returnTo" => [1, "\"a\",", "b"]},
%({a: "'", "b": "5,000"}) => {"a" => "'", "b" => "5,000"},
%({a: "a's, b's and c's", "b": "5,000"}) => {"a" => "a's, b's and c's", "b" => "5,000"},
%({"a": "'", "b": "5,000"}) => {"a" => "'", "b" => "5,000"},
%({"a": "a's, b's and c's", "b": "5,000"}) => {"a" => "a's, b's and c's", "b" => "5,000"},
# multibyte
%({"matzue": "松江", "asakusa": "浅草"}) => {"matzue" => "松江", "asakusa" => "浅草"},
%({a: "2007-01-01"}) => {'a' => Date.new(2007, 1, 1)},
%({a: "2007-01-01 01:12:34 Z"}) => {'a' => Time.utc(2007, 1, 1, 1, 12, 34)},
%({"a": "2007-01-01"}) => {'a' => Date.new(2007, 1, 1)},
%({"a": "2007-01-01 01:12:34 Z"}) => {'a' => Time.utc(2007, 1, 1, 1, 12, 34)},
# no time zone
%({a: "2007-01-01 01:12:34"}) => {'a' => "2007-01-01 01:12:34"},
%({"a": "2007-01-01 01:12:34"}) => {'a' => "2007-01-01 01:12:34"},
# needs to be *exact*
%({a: " 2007-01-01 01:12:34 Z "}) => {'a' => " 2007-01-01 01:12:34 Z "},
%({a: "2007-01-01 : it's your birthday"}) => {'a' => "2007-01-01 : it's your birthday"},
%({"a": " 2007-01-01 01:12:34 Z "}) => {'a' => " 2007-01-01 01:12:34 Z "},
%({"a": "2007-01-01 : it's your birthday"}) => {'a' => "2007-01-01 : it's your birthday"},
%([]) => [],
%({}) => {},
%(1) => 1,
%("") => "",
%("\\"") => "\"",
%(null) => nil,
%(true) => true,
%(false) => false,
%q("http:\/\/test.host\/posts\/1") => "http://test.host/posts/1",
%q("\u003cunicode\u0020escape\u003e") => "<unicode escape>",
%q("\\\\u0020skip double backslashes") => "\\u0020skip double backslashes",
%q({a: "\u003cbr /\u003e"}) => {'a' => "<br />"},
%q({b:["\u003ci\u003e","\u003cb\u003e","\u003cu\u003e"]}) => {'b' => ["<i>","<b>","<u>"]}
%({"a":1}) => {"a" => 1},
%({"a": ""}) => {"a" => ""},
%({"a":"\\""}) => {"a" => "\""},
%({"a": null}) => {"a" => nil},
%({"a": true}) => {"a" => true},
%({"a": false}) => {"a" => false},
%q({"a": "http:\/\/test.host\/posts\/1"}) => {"a" => "http://test.host/posts/1"},
%q({"a": "\u003cunicode\u0020escape\u003e"}) => {"a" => "<unicode escape>"},
%q({"a": "\\\\u0020skip double backslashes"}) => {"a" => "\\u0020skip double backslashes"},
%q({"a": "\u003cbr /\u003e"}) => {'a' => "<br />"},
%q({"b":["\u003ci\u003e","\u003cb\u003e","\u003cu\u003e"]}) => {'b' => ["<i>","<b>","<u>"]}
}
TESTS.each do |json, expected|
test "json decodes #{json}" do
assert_nothing_raised do
assert_equal expected, ActiveSupport::JSON.decode(json)
backends = %w(Yaml)
begin
gem 'json', '>= 1.1'
require 'json'
backends << "JSONGem"
rescue Gem::LoadError
# Skip JSON gem tests
end
backends.each do |backend|
TESTS.each do |json, expected|
test "json decodes #{json} with the #{backend} backend" do
ActiveSupport.parse_json_times = true
silence_warnings do
ActiveSupport::JSON.with_backend backend do
assert_nothing_raised do
assert_equal expected, ActiveSupport::JSON.decode(json)
end
end
end
end
end
end
if backends.include?("JSONGem")
test "json decodes time json with time parsing disabled" do
ActiveSupport.parse_json_times = false
expected = {"a" => "2007-01-01 01:12:34 Z"}
ActiveSupport::JSON.with_backend "JSONGem" do
assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"}))
end
end
end
def test_failed_json_decoding
assert_raise(ActiveSupport::JSON::ParseError) { ActiveSupport::JSON.decode(%({: 1})) }
end

@ -19,14 +19,14 @@ def initialize(a, b)
[ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ],
[ 'http://test.host/posts/1', %("http://test.host/posts/1")]]
ArrayTests = [[ ['a', 'b', 'c'], %([\"a\", \"b\", \"c\"]) ],
[ [1, 'a', :b, nil, false], %([1, \"a\", \"b\", null, false]) ]]
ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ],
[ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]]
SymbolTests = [[ :a, %("a") ],
[ :this, %("this") ],
[ :"a b", %("a b") ]]
ObjectTests = [[ Foo.new(1, 2), %({\"a\": 1, \"b\": 2}) ]]
ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]]
VariableTests = [[ ActiveSupport::JSON::Variable.new('foo'), 'foo'],
[ ActiveSupport::JSON::Variable.new('alert("foo")'), 'alert("foo")']]
@ -47,7 +47,7 @@ def initialize(a, b)
ActiveSupport.escape_html_entities_in_json = class_tests !~ /^Standard/
ActiveSupport.use_standard_json_time_format = class_tests =~ /^Standard/
self.class.const_get(class_tests).each do |pair|
assert_equal pair.last, pair.first.to_json
assert_equal pair.last, ActiveSupport::JSON.encode(pair.first)
end
ensure
ActiveSupport.escape_html_entities_in_json = false
@ -57,45 +57,45 @@ def initialize(a, b)
end
def test_hash_encoding
assert_equal %({\"a\": \"b\"}), { :a => :b }.to_json
assert_equal %({\"a\": 1}), { 'a' => 1 }.to_json
assert_equal %({\"a\": [1, 2]}), { 'a' => [1,2] }.to_json
assert_equal %({"1": 2}), { 1 => 2 }.to_json
assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(:a => :b)
assert_equal %({\"a\":1}), ActiveSupport::JSON.encode('a' => 1)
assert_equal %({\"a\":[1,2]}), ActiveSupport::JSON.encode('a' => [1,2])
assert_equal %({"1":2}), ActiveSupport::JSON.encode(1 => 2)
sorted_json = '{' + {:a => :b, :c => :d}.to_json[1..-2].split(', ').sort.join(', ') + '}'
assert_equal %({\"a\": \"b\", \"c\": \"d\"}), sorted_json
sorted_json = '{' + ActiveSupport::JSON.encode(:a => :b, :c => :d)[1..-2].split(',').sort.join(',') + '}'
assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json
end
def test_utf8_string_encoded_properly_when_kcode_is_utf8
with_kcode 'UTF8' do
assert_equal '"\\u20ac2.99"', '€2.99'.to_json
assert_equal '"\\u270e\\u263a"', '✎☺'.to_json
assert_equal '"\\u20ac2.99"', ActiveSupport::JSON.encode('€2.99')
assert_equal '"\\u270e\\u263a"', ActiveSupport::JSON.encode('✎☺')
end
end
def test_exception_raised_when_encoding_circular_reference
a = [1]
a << a
assert_raise(ActiveSupport::JSON::CircularReferenceError) { a.to_json }
assert_raise(ActiveSupport::JSON::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
end
def test_hash_key_identifiers_are_always_quoted
values = {0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B"}
assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(values.to_json)
assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(ActiveSupport::JSON.encode(values))
end
def test_hash_should_allow_key_filtering_with_only
assert_equal %({"a": 1}), { 'a' => 1, :b => 2, :c => 3 }.to_json(:only => 'a')
assert_equal %({"a":1}), ActiveSupport::JSON.encode({'a' => 1, :b => 2, :c => 3}, :only => 'a')
end
def test_hash_should_allow_key_filtering_with_except
assert_equal %({"b": 2}), { 'foo' => 'bar', :b => 2, :c => 3 }.to_json(:except => ['foo', :c])
assert_equal %({"b":2}), ActiveSupport::JSON.encode({'foo' => 'bar', :b => 2, :c => 3}, :except => ['foo', :c])
end
def test_time_to_json_includes_local_offset
ActiveSupport.use_standard_json_time_format = true
with_env_tz 'US/Eastern' do
assert_equal %("2005-02-01T15:15:10-05:00"), Time.local(2005,2,1,15,15,10).to_json
assert_equal %("2005-02-01T15:15:10-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10))
end
ensure
ActiveSupport.use_standard_json_time_format = false
@ -109,7 +109,7 @@ def test_nested_hash_with_float
:latitude => 123.234
}
}
result = hash.to_json
result = ActiveSupport::JSON.encode(hash)
end
end
@ -134,6 +134,6 @@ def test_enumerable_should_passthrough_options_to_elements
ActiveSupport::JSON.expects(:encode).with(2, json_options)
ActiveSupport::JSON.expects(:encode).with('foo', json_options)
[1, 2, 'foo'].to_json(json_options)
[1, 2, 'foo'].rails_to_json(json_options)
end
end