Refactor ActiveSupport::JSON to be less obtuse. Add support for JSON decoding by way of Syck with ActiveSupport::JSON.decode(json_string). Prevent hash keys that are JavaScript reserved words from being unquoted during encoding.
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@6443 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
3d5c947155
commit
3202fbabe6
|
@ -1,5 +1,7 @@
|
|||
*SVN*
|
||||
|
||||
* Refactor ActiveSupport::JSON to be less obtuse. Add support for JSON decoding by way of Syck with ActiveSupport::JSON.decode(json_string). Prevent hash keys that are JavaScript reserved words from being unquoted during encoding. [Sam Stephenson]
|
||||
|
||||
* alias_method_chain preserves the original method's visibility. #7854 [Jonathan Viney]
|
||||
|
||||
* Update Dependencies to ignore constants inherited from ancestors. Closes #6951. [Nicholas Seckar]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
class Object #:nodoc:
|
||||
class Object
|
||||
# "", " ", nil, [], and {} are blank
|
||||
def blank?
|
||||
def blank? #:nodoc:
|
||||
if respond_to?(:empty?) && respond_to?(:strip)
|
||||
empty? or strip.empty?
|
||||
elsif respond_to?(:empty?)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
class Object #:nodoc:
|
||||
def remove_subclasses_of(*superclasses)
|
||||
class Object
|
||||
def remove_subclasses_of(*superclasses) #:nodoc:
|
||||
Class.remove_class(*subclasses_of(*superclasses))
|
||||
end
|
||||
|
||||
def subclasses_of(*superclasses)
|
||||
def subclasses_of(*superclasses) #:nodoc:
|
||||
subclasses = []
|
||||
ObjectSpace.each_object(Class) do |k|
|
||||
next unless # Exclude this class unless
|
||||
|
@ -16,23 +16,23 @@ class Object #:nodoc:
|
|||
subclasses
|
||||
end
|
||||
|
||||
def extended_by
|
||||
def extended_by #:nodoc:
|
||||
ancestors = class << self; ancestors end
|
||||
ancestors.select { |mod| mod.class == Module } - [ Object, Kernel ]
|
||||
end
|
||||
|
||||
def copy_instance_variables_from(object, exclude = [])
|
||||
def copy_instance_variables_from(object, exclude = []) #:nodoc:
|
||||
exclude += object.protected_instance_variables if object.respond_to? :protected_instance_variables
|
||||
|
||||
instance_variables = object.instance_variables - exclude.map { |name| name.to_s }
|
||||
instance_variables.each { |name| instance_variable_set(name, object.instance_variable_get(name)) }
|
||||
end
|
||||
|
||||
def extend_with_included_modules_from(object)
|
||||
def extend_with_included_modules_from(object) #:nodoc:
|
||||
object.extended_by.each { |mod| extend mod }
|
||||
end
|
||||
|
||||
def instance_values
|
||||
def instance_values #:nodoc:
|
||||
instance_variables.inject({}) do |values, name|
|
||||
values[name[1..-1]] = instance_variable_get(name)
|
||||
values
|
||||
|
@ -40,7 +40,7 @@ class Object #:nodoc:
|
|||
end
|
||||
|
||||
unless defined? instance_exec # 1.9
|
||||
def instance_exec(*arguments, &block)
|
||||
def instance_exec(*arguments, &block) #:nodoc:
|
||||
block.bind(self)[*arguments]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,21 +43,12 @@ class Object
|
|||
yield ActiveSupport::OptionMerger.new(self, options)
|
||||
end
|
||||
|
||||
# Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
|
||||
#
|
||||
# Account.find(1).to_json
|
||||
# => "{attributes: {username: \"foo\", id: \"1\", password: \"bar\"}}"
|
||||
#
|
||||
def to_json
|
||||
ActiveSupport::JSON.encode(self)
|
||||
end
|
||||
|
||||
# A duck-type assistant method. For example, ActiveSupport extends Date
|
||||
# to define an acts_like_date? method, and extends Time to define
|
||||
# acts_like_time?. As a result, we can do "x.acts_like?(:time)" and
|
||||
# "x.acts_like?(:date)" to do duck-type-safe comparisons, since classes that
|
||||
# we want to act like Time simply need to define an acts_like_time? method.
|
||||
def acts_like?(duck)
|
||||
respond_to? :"acts_like_#{duck}?"
|
||||
respond_to? "acts_like_#{duck}?"
|
||||
end
|
||||
end
|
|
@ -480,18 +480,18 @@ class Class
|
|||
end
|
||||
end
|
||||
|
||||
class Object #:nodoc:
|
||||
class Object
|
||||
|
||||
alias_method :load_without_new_constant_marking, :load
|
||||
|
||||
def load(file, *extras)
|
||||
def load(file, *extras) #:nodoc:
|
||||
Dependencies.new_constants_in(Object) { super(file, *extras) }
|
||||
rescue Exception => exception # errors from loading file
|
||||
exception.blame_file! file
|
||||
raise
|
||||
end
|
||||
|
||||
def require(file, *extras)
|
||||
def require(file, *extras) #:nodoc:
|
||||
Dependencies.new_constants_in(Object) { super(file, *extras) }
|
||||
rescue Exception => exception # errors from required file
|
||||
exception.blame_file! file
|
||||
|
|
|
@ -1,48 +1,31 @@
|
|||
require 'active_support/json/encoders'
|
||||
require 'active_support/json/encoding'
|
||||
require 'active_support/json/decoding'
|
||||
|
||||
module ActiveSupport
|
||||
module JSON #:nodoc:
|
||||
class CircularReferenceError < StandardError #:nodoc:
|
||||
end
|
||||
|
||||
# A string that returns itself as as its JSON-encoded form.
|
||||
class Variable < String #:nodoc:
|
||||
def to_json
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
# When +true+, Hash#to_json will omit quoting string or symbol keys
|
||||
# if the keys are valid JavaScript identifiers. Note that this is
|
||||
# technically improper JSON (all object keys must be quoted), so if
|
||||
# you need strict JSON compliance, set this option to +false+.
|
||||
mattr_accessor :unquote_hash_key_identifiers
|
||||
@@unquote_hash_key_identifiers = true
|
||||
module JSON
|
||||
RESERVED_WORDS = %w(
|
||||
abstract delete goto private transient
|
||||
boolean do if protected try
|
||||
break double implements public typeof
|
||||
byte else import return var
|
||||
case enum in short void
|
||||
catch export instanceof static volatile
|
||||
char extends int super while
|
||||
class final interface switch with
|
||||
const finally long synchronized
|
||||
continue float native this
|
||||
debugger for new throw
|
||||
default function package throws
|
||||
) #:nodoc:
|
||||
|
||||
class << self
|
||||
REFERENCE_STACK_VARIABLE = :json_reference_stack
|
||||
|
||||
def encode(value)
|
||||
raise_on_circular_reference(value) do
|
||||
Encoders[value.class].call(value)
|
||||
end
|
||||
def valid_identifier?(key) #:nodoc:
|
||||
key.to_s =~ /^[[:alpha:]_$][[:alnum:]_$]*$/ && !reserved_word?(key)
|
||||
end
|
||||
|
||||
def can_unquote_identifier?(key)
|
||||
return false unless unquote_hash_key_identifiers
|
||||
key.to_s =~ /^[[:alpha:]_$][[:alnum:]_$]*$/
|
||||
|
||||
def reserved_word?(key) #:nodoc:
|
||||
RESERVED_WORDS.include?(key.to_s)
|
||||
end
|
||||
|
||||
protected
|
||||
def raise_on_circular_reference(value)
|
||||
stack = Thread.current[REFERENCE_STACK_VARIABLE] ||= []
|
||||
raise CircularReferenceError, 'object references itself' if
|
||||
stack.include? value
|
||||
stack << value
|
||||
yield
|
||||
ensure
|
||||
stack.pop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
require 'yaml'
|
||||
require 'strscan'
|
||||
|
||||
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
|
||||
# Ensure that ":" and "," are always followed by a space
|
||||
def convert_json_to_yaml(json) #:nodoc:
|
||||
scanner, quoting, marks = StringScanner.new(json), false, []
|
||||
|
||||
while scanner.scan_until(/(['":,]|\\.)/)
|
||||
case char = scanner[1]
|
||||
when '"', "'"
|
||||
quoting = quoting == char ? false : char
|
||||
when ":", ","
|
||||
marks << scanner.pos - 1 unless quoting
|
||||
end
|
||||
end
|
||||
|
||||
if marks.empty?
|
||||
json
|
||||
else
|
||||
ranges = ([0] + marks.map(&:succ)).zip(marks + [json.length])
|
||||
ranges.map { |(left, right)| json[left..right] }.join(" ")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,25 +0,0 @@
|
|||
module ActiveSupport
|
||||
module JSON #:nodoc:
|
||||
module Encoders
|
||||
mattr_accessor :encoders
|
||||
@@encoders = {}
|
||||
|
||||
class << self
|
||||
def define_encoder(klass, &block)
|
||||
encoders[klass] = block
|
||||
end
|
||||
|
||||
def [](klass)
|
||||
klass.ancestors.each do |k|
|
||||
encoder = encoders[k]
|
||||
return encoder if encoder
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Dir[File.dirname(__FILE__) + '/encoders/*.rb'].each do |file|
|
||||
require file[0..-4]
|
||||
end
|
|
@ -1,68 +0,0 @@
|
|||
module ActiveSupport
|
||||
module JSON #:nodoc:
|
||||
module Encoders #:nodoc:
|
||||
define_encoder Object do |object|
|
||||
object.instance_values.to_json
|
||||
end
|
||||
|
||||
define_encoder TrueClass do
|
||||
'true'
|
||||
end
|
||||
|
||||
define_encoder FalseClass do
|
||||
'false'
|
||||
end
|
||||
|
||||
define_encoder NilClass do
|
||||
'null'
|
||||
end
|
||||
|
||||
ESCAPED_CHARS = {
|
||||
"\010" => '\b',
|
||||
"\f" => '\f',
|
||||
"\n" => '\n',
|
||||
"\r" => '\r',
|
||||
"\t" => '\t',
|
||||
'"' => '\"',
|
||||
'\\' => '\\\\'
|
||||
}
|
||||
|
||||
define_encoder String do |string|
|
||||
'"' + string.gsub(/[\010\f\n\r\t"\\]/) { |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}/, '\\\\u\&')
|
||||
} + '"'
|
||||
end
|
||||
|
||||
define_encoder Numeric do |numeric|
|
||||
numeric.to_s
|
||||
end
|
||||
|
||||
define_encoder Symbol do |symbol|
|
||||
symbol.to_s.to_json
|
||||
end
|
||||
|
||||
define_encoder Enumerable do |enumerable|
|
||||
"[#{enumerable.map { |value| value.to_json } * ', '}]"
|
||||
end
|
||||
|
||||
define_encoder Hash do |hash|
|
||||
returning result = '{' do
|
||||
result << hash.map do |key, value|
|
||||
key = ActiveSupport::JSON::Variable.new(key.to_s) if
|
||||
ActiveSupport::JSON.can_unquote_identifier?(key)
|
||||
"#{key.to_json}: #{value.to_json}"
|
||||
end * ', '
|
||||
result << '}'
|
||||
end
|
||||
end
|
||||
|
||||
define_encoder Regexp do |regexp|
|
||||
regexp.inspect
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
module Enumerable
|
||||
def to_json #:nodoc:
|
||||
"[#{map { |value| ActiveSupport::JSON.encode(value) } * ', '}]"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class FalseClass
|
||||
def to_json #:nodoc:
|
||||
'false'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
class Hash
|
||||
def to_json #:nodoc:
|
||||
returning result = '{' do
|
||||
result << map do |key, value|
|
||||
key = ActiveSupport::JSON::Variable.new(key.to_s) if
|
||||
ActiveSupport::JSON.can_unquote_identifier?(key)
|
||||
"#{ActiveSupport::JSON.encode(key)}: #{ActiveSupport::JSON.encode(value)}"
|
||||
end * ', '
|
||||
result << '}'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class NilClass
|
||||
def to_json #:nodoc:
|
||||
'null'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class Numeric
|
||||
def to_json #:nodoc:
|
||||
to_s
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
class Object
|
||||
# Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
|
||||
#
|
||||
# Account.find(1).to_json
|
||||
# => "{attributes: {username: \"foo\", id: \"1\", password: \"bar\"}}"
|
||||
#
|
||||
def to_json
|
||||
ActiveSupport::JSON.encode(instance_values)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class Regexp
|
||||
def to_json #:nodoc:
|
||||
inspect
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
module ActiveSupport
|
||||
module JSON
|
||||
module Encoding
|
||||
ESCAPED_CHARS = {
|
||||
"\010" => '\b',
|
||||
"\f" => '\f',
|
||||
"\n" => '\n',
|
||||
"\r" => '\r',
|
||||
"\t" => '\t',
|
||||
'"' => '\"',
|
||||
'\\' => '\\\\'
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class String
|
||||
def to_json #:nodoc:
|
||||
'"' + gsub(/[\010\f\n\r\t"\\]/) { |s|
|
||||
ActiveSupport::JSON::Encoding::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}/, '\\\\u\&')
|
||||
} + '"'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class Symbol
|
||||
def to_json #:nodoc:
|
||||
ActiveSupport::JSON.encode(to_s)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class TrueClass
|
||||
def to_json #:nodoc:
|
||||
'true'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,45 @@
|
|||
require 'active_support/json/variable'
|
||||
|
||||
require 'active_support/json/encoders/object' # Require this file explicitly for rdoc
|
||||
Dir[File.dirname(__FILE__) + '/encoders/**/*.rb'].each { |file| require file[0..-4] }
|
||||
|
||||
module ActiveSupport
|
||||
module JSON
|
||||
# When +true+, Hash#to_json will omit quoting string or symbol keys
|
||||
# if the keys are valid JavaScript identifiers. Note that this is
|
||||
# technically improper JSON (all object keys must be quoted), so if
|
||||
# you need strict JSON compliance, set this option to +false+.
|
||||
mattr_accessor :unquote_hash_key_identifiers
|
||||
@@unquote_hash_key_identifiers = true
|
||||
|
||||
class CircularReferenceError < StandardError
|
||||
end
|
||||
|
||||
class << self
|
||||
REFERENCE_STACK_VARIABLE = :json_reference_stack #:nodoc:
|
||||
|
||||
# Converts a Ruby object into a JSON string.
|
||||
def encode(value)
|
||||
raise_on_circular_reference(value) do
|
||||
value.send(:to_json)
|
||||
end
|
||||
end
|
||||
|
||||
def can_unquote_identifier?(key) #:nodoc:
|
||||
unquote_hash_key_identifiers &&
|
||||
ActiveSupport::JSON.valid_identifier?(key)
|
||||
end
|
||||
|
||||
protected
|
||||
def raise_on_circular_reference(value) #:nodoc:
|
||||
stack = Thread.current[REFERENCE_STACK_VARIABLE] ||= []
|
||||
raise CircularReferenceError, 'object references itself' if
|
||||
stack.include? value
|
||||
stack << value
|
||||
yield
|
||||
ensure
|
||||
stack.pop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
module ActiveSupport
|
||||
module JSON
|
||||
# A string that returns itself as as its JSON-encoded form.
|
||||
class Variable < String
|
||||
def to_json
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -48,13 +48,13 @@ module Kernel #:nodoc:
|
|||
end
|
||||
end
|
||||
|
||||
class Object #:nodoc:
|
||||
class Object
|
||||
class << self
|
||||
alias_method :blank_slate_method_added, :method_added
|
||||
|
||||
# Detect method additions to Object and remove them in the
|
||||
# BlankSlate class.
|
||||
def method_added(name)
|
||||
def method_added(name) #:nodoc:
|
||||
blank_slate_method_added(name)
|
||||
return if self != Object
|
||||
Builder::BlankSlate.hide(name)
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
require File.dirname(__FILE__) + '/../abstract_unit'
|
||||
|
||||
class TestJSONDecoding < Test::Unit::TestCase
|
||||
TESTS = {
|
||||
%({"returnTo":{"/categories":"/"}}) => {"returnTo" => {"/categories" => "/"}},
|
||||
%({returnTo:{"/categories":"/"}}) => {"returnTo" => {"/categories" => "/"}},
|
||||
%({"return\\"To\\":":{"/categories":"/"}}) => {"return\"To\":" => {"/categories" => "/"}},
|
||||
%({"returnTo":{"/categories":1}}) => {"returnTo" => {"/categories" => 1}},
|
||||
%({"returnTo":[1,"a"]}) => {"returnTo" => [1, "a"]},
|
||||
%({"returnTo":[1,"\\"a\\",", "b"]}) => {"returnTo" => [1, "\"a\",", "b"]},
|
||||
%([]) => [],
|
||||
%({}) => {},
|
||||
%(1) => 1,
|
||||
%("") => "",
|
||||
%("\\"") => "\"",
|
||||
%(null) => nil,
|
||||
%(true) => true,
|
||||
%(false) => false
|
||||
}
|
||||
|
||||
def test_json_decoding
|
||||
TESTS.each do |json, expected|
|
||||
assert_nothing_raised do
|
||||
assert_equal expected, ActiveSupport::JSON.decode(json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,12 +1,12 @@
|
|||
require File.dirname(__FILE__) + '/abstract_unit'
|
||||
require File.dirname(__FILE__) + '/../abstract_unit'
|
||||
|
||||
class Foo
|
||||
def initialize(a, b)
|
||||
@a, @b = a, b
|
||||
class TestJSONEncoding < Test::Unit::TestCase
|
||||
class Foo
|
||||
def initialize(a, b)
|
||||
@a, @b = a, b
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class TestJSONEmitters < Test::Unit::TestCase
|
||||
TrueTests = [[ true, %(true) ]]
|
||||
FalseTests = [[ false, %(false) ]]
|
||||
NilTests = [[ nil, %(null) ]]
|
||||
|
@ -70,9 +70,14 @@ class TestJSONEmitters < Test::Unit::TestCase
|
|||
end
|
||||
|
||||
def test_unquote_hash_key_identifiers
|
||||
values = {0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B"}
|
||||
assert_equal %({"a": "a", 0: 0, "_": "_", 1: 1, "$": "$", "A": "A", "A0B": "A0B", "A0": "A0"}), values.to_json
|
||||
unquote(true) { assert_equal %({a: "a", 0: 0, _: "_", 1: 1, $: "$", A: "A", A0B: "A0B", A0: "A0"}), values.to_json }
|
||||
values = {0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B"}
|
||||
assert_equal %w( "$" "A" "A0" "A0B" "_" "a" 0 1 ), object_keys(values.to_json)
|
||||
unquote(true) { assert_equal %w( $ 0 1 A A0 A0B _ a ), object_keys(values.to_json) }
|
||||
end
|
||||
|
||||
def test_unquote_hash_key_identifiers_ignores_javascript_reserved_words
|
||||
values = {"hello" => "world", "this" => "that", "with" => "foo"}
|
||||
unquote(true) { assert_equal %w( "this" "with" hello ), object_keys(values.to_json) }
|
||||
end
|
||||
|
||||
protected
|
||||
|
@ -84,4 +89,8 @@ class TestJSONEmitters < Test::Unit::TestCase
|
|||
ActiveSupport::JSON.unquote_hash_key_identifiers = previous_value if block_given?
|
||||
end
|
||||
|
||||
def object_keys(json_object)
|
||||
json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue