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:
Sam Stephenson 2007-03-18 07:05:58 +00:00
parent 3d5c947155
commit 3202fbabe6
24 changed files with 265 additions and 166 deletions

View File

@ -1,5 +1,7 @@
*SVN* *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] * alias_method_chain preserves the original method's visibility. #7854 [Jonathan Viney]
* Update Dependencies to ignore constants inherited from ancestors. Closes #6951. [Nicholas Seckar] * Update Dependencies to ignore constants inherited from ancestors. Closes #6951. [Nicholas Seckar]

View File

@ -1,6 +1,6 @@
class Object #:nodoc: class Object
# "", " ", nil, [], and {} are blank # "", " ", nil, [], and {} are blank
def blank? def blank? #:nodoc:
if respond_to?(:empty?) && respond_to?(:strip) if respond_to?(:empty?) && respond_to?(:strip)
empty? or strip.empty? empty? or strip.empty?
elsif respond_to?(:empty?) elsif respond_to?(:empty?)

View File

@ -1,9 +1,9 @@
class Object #:nodoc: class Object
def remove_subclasses_of(*superclasses) def remove_subclasses_of(*superclasses) #:nodoc:
Class.remove_class(*subclasses_of(*superclasses)) Class.remove_class(*subclasses_of(*superclasses))
end end
def subclasses_of(*superclasses) def subclasses_of(*superclasses) #:nodoc:
subclasses = [] subclasses = []
ObjectSpace.each_object(Class) do |k| ObjectSpace.each_object(Class) do |k|
next unless # Exclude this class unless next unless # Exclude this class unless
@ -16,23 +16,23 @@ class Object #:nodoc:
subclasses subclasses
end end
def extended_by def extended_by #:nodoc:
ancestors = class << self; ancestors end ancestors = class << self; ancestors end
ancestors.select { |mod| mod.class == Module } - [ Object, Kernel ] ancestors.select { |mod| mod.class == Module } - [ Object, Kernel ]
end 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 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 = object.instance_variables - exclude.map { |name| name.to_s }
instance_variables.each { |name| instance_variable_set(name, object.instance_variable_get(name)) } instance_variables.each { |name| instance_variable_set(name, object.instance_variable_get(name)) }
end end
def extend_with_included_modules_from(object) def extend_with_included_modules_from(object) #:nodoc:
object.extended_by.each { |mod| extend mod } object.extended_by.each { |mod| extend mod }
end end
def instance_values def instance_values #:nodoc:
instance_variables.inject({}) do |values, name| instance_variables.inject({}) do |values, name|
values[name[1..-1]] = instance_variable_get(name) values[name[1..-1]] = instance_variable_get(name)
values values
@ -40,7 +40,7 @@ class Object #:nodoc:
end end
unless defined? instance_exec # 1.9 unless defined? instance_exec # 1.9
def instance_exec(*arguments, &block) def instance_exec(*arguments, &block) #:nodoc:
block.bind(self)[*arguments] block.bind(self)[*arguments]
end end
end end

View File

@ -43,21 +43,12 @@ class Object
yield ActiveSupport::OptionMerger.new(self, options) yield ActiveSupport::OptionMerger.new(self, options)
end 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 # A duck-type assistant method. For example, ActiveSupport extends Date
# to define an acts_like_date? method, and extends Time to define # 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 # 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 # "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. # we want to act like Time simply need to define an acts_like_time? method.
def acts_like?(duck) def acts_like?(duck)
respond_to? :"acts_like_#{duck}?" respond_to? "acts_like_#{duck}?"
end end
end end

View File

@ -480,18 +480,18 @@ class Class
end end
end end
class Object #:nodoc: class Object
alias_method :load_without_new_constant_marking, :load 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) } Dependencies.new_constants_in(Object) { super(file, *extras) }
rescue Exception => exception # errors from loading file rescue Exception => exception # errors from loading file
exception.blame_file! file exception.blame_file! file
raise raise
end end
def require(file, *extras) def require(file, *extras) #:nodoc:
Dependencies.new_constants_in(Object) { super(file, *extras) } Dependencies.new_constants_in(Object) { super(file, *extras) }
rescue Exception => exception # errors from required file rescue Exception => exception # errors from required file
exception.blame_file! file exception.blame_file! file

View File

