diff options
Diffstat (limited to 'activesupport/lib')
48 files changed, 786 insertions, 408 deletions
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index d83e259a2a..84d9a0e6d8 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -27,6 +27,11 @@ module ActiveSupport Thread.current[:#{thread_local_key}] = nil end EOS + + def klass.to_s + "ActiveSupport::Cache::Strategy::LocalCache" + end + klass end end diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb index cc0a1ebc12..82c6b1243a 100644 --- a/activesupport/lib/active_support/core_ext/array.rb +++ b/activesupport/lib/active_support/core_ext/array.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/grouping' require 'active_support/core_ext/array/random_access' +require 'active_support/core_ext/array/wrapper' class Array #:nodoc: include ActiveSupport::CoreExtensions::Array::Access @@ -10,4 +11,5 @@ class Array #:nodoc: include ActiveSupport::CoreExtensions::Array::ExtractOptions include ActiveSupport::CoreExtensions::Array::Grouping include ActiveSupport::CoreExtensions::Array::RandomAccess + extend ActiveSupport::CoreExtensions::Array::Wrapper end diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 69d35dafd3..ba8e022fb2 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -6,14 +6,27 @@ module ActiveSupport #:nodoc: # * <tt>:words_connector</tt> - The sign or word used to join the elements in arrays with two or more elements (default: ", ") # * <tt>:two_words_connector</tt> - The sign or word used to join the elements in arrays with two elements (default: " and ") # * <tt>:last_word_connector</tt> - The sign or word used to join the last element in arrays with three or more elements (default: ", and ") - def to_sentence(options = {}) - options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale) - - default_words_connector = I18n.translate(:'support.array.words_connector', :locale => options[:locale]) + def to_sentence(options = {}) + default_words_connector = I18n.translate(:'support.array.words_connector', :locale => options[:locale]) default_two_words_connector = I18n.translate(:'support.array.two_words_connector', :locale => options[:locale]) default_last_word_connector = I18n.translate(:'support.array.last_word_connector', :locale => options[:locale]) - options.reverse_merge! :words_connector => default_words_connector, :two_words_connector => default_two_words_connector, :last_word_connector => default_last_word_connector + # Try to emulate to_senteces previous to 2.3 + if options.has_key?(:connector) || options.has_key?(:skip_last_comma) + ::ActiveSupport::Deprecation.warn(":connector has been deprecated. Use :words_connector instead", caller) if options.has_key? :connector + ::ActiveSupport::Deprecation.warn(":skip_last_comma has been deprecated. Use :last_word_connector instead", caller) if options.has_key? :skip_last_comma + + skip_last_comma = options.delete :skip_last_comma + if connector = options.delete(:connector) + options[:last_word_connector] ||= skip_last_comma ? connector : ", #{connector}" + else + options[:last_word_connector] ||= skip_last_comma ? default_two_words_connector : default_last_word_connector + end + end + + options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale) + options.reverse_merge! :words_connector => default_words_connector, :two_words_connector => default_two_words_connector, :last_word_connector => default_last_word_connector + case length when 0 "" diff --git a/activesupport/lib/active_support/core_ext/array/wrapper.rb b/activesupport/lib/active_support/core_ext/array/wrapper.rb new file mode 100644 index 0000000000..80b8f05531 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/array/wrapper.rb @@ -0,0 +1,24 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Array #:nodoc: + module Wrapper + # Wraps the object in an Array unless it's an Array. Converts the + # object to an Array using #to_ary if it implements that. + def wrap(object) + case object + when nil + [] + when self + object + else + if object.respond_to?(:to_ary) + object.to_ary + else + [object] + end + end + end + end + end + end +end diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index 43d70c7013..7f94da015b 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -1,7 +1,7 @@ module ActiveSupport #:nodoc: module CoreExtensions #:nodoc: module Date #:nodoc: - # Enables the use of time calculations within Time itself + # Enables the use of time calculations within Date itself module Calculations def self.included(base) #:nodoc: base.extend ClassMethods diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index 976d462e8e..8cc5654a4b 100644 --- a/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -27,7 +27,7 @@ module ActiveSupport #:nodoc: old_stat = stat(file_name) rescue Errno::ENOENT # No old permissions, write a temp file to determine the defaults - check_name = ".permissions_check.#{Thread.current.object_id}.#{Process.pid}.#{rand(1000000)}" + check_name = join(dirname(file_name), ".permissions_check.#{Thread.current.object_id}.#{Process.pid}.#{rand(1000000)}") open(check_name, "w") { } old_stat = stat(check_name) unlink(check_name) diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 991a5a6a89..f8a5e70eea 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -24,11 +24,11 @@ module ActiveSupport #:nodoc: "Bignum" => "integer", "BigDecimal" => "decimal", "Float" => "float", + "TrueClass" => "boolean", + "FalseClass" => "boolean", "Date" => "date", "DateTime" => "datetime", - "Time" => "datetime", - "TrueClass" => "boolean", - "FalseClass" => "boolean" + "Time" => "datetime" } unless defined?(XML_TYPE_NAMES) XML_FORMATTING = { diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb index c96c5160b3..34ba8a005d 100644 --- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb +++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb @@ -91,6 +91,12 @@ class HashWithIndifferentAccess < Hash self.dup.update(hash) end + # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second. + # This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess. + def reverse_merge(other_hash) + super other_hash.with_indifferent_access + end + # Removes a specified key from the hash. def delete(key) super(convert_key(key)) diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index de99fe5791..48e812aaf8 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -1,4 +1,4 @@ -require 'active_support/inflector' +require 'active_support/inflector' unless defined?(ActiveSupport::Inflector) module ActiveSupport #:nodoc: module CoreExtensions #:nodoc: @@ -102,8 +102,8 @@ module ActiveSupport #:nodoc: # # <%= link_to(@person.name, person_path %> # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a> - def parameterize - Inflector.parameterize(self) + def parameterize(sep = '-') + Inflector.parameterize(self, sep) end # Creates the name of a table like Rails does for models to table names. This method diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 5ed750afcc..d13d0e01a7 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -116,22 +116,14 @@ module ActiveSupport #:nodoc: seconds_to_advance == 0 ? time_advanced_by_date : time_advanced_by_date.since(seconds_to_advance) end - # Returns a new Time representing the time a number of seconds ago, this is basically a wrapper around the Numeric extension + # Returns a new Time representing the time a number of seconds ago def ago(seconds) self.since(-seconds) end - # Returns a new Time representing the time a number of seconds since the instance time, this is basically a wrapper around - # the Numeric extension. + # Returns a new Time representing the time a number of seconds since the instance time def since(seconds) - f = seconds.since(self) - if ActiveSupport::Duration === seconds - f - else - initial_dst = self.dst? ? 1 : 0 - final_dst = f.dst? ? 1 : 0 - (seconds.abs >= 86400 && initial_dst != final_dst) ? f + (initial_dst - final_dst).hours : f - end + self + seconds rescue self.to_datetime.since(seconds) end diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index f42be46770..e6f9134661 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -10,7 +10,7 @@ module ActiveSupport #:nodoc: :short => "%d %b %H:%M", :long => "%B %d, %Y %H:%M", :long_ordinal => lambda { |time| time.strftime("%B #{time.day.ordinalize}, %Y %H:%M") }, - :rfc822 => "%a, %d %b %Y %H:%M:%S %z" + :rfc822 => lambda { |time| time.strftime("%a, %d %b %Y %H:%M:%S #{time.formatted_offset(false)}") } } def self.included(base) #:nodoc: diff --git a/activesupport/lib/active_support/core_ext/try.rb b/activesupport/lib/active_support/core_ext/try.rb index 0dccd40c55..3de198d198 100644 --- a/activesupport/lib/active_support/core_ext/try.rb +++ b/activesupport/lib/active_support/core_ext/try.rb @@ -1,6 +1,9 @@ class Object - # Tries to send the method only if object responds to it. Return +nil+ otherwise. - # It will also forward any arguments and/or block like Object#send does. + # Invokes the method identified by the symbol +method+, passing it any arguments + # and/or the block specified, just like the regular Ruby <tt>Object#send</tt> does. + # + # *Unlike* that method however, a +NoMethodError+ exception will *not* be raised + # and +nil+ will be returned instead, if the receiving object is a +nil+ object or NilClass. # # ==== Examples # @@ -12,14 +15,17 @@ class Object # With try # @person.try(:name) # - # Try also accepts arguments/blocks for the method it is trying + # +try+ also accepts arguments and/or a block, for the method it is trying # Person.try(:find, 1) # @people.try(:collect) {|p| p.name} #-- - # This method def is for rdoc only. The alias_method below overrides it as an optimization. + # This method definition below is for rdoc purposes only. The alias_method call + # below overrides it as an optimization since +try+ behaves like +Object#send+, + # unless called on +NilClass+. def try(method, *args, &block) send(method, *args, &block) end + remove_method :try alias_method :try, :__send__ end diff --git a/activesupport/lib/active_support/core_ext/uri.rb b/activesupport/lib/active_support/core_ext/uri.rb new file mode 100644 index 0000000000..9a1c61d99b --- /dev/null +++ b/activesupport/lib/active_support/core_ext/uri.rb @@ -0,0 +1,16 @@ +if RUBY_VERSION >= '1.9' + require 'uri' + + str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. + str.force_encoding(Encoding::UTF_8) if str.respond_to?(:force_encoding) + + unless str == URI.unescape(URI.escape(str)) + URI::Parser.class_eval do + remove_method :unescape + def unescape(str, escaped = @regexp[:ESCAPED]) + enc = (str.encoding == Encoding::US_ASCII) ? Encoding::UTF_8 : str.encoding + str.gsub(escaped) { [$&[1, 2].hex].pack('C') }.force_encoding(enc) + end + end + end +end diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index c41e86dfd1..f64661c5b1 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -70,7 +70,7 @@ module ActiveSupport [:years, :months, :days, :minutes, :seconds].map do |length| n = consolidated[length] "#{n} #{n == 1 ? length.to_s.singularize : length.to_s}" if n.nonzero? - end.compact.to_sentence + end.compact.to_sentence(:locale => :en) end protected diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb index b6d276953a..8fb3fa9aa2 100644 --- a/activesupport/lib/active_support/inflections.rb +++ b/activesupport/lib/active_support/inflections.rb @@ -42,6 +42,7 @@ module ActiveSupport inflect.singular(/(vert|ind)ices$/i, '\1ex') inflect.singular(/(matr)ices$/i, '\1ix') inflect.singular(/(quiz)zes$/i, '\1') + inflect.singular(/(database)s$/i, '\1') inflect.irregular('person', 'people') inflect.irregular('man', 'men') diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb index 4921b99677..3ed30bdf56 100644 --- a/activesupport/lib/active_support/inflector.rb +++ b/activesupport/lib/active_support/inflector.rb @@ -257,15 +257,17 @@ module ActiveSupport # <%= link_to(@person.name, person_path(@person)) %> # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a> def parameterize(string, sep = '-') - re_sep = Regexp.escape(sep) # replace accented chars with ther ascii equivalents parameterized_string = transliterate(string) # Turn unwanted chars into the seperator parameterized_string.gsub!(/[^a-z0-9\-_\+]+/i, sep) - # No more than one of the separator in a row. - parameterized_string.squeeze!(sep) - # Remove leading/trailing separator. - parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '') + unless sep.blank? + re_sep = Regexp.escape(sep) + # No more than one of the separator in a row. + parameterized_string.gsub!(/#{re_sep}{2,}/, sep) + # Remove leading/trailing separator. + parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '') + end parameterized_string.downcase end @@ -284,7 +286,7 @@ module ActiveSupport # The iconv transliteration code doesn't function correctly # on some platforms, but it's very fast where it does function. - elsif "foo" != Inflector.transliterate("föö") + elsif "foo" != (Inflector.transliterate("föö") rescue nil) undef_method :transliterate def transliterate(string) string.mb_chars.normalize(:kd). # Decompose accented characters diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index 9da4048272..0e079341ff 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -43,13 +43,32 @@ module ActiveSupport end if marks.empty? - json.gsub(/\\\//, '/') + json.gsub(/\\([\\\/]|u[[:xdigit:]]{4})/) do + ustr = $1 + if ustr.starts_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 << json.length + right_pos = marks << scanner.pos + scanner.rest_size output = [] left_pos.each_with_index do |left, i| - output << json[left.succ..right_pos[i]] + scanner.pos = left.succ + output << scanner.peek(right_pos[i] - scanner.pos + 1).gsub(/\\([\\\/]|u[[:xdigit:]]{4})/) do + ustr = $1 + if ustr.starts_with?('u') + [ustr[1..-1].to_i(16)].pack("U") + elsif ustr == '\\' + '\\\\' + else + ustr + end + end end output = output * " " diff --git a/activesupport/lib/active_support/json/encoders/hash.rb b/activesupport/lib/active_support/json/encoders/hash.rb index 16dc8337f5..e38b4f3e16 100644 --- a/activesupport/lib/active_support/json/encoders/hash.rb +++ b/activesupport/lib/active_support/json/encoders/hash.rb @@ -31,17 +31,16 @@ class Hash def to_json(options = {}) #:nodoc: hash_keys = self.keys - if options[:except] - hash_keys = hash_keys - Array(options[:except]) - elsif options[:only] - hash_keys = hash_keys & Array(options[:only]) + if except = options[:except] + hash_keys = hash_keys - Array.wrap(except) + elsif only = options[:only] + hash_keys = hash_keys & Array.wrap(only) end - returning result = '{' do - result << hash_keys.map do |key| - "#{ActiveSupport::JSON.encode(key.to_s)}: #{ActiveSupport::JSON.encode(self[key], options)}" - end * ', ' - result << '}' - end + result = '{' + result << hash_keys.map do |key| + "#{ActiveSupport::JSON.encode(key.to_s)}: #{ActiveSupport::JSON.encode(self[key], options)}" + end * ', ' + result << '}' end end diff --git a/activesupport/lib/active_support/memoizable.rb b/activesupport/lib/active_support/memoizable.rb index 945c1a68bb..2b85fd7be4 100644 --- a/activesupport/lib/active_support/memoizable.rb +++ b/activesupport/lib/active_support/memoizable.rb @@ -102,6 +102,10 @@ module ActiveSupport end # end end # end end # end + # + if private_method_defined?(#{original_method.inspect}) # if private_method_defined?(:_unmemoized_mime_type) + private #{symbol.inspect} # private :mime_type + end # end EOS end end diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index a00b165222..60f082bcc1 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -344,7 +344,19 @@ module ActiveSupport #:nodoc: end alias_method :[], :slice - # Converts first character in the string to Unicode value + # Like <tt>String#slice!</tt>, except instead of byte offsets you specify character offsets. + # + # Example: + # s = 'こんにちは' + # s.mb_chars.slice!(2..3).to_s #=> "にち" + # s #=> "こんは" + def slice!(*args) + slice = self[*args] + self[*args] = '' + slice + end + + # Returns the codepoint of the first character in the string. # # Example: # 'こんにちは'.mb_chars.ord #=> 12371 @@ -432,7 +444,7 @@ module ActiveSupport #:nodoc: chars(self.class.tidy_bytes(@wrapped_string)) end - %w(lstrip rstrip strip reverse upcase downcase slice tidy_bytes capitalize).each do |method| + %w(lstrip rstrip strip reverse upcase downcase tidy_bytes capitalize).each do |method| define_method("#{method}!") do |*args| unless args.nil? @wrapped_string = send(method, *args).to_s @@ -617,6 +629,8 @@ module ActiveSupport #:nodoc: # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent resulting in a valid UTF-8 string. def tidy_bytes(string) string.split(//u).map do |c| + c.force_encoding(Encoding::ASCII) if c.respond_to?(:force_encoding) + if !UTF8_PAT.match(c) n = c.unpack('C')[0] n < 128 ? n.chr : diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 66aab9e562..fed8094a24 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -54,7 +54,7 @@ module ActiveSupport end def to_hash - Hash.new(self) + self end def each_key @@ -93,7 +93,7 @@ module ActiveSupport end def inspect - "#<OrderedHash #{self.to_hash.inspect}>" + "#<OrderedHash #{super}>" end private diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 3d4924f4eb..50e25ef740 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -21,7 +21,7 @@ module ActiveSupport alias_method :method_name, :name else # TODO: Figure out how to get the Rails::BacktraceFilter into minitest/unit - if defined?(Rails) + if defined?(Rails) && ENV['BACKTRACE'].nil? require 'rails/backtrace_cleaner' Test::Unit::Util::BacktraceFilter.module_eval { include Rails::BacktraceFilterForTestUnit } end diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index ce2f44efd6..ca51adba1e 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -31,13 +31,17 @@ module ActiveSupport # assert_difference 'Article.count', -1, "An Article should be destroyed" do # post :delete, :id => ... # end - def assert_difference(expressions, difference = 1, message = nil, &block) - expression_evaluations = Array(expressions).collect{ |expression| lambda { eval(expression, block.send(:binding)) } } + def assert_difference(expression, difference = 1, message = nil, &block) + b = block.send(:binding) + exps = Array.wrap(expression) + before = exps.map { |e| eval(e, b) } - original_values = expression_evaluations.inject([]) { |memo, expression| memo << expression.call } yield - expression_evaluations.each_with_index do |expression, i| - assert_equal original_values[i] + difference, expression.call, message + + exps.each_with_index do |e, i| + error = "#{e.inspect} didn't change by #{difference}" + error = "#{message}.\n#{error}" if message + assert_equal(before[i] + difference, eval(e, b), error) end end @@ -53,8 +57,8 @@ module ActiveSupport # assert_no_difference 'Article.count', "An Article should not be destroyed" do # post :create, :article => invalid_attributes # end - def assert_no_difference(expressions, message = nil, &block) - assert_difference expressions, 0, message, &block + def assert_no_difference(expression, message = nil, &block) + assert_difference expression, 0, message, &block end end end diff --git a/activesupport/lib/active_support/testing/core_ext/test.rb b/activesupport/lib/active_support/testing/core_ext/test.rb deleted file mode 100644 index d3f38f0bc7..0000000000 --- a/activesupport/lib/active_support/testing/core_ext/test.rb +++ /dev/null @@ -1,6 +0,0 @@ -require 'active_support/testing/core_ext/test/unit/assertions' -require 'active_support/testing/setup_and_teardown' - -class Test::Unit::TestCase #:nodoc: - include ActiveSupport::Testing::SetupAndTeardown -end
\ No newline at end of file diff --git a/activesupport/lib/active_support/testing/core_ext/test/unit/assertions.rb b/activesupport/lib/active_support/testing/core_ext/test/unit/assertions.rb deleted file mode 100644 index e5853bf828..0000000000 --- a/activesupport/lib/active_support/testing/core_ext/test/unit/assertions.rb +++ /dev/null @@ -1,72 +0,0 @@ -require 'test/unit/assertions' -module Test - module Unit - #-- - # FIXME: no Proc#binding in Ruby 2, must change this API - #++ - module Assertions - # Test numeric difference between the return value of an expression as a result of what is evaluated - # in the yielded block. - # - # assert_difference 'Article.count' do - # post :create, :article => {...} - # end - # - # An arbitrary expression is passed in and evaluated. - # - # assert_difference 'assigns(:article).comments(:reload).size' do - # post :create, :comment => {...} - # end - # - # An arbitrary positive or negative difference can be specified. The default is +1. - # - # assert_difference 'Article.count', -1 do - # post :delete, :id => ... - # end - # - # An array of expressions can also be passed in and evaluated. - # - # assert_difference [ 'Article.count', 'Post.count' ], +2 do - # post :create, :article => {...} - # end - # - # A error message can be specified. - # - # assert_difference 'Article.count', -1, "An Article should be destroyed" do - # post :delete, :id => ... - # end - def assert_difference(expressions, difference = 1, message = nil, &block) - expression_evaluations = Array(expressions).map do |expression| - [expression, lambda do - eval(expression, block.__send__(:binding)) - end] - end - - original_values = expression_evaluations.inject([]) { |memo, expression| memo << expression[1].call } - yield - expression_evaluations.each_with_index do |expression, i| - full_message = "" - full_message << "#{message}.\n" if message - full_message << "<#{expression[0]}> was the expression that failed" - assert_equal original_values[i] + difference, expression[1].call, full_message - end - end - - # Assertion that the numeric result of evaluating an expression is not changed before and after - # invoking the passed in block. - # - # assert_no_difference 'Article.count' do - # post :create, :article => invalid_attributes - # end - # - # A error message can be specified. - # - # assert_no_difference 'Article.count', "An Article should not be destroyed" do - # post :create, :article => invalid_attributes - # end - def assert_no_difference(expressions, message = nil, &block) - assert_difference expressions, 0, message, &block - end - end - end -end diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb index 6248bf1921..4537c30e9c 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -8,7 +8,7 @@ module ActiveSupport include ActiveSupport::Callbacks define_callbacks :setup, :teardown - if defined? MiniTest + if defined?(MiniTest::Assertions) && TestCase < MiniTest::Assertions include ForMiniTest elsif defined? Spec include ForRspec @@ -47,7 +47,12 @@ module ActiveSupport return if @method_name.to_s == "default_test" if using_mocha = respond_to?(:mocha_verify) - assertion_counter = Mocha::TestCaseAdapter::AssertionCounter.new(result) + assertion_counter_klass = if defined?(Mocha::TestCaseAdapter::AssertionCounter) + Mocha::TestCaseAdapter::AssertionCounter + else + Mocha::Integration::TestUnit::AssertionCounter + end + assertion_counter = assertion_counter_klass.new(result) end yield(Test::Unit::TestCase::STARTED, name) diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 3a5a083629..9068afef2e 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -31,6 +31,11 @@ module ActiveSupport # t.is_a?(Time) # => true # t.is_a?(ActiveSupport::TimeWithZone) # => true class TimeWithZone + + def self.name + 'Time' # Report class name as 'Time' to thwart type checking + end + include Comparable attr_reader :time_zone @@ -155,6 +160,7 @@ module ActiveSupport "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby 1.9 Time#to_s format end end + alias_method :to_formatted_s, :to_s # Replaces <tt>%Z</tt> and <tt>%z</tt> directives with +zone+ and +formatted_offset+, respectively, before passing to # Time#strftime, so that zone information is correct @@ -229,7 +235,7 @@ module ActiveSupport def advance(options) # If we're advancing a value of variable length (i.e., years, weeks, months, days), advance from #time, # otherwise advance from #utc, for accuracy when moving across DST boundaries - if options.detect {|k,v| [:years, :weeks, :months, :days].include? k} + if options.values_at(:years, :weeks, :months, :days).any? method_missing(:advance, options) else utc.advance(options).in_time_zone(time_zone) @@ -327,7 +333,7 @@ module ActiveSupport end def duration_of_variable_length?(obj) - ActiveSupport::Duration === obj && obj.parts.flatten.detect {|p| [:years, :months, :days].include? p } + ActiveSupport::Duration === obj && obj.parts.any? {|p| [:years, :months, :days].include? p[0] } end end end diff --git a/activesupport/lib/active_support/vendor.rb b/activesupport/lib/active_support/vendor.rb index 3d7d52ca71..28852e65c8 100644 --- a/activesupport/lib/active_support/vendor.rb +++ b/activesupport/lib/active_support/vendor.rb @@ -9,9 +9,9 @@ end require 'builder' begin - gem 'memcache-client', '~> 1.5.0.5' + gem 'memcache-client', '>= 1.6.5' rescue Gem::LoadError - $:.unshift "#{File.dirname(__FILE__)}/vendor/memcache-client-1.5.0.5" + $:.unshift "#{File.dirname(__FILE__)}/vendor/memcache-client-1.6.5" end begin @@ -22,8 +22,8 @@ end # TODO I18n gem has not been released yet # begin -# gem 'i18n', '~> 0.1.1' +# gem 'i18n', '~> 0.1.3' # rescue Gem::LoadError - $:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.1.1/lib" + $:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.1.3/lib" require 'i18n' # end diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore b/activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore deleted file mode 100644 index 0f41a39f89..0000000000 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.DS_Store -test/rails/fixtures -doc diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/MIT-LICENSE b/activesupport/lib/active_support/vendor/i18n-0.1.3/MIT-LICENSE index ed8e9ee66d..ed8e9ee66d 100755 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/MIT-LICENSE +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/MIT-LICENSE diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/README.textile b/activesupport/lib/active_support/vendor/i18n-0.1.3/README.textile index a07fc8426d..a07fc8426d 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/README.textile +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/README.textile diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/Rakefile b/activesupport/lib/active_support/vendor/i18n-0.1.3/Rakefile index 2164e13e69..2164e13e69 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/Rakefile +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/Rakefile diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/i18n.gemspec b/activesupport/lib/active_support/vendor/i18n-0.1.3/i18n.gemspec index 14294606bd..f102689a6f 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/i18n.gemspec +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/i18n.gemspec @@ -1,7 +1,7 @@ Gem::Specification.new do |s| s.name = "i18n" - s.version = "0.1.1" - s.date = "2008-10-26" + s.version = "0.1.3" + s.date = "2009-01-09" s.summary = "Internationalization support for Ruby" s.email = "rails-i18n@googlegroups.com" s.homepage = "http://rails-i18n.org" diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n.rb index 76361bed90..76361bed90 100755 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n.rb diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/backend/simple.rb index b54164d496..c09acd7d2d 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/backend/simple.rb @@ -151,12 +151,7 @@ module I18n def interpolate(locale, string, values = {}) return string unless string.is_a?(String) - if string.respond_to?(:force_encoding) - original_encoding = string.encoding - string.force_encoding(Encoding::BINARY) - end - - result = string.gsub(MATCH) do + string.gsub(MATCH) do escaped, pattern, key = $1, $2, $2.to_sym if escaped @@ -169,9 +164,6 @@ module I18n values[key].to_s end end - - result.force_encoding(original_encoding) if original_encoding - result end # Loads a single translations file by delegating to #load_rb or diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/exceptions.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/exceptions.rb index b5cea7acb4..b5cea7acb4 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/exceptions.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/exceptions.rb diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/all.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/all.rb index 353712da49..353712da49 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/all.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/all.rb diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_exceptions_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_exceptions_test.rb index dfcba6901f..dfcba6901f 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_exceptions_test.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_exceptions_test.rb diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_test.rb index bbb35ec809..50d6832c9e 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_test.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_test.rb @@ -116,10 +116,10 @@ class I18nTest < Test::Unit::TestCase end def test_localize_nil_raises_argument_error - assert_raises(I18n::ArgumentError) { I18n.l nil } + assert_raise(I18n::ArgumentError) { I18n.l nil } end def test_localize_object_raises_argument_error - assert_raises(I18n::ArgumentError) { I18n.l Object.new } + assert_raise(I18n::ArgumentError) { I18n.l Object.new } end end diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.rb index 6044ce10d9..6044ce10d9 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.rb diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.yml b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.yml index 0b298c9c0e..0b298c9c0e 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.yml +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.yml diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/simple_backend_test.rb index 8ba7036abf..65f3ac11a6 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/simple_backend_test.rb @@ -155,7 +155,7 @@ class I18nSimpleBackendTranslateTest < Test::Unit::TestCase end def test_translate_given_an_array_of_inexistent_keys_it_raises_missing_translation_data - assert_raises I18n::MissingTranslationData do + assert_raise I18n::MissingTranslationData do @backend.translate('en', :does_not_exist, :scope => [:foo], :default => [:does_not_exist_2, :does_not_exist_3]) end end @@ -180,11 +180,11 @@ class I18nSimpleBackendTranslateTest < Test::Unit::TestCase end def test_translate_given_nil_as_a_locale_raises_an_argument_error - assert_raises(I18n::InvalidLocale){ @backend.translate nil, :bar } + assert_raise(I18n::InvalidLocale){ @backend.translate nil, :bar } end def test_translate_with_a_bogus_key_and_no_default_raises_missing_translation_data - assert_raises(I18n::MissingTranslationData){ @backend.translate 'de', :bogus } + assert_raise(I18n::MissingTranslationData){ @backend.translate 'de', :bogus } end end @@ -230,15 +230,15 @@ class I18nSimpleBackendPluralizeTest < Test::Unit::TestCase end def test_interpolate_given_incomplete_pluralization_data_raises_invalid_pluralization_data - assert_raises(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, {:one => 'bar'}, 2) } + assert_raise(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, {:one => 'bar'}, 2) } end # def test_interpolate_given_a_string_raises_invalid_pluralization_data - # assert_raises(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, 'bar', 2) } + # assert_raise(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, 'bar', 2) } # end # # def test_interpolate_given_an_array_raises_invalid_pluralization_data - # assert_raises(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, ['bar'], 2) } + # assert_raise(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, ['bar'], 2) } # end end @@ -253,6 +253,32 @@ class I18nSimpleBackendInterpolateTest < Test::Unit::TestCase assert_equal 'Häi David!', @backend.send(:interpolate, nil, 'Häi {{name}}!', :name => 'David') end + def test_interpolate_given_an_unicode_value_hash_interpolates_to_the_string + assert_equal 'Hi ゆきひろ!', @backend.send(:interpolate, nil, 'Hi {{name}}!', :name => 'ゆきひろ') + end + + def test_interpolate_given_an_unicode_value_hash_interpolates_into_unicode_string + assert_equal 'こんにちは、ゆきひろさん!', @backend.send(:interpolate, nil, 'こんにちは、{{name}}さん!', :name => 'ゆきひろ') + end + + if Kernel.const_defined?(:Encoding) + def test_interpolate_given_a_non_unicode_multibyte_value_hash_interpolates_into_a_string_with_the_same_encoding + assert_equal euc_jp('Hi ゆきひろ!'), @backend.send(:interpolate, nil, 'Hi {{name}}!', :name => euc_jp('ゆきひろ')) + end + + def test_interpolate_given_an_unicode_value_hash_into_a_non_unicode_multibyte_string_raises_encoding_compatibility_error + assert_raise(Encoding::CompatibilityError) do + @backend.send(:interpolate, nil, euc_jp('こんにちは、{{name}}さん!'), :name => 'ゆきひろ') + end + end + + def test_interpolate_given_a_non_unicode_multibyte_value_hash_into_an_unicode_string_raises_encoding_compatibility_error + assert_raise(Encoding::CompatibilityError) do + @backend.send(:interpolate, nil, 'こんにちは、{{name}}さん!', :name => euc_jp('ゆきひろ')) + end + end + end + def test_interpolate_given_nil_as_a_string_returns_nil assert_nil @backend.send(:interpolate, nil, nil, :name => 'David') end @@ -266,11 +292,17 @@ class I18nSimpleBackendInterpolateTest < Test::Unit::TestCase end def test_interpolate_given_an_empty_values_hash_raises_missing_interpolation_argument - assert_raises(I18n::MissingInterpolationArgument) { @backend.send(:interpolate, nil, 'Hi {{name}}!', {}) } + assert_raise(I18n::MissingInterpolationArgument) { @backend.send(:interpolate, nil, 'Hi {{name}}!', {}) } end def test_interpolate_given_a_string_containing_a_reserved_key_raises_reserved_interpolation_key - assert_raises(I18n::ReservedInterpolationKey) { @backend.send(:interpolate, nil, '{{default}}', {:default => nil}) } + assert_raise(I18n::ReservedInterpolationKey) { @backend.send(:interpolate, nil, '{{default}}', {:default => nil}) } + end + + private + + def euc_jp(string) + string.encode!(Encoding::EUC_JP) end end @@ -320,11 +352,11 @@ class I18nSimpleBackendLocalizeDateTest < Test::Unit::TestCase end def test_localize_nil_raises_argument_error - assert_raises(I18n::ArgumentError) { @backend.localize 'de', nil } + assert_raise(I18n::ArgumentError) { @backend.localize 'de', nil } end def test_localize_object_raises_argument_error - assert_raises(I18n::ArgumentError) { @backend.localize 'de', Object.new } + assert_raise(I18n::ArgumentError) { @backend.localize 'de', Object.new } end end @@ -454,7 +486,7 @@ class I18nSimpleBackendLoadTranslationsTest < Test::Unit::TestCase include I18nSimpleBackendTestSetup def test_load_translations_with_unknown_file_type_raises_exception - assert_raises(I18n::UnknownFileType) { @backend.load_translations "#{@locale_dir}/en.xml" } + assert_raise(I18n::UnknownFileType) { @backend.load_translations "#{@locale_dir}/en.xml" } end def test_load_translations_with_ruby_file_type_does_not_raise_exception @@ -485,6 +517,10 @@ end class I18nSimpleBackendLoadPathTest < Test::Unit::TestCase include I18nSimpleBackendTestSetup + def teardown + I18n.load_path = [] + end + def test_nested_load_paths_do_not_break_locale_loading @backend = I18n::Backend::Simple.new I18n.load_path = [[File.dirname(__FILE__) + '/locale/en.yml']] @@ -492,6 +528,14 @@ class I18nSimpleBackendLoadPathTest < Test::Unit::TestCase assert_nothing_raised { @backend.send :init_translations } assert_not_nil backend_get_translations end + + def test_adding_arrays_of_filenames_to_load_path_do_not_break_locale_loading + @backend = I18n::Backend::Simple.new + I18n.load_path << Dir[File.dirname(__FILE__) + '/locale/*.{rb,yml}'] + assert_nil backend_get_translations + assert_nothing_raised { @backend.send :init_translations } + assert_not_nil backend_get_translations + end end class I18nSimpleBackendReloadTranslationsTest < Test::Unit::TestCase @@ -521,4 +565,4 @@ class I18nSimpleBackendReloadTranslationsTest < Test::Unit::TestCase @backend.reload! assert_equal @backend.initialized?, false end -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/vendor/memcache-client-1.5.0.5/memcache.rb b/activesupport/lib/active_support/vendor/memcache-client-1.6.5/memcache.rb index e90ddf3359..4d594c2089 100644 --- a/activesupport/lib/active_support/vendor/memcache-client-1.5.0.5/memcache.rb +++ b/activesupport/lib/active_support/vendor/memcache-client-1.6.5/memcache.rb @@ -1,52 +1,21 @@ -# All original code copyright 2005, 2006, 2007 Bob Cottrell, Eric Hodel, -# The Robot Co-op. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the names of the authors nor the names of their contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS -# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, -# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT -# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR -# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE -# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - $TESTING = defined?($TESTING) && $TESTING require 'socket' require 'thread' require 'timeout' -require 'rubygems' require 'zlib' +require 'digest/sha1' ## # A Ruby client library for memcached. # -# This is intended to provide access to basic memcached functionality. It -# does not attempt to be complete implementation of the entire API, but it is -# approaching a complete implementation. class MemCache ## # The version of MemCache you are using. - VERSION = '1.5.0.5' + VERSION = '1.6.4.99' ## # Default options for the cache object. @@ -54,8 +23,10 @@ class MemCache DEFAULT_OPTIONS = { :namespace => nil, :readonly => false, - :multithread => false, - :failover => true + :multithread => true, + :failover => true, + :timeout => 0.5, + :logger => nil, } ## @@ -69,13 +40,6 @@ class MemCache DEFAULT_WEIGHT = 1 ## - # The amount of time to wait for a response from a memcached server. If a - # response is not completed within this time, the connection to the server - # will be closed and an error will be raised. - - attr_accessor :request_timeout - - ## # The namespace for this instance attr_reader :namespace @@ -91,9 +55,22 @@ class MemCache attr_reader :servers ## - # Whether this client should failover reads and writes to another server + # Socket timeout limit with this client, defaults to 0.5 sec. + # Set to nil to disable timeouts. + + attr_reader :timeout + + ## + # Should the client try to failover to another server if the + # first server is down? Defaults to true. + + attr_reader :failover + + ## + # Log debug/info/warn/error to the given Logger, defaults to nil. + + attr_reader :logger - attr_accessor :failover ## # Accepts a list of +servers+ and a list of +opts+. +servers+ may be # omitted. See +servers=+ for acceptable server list arguments. @@ -103,7 +80,11 @@ class MemCache # [:namespace] Prepends this value to all keys added or retrieved. # [:readonly] Raises an exception on cache writes when true. # [:multithread] Wraps cache access in a Mutex for thread safety. - # + # [:failover] Should the client try to failover to another server if the + # first server is down? Defaults to true. + # [:timeout] Time to use as the socket read timeout. Defaults to 0.5 sec, + # set to nil to disable timeouts (this is a major performance penalty in Ruby 1.8). + # [:logger] Logger to use for info/debug output, defaults to nil # Other options are ignored. def initialize(*args) @@ -130,9 +111,15 @@ class MemCache @namespace = opts[:namespace] @readonly = opts[:readonly] @multithread = opts[:multithread] - @failover = opts[:failover] + @timeout = opts[:timeout] + @failover = opts[:failover] + @logger = opts[:logger] @mutex = Mutex.new if @multithread - @buckets = [] + + logger.info { "memcache-client #{VERSION} #{Array(servers).inspect}" } if logger + + Thread.current[:memcache_client] = self.object_id if !@multithread + self.servers = servers end @@ -140,8 +127,8 @@ class MemCache # Returns a string representation of the cache object. def inspect - "<MemCache: %d servers, %d buckets, ns: %p, ro: %p>" % - [@servers.length, @buckets.length, @namespace, @readonly] + "<MemCache: %d servers, ns: %p, ro: %p>" % + [@servers.length, @namespace, @readonly] end ## @@ -162,7 +149,7 @@ class MemCache # Set the servers that the requests will be distributed between. Entries # can be either strings of the form "hostname:port" or # "hostname:port:weight" or MemCache::Server objects. - + # def servers=(servers) # Create the server objects. @servers = Array(servers).collect do |server| @@ -172,21 +159,17 @@ class MemCache port ||= DEFAULT_PORT weight ||= DEFAULT_WEIGHT Server.new self, host, port, weight - when Server - if server.memcache.multithread != @multithread then - raise ArgumentError, "can't mix threaded and non-threaded servers" - end - server else - raise TypeError, "cannot convert #{server.class} into MemCache::Server" + server end end - # Create an array of server buckets for weight selection of servers. - @buckets = [] - @servers.each do |server| - server.weight.times { @buckets.push(server) } - end + logger.debug { "Servers now: #{@servers.inspect}" } if logger + + # There's no point in doing this if there's only one server + @continuum = create_continuum_for(@servers) if @servers.size > 1 + + @servers end ## @@ -210,6 +193,7 @@ class MemCache def get(key, raw = false) with_server(key) do |server, cache_key| value = cache_get server, cache_key + logger.debug { "GET #{key} from #{server.inspect}: #{value ? value.to_s.size : 'nil'}" } if logger return nil if value.nil? value = Marshal.load value unless raw return value @@ -233,6 +217,8 @@ class MemCache # cache["a"] = 1 # cache["b"] = 2 # cache.get_multi "a", "b" # => { "a" => 1, "b" => 2 } + # + # Note that get_multi assumes the values are marshalled. def get_multi(*keys) raise MemCacheError, 'No active servers' unless active? @@ -252,15 +238,20 @@ class MemCache results = {} server_keys.each do |server, keys_for_server| - keys_for_server = keys_for_server.join ' ' - values = cache_get_multi server, keys_for_server - values.each do |key, value| - results[cache_keys[key]] = Marshal.load value + keys_for_server_str = keys_for_server.join ' ' + begin + values = cache_get_multi server, keys_for_server_str + values.each do |key, value| + results[cache_keys[key]] = Marshal.load value + end + rescue IndexError => e + # Ignore this server and try the others + logger.warn { "Unable to retrieve #{keys_for_server.size} elements from #{server.inspect}: #{e.message}"} if logger end end return results - rescue TypeError, IndexError => err + rescue TypeError => err handle_error nil, err end @@ -285,12 +276,19 @@ class MemCache # Warning: Readers should not call this method in the event of a cache miss; # see MemCache#add. + ONE_MB = 1024 * 1024 + def set(key, value, expiry = 0, raw = false) raise MemCacheError, "Update of readonly cache" if @readonly with_server(key) do |server, cache_key| value = Marshal.dump value unless raw - command = "set #{cache_key} 0 #{expiry} #{value.to_s.size}\r\n#{value}\r\n" + logger.debug { "SET #{key} to #{server.inspect}: #{value ? value.to_s.size : 'nil'}" } if logger + + data = value.to_s + raise MemCacheError, "Value too large, memcached can only store 1MB of data per key" if data.size > ONE_MB + + command = "set #{cache_key} 0 #{expiry} #{data.size}\r\n#{data}\r\n" with_socket_management(server) do |socket| socket.write command @@ -319,7 +317,8 @@ class MemCache raise MemCacheError, "Update of readonly cache" if @readonly with_server(key) do |server, cache_key| value = Marshal.dump value unless raw - command = "add #{cache_key} 0 #{expiry} #{value.size}\r\n#{value}\r\n" + logger.debug { "ADD #{key} to #{server}: #{value ? value.to_s.size : 'nil'}" } if logger + command = "add #{cache_key} 0 #{expiry} #{value.to_s.size}\r\n#{value}\r\n" with_socket_management(server) do |socket| socket.write command @@ -353,7 +352,6 @@ class MemCache raise MemCacheError, "Update of readonly cache" if @readonly begin - @mutex.lock if @multithread @servers.each do |server| with_socket_management(server) do |socket| socket.write "flush_all\r\n" @@ -364,8 +362,6 @@ class MemCache end rescue IndexError => err handle_error nil, err - ensure - @mutex.unlock if @multithread end end @@ -424,7 +420,7 @@ class MemCache while line = socket.gets do raise_on_error_response! line break if line == "END\r\n" - if line =~ /\ASTAT ([\w]+) ([\w\.\:]+)/ then + if line =~ /\ASTAT ([\S]+) ([\w\.\:]+)/ then name, value = $1, $2 stats[name] = case name when 'version' @@ -478,6 +474,14 @@ class MemCache end ## + # Returns an interoperable hash value for +key+. (I think, docs are + # sketchy for down servers). + + def hash_for(key) + Zlib.crc32(key) + end + + ## # Pick a server to handle the request based on a hash of the key. def get_server_for_key(key, options = {}) @@ -487,27 +491,17 @@ class MemCache raise MemCacheError, "No servers available" if @servers.empty? return @servers.first if @servers.length == 1 - hkey = hash_for key - - if @failover - 20.times do |try| - server = @buckets[hkey % @buckets.compact.size] - return server if server.alive? - hkey += hash_for "#{try}#{key}" - end - else - return @buckets[hkey % @buckets.compact.size] - end - - raise MemCacheError, "No servers available" - end + hkey = hash_for(key) - ## - # Returns an interoperable hash value for +key+. (I think, docs are - # sketchy for down servers). + 20.times do |try| + entryidx = Continuum.binary_search(@continuum, hkey) + server = @continuum[entryidx].server + return server if server.alive? + break unless failover + hkey = hash_for "#{try}#{key}" + end - def hash_for(key) - (Zlib.crc32(key) >> 16) & 0x7fff + raise MemCacheError, "No servers available" end ## @@ -608,24 +602,28 @@ class MemCache # failures (but does still apply to unexpectedly lost connections etc.). def with_socket_management(server, &block) + check_multithread_status! + @mutex.lock if @multithread retried = false - + begin socket = server.socket # Raise an IndexError to show this server is out of whack. If were inside # a with_server block, we'll catch it and attempt to restart the operation. - + raise IndexError, "No connection to server (#{server.status})" if socket.nil? - + block.call(socket) - + rescue SocketError => err - server.mark_dead(err.message) + logger.warn { "Socket failure: #{err.message}" } if logger + server.mark_dead(err) handle_error(server, err) - rescue MemCacheError, SocketError, SystemCallError, IOError => err + rescue MemCacheError, SystemCallError, IOError => err + logger.warn { "Generic failure: #{err.class.name}: #{err.message}" } if logger handle_error(server, err) if retried || socket.nil? retried = true retry @@ -640,8 +638,9 @@ class MemCache server, cache_key = request_setup(key) yield server, cache_key rescue IndexError => e + logger.warn { "Server failed: #{e.class.name}: #{e.message}" } if logger if !retried && @servers.size > 1 - puts "Connection to server #{server.inspect} DIED! Retrying operation..." + logger.info { "Connection to server #{server.inspect} DIED! Retrying operation..." } if logger retried = true retry end @@ -677,6 +676,37 @@ class MemCache end end + def create_continuum_for(servers) + total_weight = servers.inject(0) { |memo, srv| memo + srv.weight } + continuum = [] + + servers.each do |server| + entry_count_for(server, servers.size, total_weight).times do |idx| + hash = Digest::SHA1.hexdigest("#{server.host}:#{server.port}:#{idx}") + value = Integer("0x#{hash[0..7]}") + continuum << Continuum::Entry.new(value, server) + end + end + + continuum.sort { |a, b| a.value <=> b.value } + end + + def entry_count_for(server, total_servers, total_weight) + ((total_servers * Continuum::POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor + end + + def check_multithread_status! + return if @multithread + + if Thread.current[:memcache_client] != self.object_id + raise MemCacheError, <<-EOM + You are accessing this memcache-client instance from multiple threads but have not enabled multithread support. + Normally: MemCache.new(['localhost:11211'], :multithread => true) + In Rails: config.cache_store = [:mem_cache_store, 'localhost:11211', { :multithread => true }] + EOM + end + end + ## # This class represents a memcached server instance. @@ -690,13 +720,6 @@ class MemCache CONNECT_TIMEOUT = 0.25 ## - # The amount of time to wait for a response from a memcached server. - # If a response isn't received within this time limit, - # the server will be marked as down. - - SOCKET_TIMEOUT = 0.5 - - ## # The amount of time to wait before attempting to re-establish a # connection with a server that is marked dead. @@ -727,6 +750,8 @@ class MemCache attr_reader :status + attr_reader :logger + ## # Create a new MemCache::Server object for the memcached instance # listening on the given host and port, weighted by the given weight. @@ -735,17 +760,15 @@ class MemCache raise ArgumentError, "No host specified" if host.nil? or host.empty? raise ArgumentError, "No port specified" if port.nil? or port.to_i.zero? - @memcache = memcache @host = host @port = port.to_i @weight = weight.to_i - @multithread = @memcache.multithread - @mutex = Mutex.new - @sock = nil @retry = nil @status = 'NOT CONNECTED' + @timeout = memcache.timeout + @logger = memcache.logger end ## @@ -770,7 +793,6 @@ class MemCache # Returns the connected socket object on success or nil on failure. def socket - @mutex.lock if @multithread return @sock if @sock and not @sock.closed? @sock = nil @@ -780,8 +802,7 @@ class MemCache # Attempt to connect if not already connected. begin - - @sock = TCPTimeoutSocket.new @host, @port + @sock = @timeout ? TCPTimeoutSocket.new(@host, @port, @timeout) : TCPSocket.new(@host, @port) if Socket.constants.include? 'TCP_NODELAY' then @sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1 @@ -789,12 +810,11 @@ class MemCache @retry = nil @status = 'CONNECTED' rescue SocketError, SystemCallError, IOError, Timeout::Error => err - mark_dead err.message + logger.warn { "Unable to open socket: #{err.class.name}, #{err.message}" } if logger + mark_dead err end return @sock - ensure - @mutex.unlock if @multithread end ## @@ -802,24 +822,23 @@ class MemCache # object. The server is not considered dead. def close - @mutex.lock if @multithread @sock.close if @sock && !@sock.closed? @sock = nil @retry = nil @status = "NOT CONNECTED" - ensure - @mutex.unlock if @multithread end ## # Mark the server as dead and close its socket. - def mark_dead(reason = "Unknown error") + def mark_dead(error) @sock.close if @sock && !@sock.closed? @sock = nil @retry = Time.now + RETRY_DELAY - @status = sprintf "%s:%s DEAD: %s, will retry at %s", @host, @port, reason, @retry + reason = "#{error.class.name}: #{error.message}" + @status = sprintf "%s:%s DEAD (%s), will retry at %s", @host, @port, reason, @retry + @logger.info { @status } if @logger end end @@ -833,36 +852,84 @@ end # TCPSocket facade class which implements timeouts. class TCPTimeoutSocket - def initialize(*args) + + def initialize(host, port, timeout) Timeout::timeout(MemCache::Server::CONNECT_TIMEOUT, SocketError) do - @sock = TCPSocket.new(*args) - @len = MemCache::Server::SOCKET_TIMEOUT.to_f || 0.5 + @sock = TCPSocket.new(host, port) + @len = timeout end end - + def write(*args) Timeout::timeout(@len, SocketError) do @sock.write(*args) end end - + def gets(*args) Timeout::timeout(@len, SocketError) do @sock.gets(*args) end end - + def read(*args) Timeout::timeout(@len, SocketError) do @sock.read(*args) end end - + def _socket @sock end - + def method_missing(meth, *args) @sock.__send__(meth, *args) end -end
\ No newline at end of file + + def closed? + @sock.closed? + end + + def close + @sock.close + end +end + +module Continuum + POINTS_PER_SERVER = 160 # this is the default in libmemcached + + # Find the closest index in Continuum with value <= the given value + def self.binary_search(ary, value, &block) + upper = ary.size - 1 + lower = 0 + idx = 0 + + while(lower <= upper) do + idx = (lower + upper) / 2 + comp = ary[idx].value <=> value + + if comp == 0 + return idx + elsif comp > 0 + upper = idx - 1 + else + lower = idx + 1 + end + end + return upper + end + + class Entry + attr_reader :value + attr_reader :server + + def initialize(val, srv) + @value = val + @server = srv + end + + def inspect + "<#{value}, #{server.host}:#{server.port}>" + end + end +end diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb index 3e2b29b327..30f598a8de 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -2,7 +2,7 @@ module ActiveSupport module VERSION #:nodoc: MAJOR = 2 MINOR = 3 - TINY = 0 + TINY = 2 STRING = [MAJOR, MINOR, TINY].join('.') end diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index bfc3d7b00b..ccd1349491 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -1,111 +1,31 @@ -# = XmlMini -# This is a derivitive work of XmlSimple 1.0.11 -# Author:: Joseph Holsten <joseph@josephholsten.com> -# Copyright:: Copyright (c) 2008 Joseph Holsten -# Copyright:: Copyright (c) 2003-2006 Maik Schmidt <contact@maik-schmidt.de> -# License:: Distributes under the same terms as Ruby. -module XmlMini - extend self - - CONTENT_KEY = '__content__'.freeze - - # Parse an XML Document string into a simple hash - # - # Same as XmlSimple::xml_in but doesn't shoot itself in the foot, - # and uses the defaults from ActiveSupport +module ActiveSupport + # = XmlMini # - # string:: - # XML Document string to parse - def parse(string) - require 'rexml/document' unless defined?(REXML::Document) - doc = REXML::Document.new(string) - merge_element!({}, doc.root) - end - - private - # Convert an XML element and merge into the hash - # - # hash:: - # Hash to merge the converted element into. - # element:: - # XML element to merge into hash - def merge_element!(hash, element) - merge!(hash, element.name, collapse(element)) - end - - # Actually converts an XML document element into a data structure. - # - # element:: - # The document element to be collapsed. - def collapse(element) - hash = get_attributes(element) - - if element.has_elements? - element.each_element {|child| merge_element!(hash, child) } - merge_texts!(hash, element) unless empty_content?(element) - hash - else - merge_texts!(hash, element) - end - end - - # Merge all the texts of an element into the hash - # - # hash:: - # Hash to add the converted emement to. - # element:: - # XML element whose texts are to me merged into the hash - def merge_texts!(hash, element) - unless element.has_text? - hash + # To use the much faster libxml parser: + # gem 'libxml-ruby', '=0.9.7' + # XmlMini.backend = 'LibXML' + module XmlMini + extend self + + attr_reader :backend + delegate :parse, :to => :backend + + def backend=(name) + if name.is_a?(Module) + @backend = name else - # must use value to prevent double-escaping - merge!(hash, CONTENT_KEY, element.texts.sum(&:value)) + require "active_support/xml_mini/#{name.to_s.downcase}.rb" + @backend = ActiveSupport.const_get("XmlMini_#{name}") end end - # Adds a new key/value pair to an existing Hash. If the key to be added - # already exists and the existing value associated with key is not - # an Array, it will be wrapped in an Array. Then the new value is - # appended to that Array. - # - # hash:: - # Hash to add key/value pair to. - # key:: - # Key to be added. - # value:: - # Value to be associated with key. - def merge!(hash, key, value) - if hash.has_key?(key) - if hash[key].instance_of?(Array) - hash[key] << value - else - hash[key] = [hash[key], value] - end - elsif value.instance_of?(Array) - hash[key] = [value] - else - hash[key] = value - end - hash - end - - # Converts the attributes array of an XML element into a hash. - # Returns an empty Hash if node has no attributes. - # - # element:: - # XML element to extract attributes from. - def get_attributes(element) - attributes = {} - element.attributes.each { |n,v| attributes[n] = v } - attributes + def with_backend(name) + old_backend, self.backend = backend, name + yield + ensure + self.backend = old_backend end + end - # Determines if a document element has text content - # - # element:: - # XML element to be checked. - def empty_content?(element) - element.texts.join.blank? - end + XmlMini.backend = 'REXML' end diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb new file mode 100644 index 0000000000..3586b24a6b --- /dev/null +++ b/activesupport/lib/active_support/xml_mini/libxml.rb @@ -0,0 +1,133 @@ +require 'libxml' + +# = XmlMini LibXML implementation +module ActiveSupport + module XmlMini_LibXML #:nodoc: + extend self + + # Parse an XML Document string into a simple hash using libxml. + # string:: + # XML Document string to parse + def parse(string) + LibXML::XML.default_keep_blanks = false + + if string.blank? + {} + else + LibXML::XML::Parser.string(string.strip).parse.to_hash + end + end + + end +end + +module LibXML + module Conversions + module Document + def to_hash + root.to_hash + end + end + + module Node + CONTENT_ROOT = '__content__' + LIB_XML_LIMIT = 30000000 # Hardcoded LibXML limit + + # Convert XML document to hash + # + # hash:: + # Hash to merge the converted element into. + def to_hash(hash={}) + if text? + raise LibXML::XML::Error if content.length >= LIB_XML_LIMIT + hash[CONTENT_ROOT] = content + else + sub_hash = insert_name_into_hash(hash, name) + attributes_to_hash(sub_hash) + if array? + children_array_to_hash(sub_hash) + elsif yaml? + children_yaml_to_hash(sub_hash) + else + children_to_hash(sub_hash) + end + end + hash + end + + protected + + # Insert name into hash + # + # hash:: + # Hash to merge the converted element into. + # name:: + # name to to merge into hash + def insert_name_into_hash(hash, name) + sub_hash = {} + if hash[name] + if !hash[name].kind_of? Array + hash[name] = [hash[name]] + end + hash[name] << sub_hash + else + hash[name] = sub_hash + end + sub_hash + end + + # Insert children into hash + # + # hash:: + # Hash to merge the children into. + def children_to_hash(hash={}) + each { |child| child.to_hash(hash) } + attributes_to_hash(hash) + hash + end + + # Convert xml attributes to hash + # + # hash:: + # Hash to merge the attributes into + def attributes_to_hash(hash={}) + each_attr { |attr| hash[attr.name] = attr.value } + hash + end + + # Convert array into hash + # + # hash:: + # Hash to merge the array into + def children_array_to_hash(hash={}) + hash[child.name] = map do |child| + returning({}) { |sub_hash| child.children_to_hash(sub_hash) } + end + hash + end + + # Convert yaml into hash + # + # hash:: + # Hash to merge the yaml into + def children_yaml_to_hash(hash = {}) + hash[CONTENT_ROOT] = content unless content.blank? + hash + end + + # Check if child is of type array + def array? + child? && child.next? && child.name == child.next.name + end + + # Check if child is of type yaml + def yaml? + attributes.collect{|x| x.value}.include?('yaml') + end + + end + end +end + +LibXML::XML::Document.send(:include, LibXML::Conversions::Document) +LibXML::XML::Node.send(:include, LibXML::Conversions::Node) diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb new file mode 100644 index 0000000000..10281584fc --- /dev/null +++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb @@ -0,0 +1,77 @@ +require 'nokogiri' + +# = XmlMini Nokogiri implementation +module ActiveSupport + module XmlMini_Nokogiri #:nodoc: + extend self + + # Parse an XML Document string into a simple hash using libxml / nokogiri. + # string:: + # XML Document string to parse + def parse(string) + if string.blank? + {} + else + doc = Nokogiri::XML(string) + raise doc.errors.first if doc.errors.length > 0 + doc.to_hash + end + end + + module Conversions + module Document + def to_hash + root.to_hash + end + end + + module Node + CONTENT_ROOT = '__content__' + + # Convert XML document to hash + # + # hash:: + # Hash to merge the converted element into. + def to_hash(hash = {}) + hash[name] ||= attributes_as_hash + + walker = lambda { |memo, parent, child, callback| + next if child.blank? && 'file' != parent['type'] + + if child.text? + (memo[CONTENT_ROOT] ||= '') << child.content + next + end + + name = child.name + + child_hash = child.attributes_as_hash + if memo[name] + memo[name] = [memo[name]].flatten + memo[name] << child_hash + else + memo[name] = child_hash + end + + # Recusively walk children + child.children.each { |c| + callback.call(child_hash, child, c, callback) + } + } + + children.each { |c| walker.call(hash[name], self, c, walker) } + hash + end + + def attributes_as_hash + Hash[*(attribute_nodes.map { |node| + [node.node_name, node.value] + }.flatten)] + end + end + end + + Nokogiri::XML::Document.send(:include, Conversions::Document) + Nokogiri::XML::Node.send(:include, Conversions::Node) + end +end diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb new file mode 100644 index 0000000000..a8fdeca967 --- /dev/null +++ b/activesupport/lib/active_support/xml_mini/rexml.rb @@ -0,0 +1,108 @@ +# = XmlMini ReXML implementation +module ActiveSupport + module XmlMini_REXML #:nodoc: + extend self + + CONTENT_KEY = '__content__'.freeze + + # Parse an XML Document string into a simple hash + # + # Same as XmlSimple::xml_in but doesn't shoot itself in the foot, + # and uses the defaults from ActiveSupport + # + # string:: + # XML Document string to parse + def parse(string) + require 'rexml/document' unless defined?(REXML::Document) + doc = REXML::Document.new(string) + merge_element!({}, doc.root) + end + + private + # Convert an XML element and merge into the hash + # + # hash:: + # Hash to merge the converted element into. + # element:: + # XML element to merge into hash + def merge_element!(hash, element) + merge!(hash, element.name, collapse(element)) + end + + # Actually converts an XML document element into a data structure. + # + # element:: + # The document element to be collapsed. + def collapse(element) + hash = get_attributes(element) + + if element.has_elements? + element.each_element {|child| merge_element!(hash, child) } + merge_texts!(hash, element) unless empty_content?(element) + hash + else + merge_texts!(hash, element) + end + end + + # Merge all the texts of an element into the hash + # + # hash:: + # Hash to add the converted emement to. + # element:: + # XML element whose texts are to me merged into the hash + def merge_texts!(hash, element) + unless element.has_text? + hash + else + # must use value to prevent double-escaping + merge!(hash, CONTENT_KEY, element.texts.sum(&:value)) + end + end + + # Adds a new key/value pair to an existing Hash. If the key to be added + # already exists and the existing value associated with key is not + # an Array, it will be wrapped in an Array. Then the new value is + # appended to that Array. + # + # hash:: + # Hash to add key/value pair to. + # key:: + # Key to be added. + # value:: + # Value to be associated with key. + def merge!(hash, key, value) + if hash.has_key?(key) + if hash[key].instance_of?(Array) + hash[key] << value + else + hash[key] = [hash[key], value] + end + elsif value.instance_of?(Array) + hash[key] = [value] + else + hash[key] = value + end + hash + end + + # Converts the attributes array of an XML element into a hash. + # Returns an empty Hash if node has no attributes. + # + # element:: + # XML element to extract attributes from. + def get_attributes(element) + attributes = {} + element.attributes.each { |n,v| attributes[n] = v } + attributes + end + + # Determines if a document element has text content + # + # element:: + # XML element to be checked. + def empty_content?(element) + element.texts.join.blank? + end + end +end |