提交 3202fbab 编写于 作者: S Sam Stephenson

Refactor ActiveSupport::JSON to be less obtuse. Add support for JSON decoding...

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
上级 3d5c9471
*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]
......
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?)
......
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 @@ def subclasses_of(*superclasses)
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 @@ def instance_values
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 @@ def with_options(options)
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
\ No newline at end of file
......@@ -480,18 +480,18 @@ def const_missing(class_id)
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
......
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
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
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
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
module Enumerable
def to_json #:nodoc:
"[#{map { |value| ActiveSupport::JSON.encode(value) } * ', '}]"
end
end
class FalseClass
def to_json #:nodoc:
'false'
end
end
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
class NilClass
def to_json #:nodoc:
'null'
end
end
class Numeric
def to_json #:nodoc:
to_s
end
end
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
class Regexp
def to_json #:nodoc:
inspect
end
end
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
class Symbol
def to_json #:nodoc:
ActiveSupport::JSON.encode(to_s)
end
end
class TrueClass
def to_json #:nodoc:
'true'
end
end
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
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 @@ def method_added(name)
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)
......
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
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 @@ def test_exception_raised_when_encoding_circular_reference
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 @@ def unquote(value)
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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册