@ -1,48 +1,31 @@
require 'active_support/json/encoders' require 'active_support/json/encoding'
require 'active_support/json/decoding'
module ActiveSupport module ActiveSupport
module JSON #:nodoc: module JSON
class CircularReferenceError < StandardError #:nodoc: RESERVED_WORDS = %w(
end abstract delete goto private transient
boolean do if protected try
# A string that returns itself as as its JSON-encoded form. break double implements public typeof
class Variable < String #:nodoc: byte else import return var
def to_json case enum in short void
self catch export instanceof static volatile
end char extends int super while
end class final interface switch with
const finally long synchronized
# When +true+, Hash#to_json will omit quoting string or symbol keys continue float native this
# if the keys are valid JavaScript identifiers. Note that this is debugger for new throw
# technically improper JSON (all object keys must be quoted), so if default function package throws
# you need strict JSON compliance, set this option to +false+. ) #:nodoc:
mattr_accessor :unquote_hash_key_identifiers
@@unquote_hash_key_identifiers = true
class << self class << self
REFERENCE_STACK_VARIABLE = :json_reference_stack def valid_identifier?(key) #:nodoc:
key.to_s =~ /^[[:alpha:]_$][[:alnum:]_$]*$/ && !reserved_word?(key)
def encode(value)
raise_on_circular_reference(value) do
Encoders[value.class].call(value)
end
end end
def can_unquote_identifier?(key) def reserved_word?(key) #:nodoc:
return false unless unquote_hash_key_identifiers RESERVED_WORDS.include?(key.to_s)
key.to_s =~ /^[[:alpha:]_$][[:alnum:]_$]*$/
end 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 end
end end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
module Enumerable
def to_json #:nodoc:
"[#{map { |value| ActiveSupport::JSON.encode(value) } * ', '}]"
end
end

View File

@ -0,0 +1,5 @@
class FalseClass
def to_json #:nodoc:
'false'
end
end

View File

@ -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

View File

@ -0,0 +1,5 @@
class NilClass
def to_json #:nodoc:
'null'
end
end

View File

@ -0,0 +1,5 @@
class Numeric
def to_json #:nodoc:
to_s
end
end

View File

@ -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

View File

@ -0,0 +1,5 @@
class Regexp
def to_json #:nodoc:
inspect
end
end

View File

@ -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

View File

@ -0,0 +1,5 @@
class Symbol
def to_json #:nodoc:
ActiveSupport::JSON.encode(to_s)
end
end

View File

@ -0,0 +1,5 @@
class TrueClass
def to_json #:nodoc:
'true'
end
end

View File

@ -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

View File

@ -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

View File

@ -48,13 +48,13 @@ module Kernel #:nodoc:
end end
end end
class Object #:nodoc: class Object
class << self class << self
alias_method :blank_slate_method_added, :method_added alias_method :blank_slate_method_added, :method_added
# Detect method additions to Object and remove them in the # Detect method additions to Object and remove them in the
# BlankSlate class. # BlankSlate class.
def method_added(name) def method_added(name) #:nodoc:
blank_slate_method_added(name) blank_slate_method_added(name)
return if self != Object return if self != Object
Builder::BlankSlate.hide(name) Builder::BlankSlate.hide(name)

View File

@ -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

View File

@ -1,12 +1,12 @@
require File.dirname(__FILE__) + '/abstract_unit' require File.dirname(__FILE__) + '/../abstract_unit'
class Foo class TestJSONEncoding < Test::Unit::TestCase
def initialize(a, b) class Foo
@a, @b = a, b def initialize(a, b)
@a, @b = a, b
end
end end
end
class TestJSONEmitters < Test::Unit::TestCase
TrueTests = [[ true, %(true) ]] TrueTests = [[ true, %(true) ]]
FalseTests = [[ false, %(false) ]] FalseTests = [[ false, %(false) ]]
NilTests = [[ nil, %(null) ]] NilTests = [[ nil, %(null) ]]
@ -71,8 +71,13 @@ class TestJSONEmitters < Test::Unit::TestCase
def test_unquote_hash_key_identifiers def test_unquote_hash_key_identifiers
values = {0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B"} 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 assert_equal %w( "$" "A" "A0" "A0B" "_" "a" 0 1 ), object_keys(values.to_json)
unquote(true) { assert_equal %({a: "a", 0: 0, _: "_", 1: 1, $: "$", A: "A", A0B: "A0B", A0: "A0"}), 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 end
protected protected
@ -84,4 +89,8 @@ class TestJSONEmitters < Test::Unit::TestCase
ActiveSupport::JSON.unquote_hash_key_identifiers = previous_value if block_given? ActiveSupport::JSON.unquote_hash_key_identifiers = previous_value if block_given?
end end
def object_keys(json_object)
json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort
end
end end