mirror of
https://github.com/github/rails.git
synced 2026-01-12 08:08:31 -05:00
Compare commits
28 Commits
github29
...
activesupp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
775febba74 | ||
|
|
d33a754a92 | ||
|
|
c12cd651c2 | ||
|
|
f709b5d1c8 | ||
|
|
c6f9ec2d8d | ||
|
|
d7440a463c | ||
|
|
7655b80261 | ||
|
|
c5be730a2e | ||
|
|
052556e5cf | ||
|
|
173bc3c9e5 | ||
|
|
01280149f2 | ||
|
|
79c1106fb2 | ||
|
|
8f6982c04b | ||
|
|
a471098ab8 | ||
|
|
87ef1f0e73 | ||
|
|
e42c679e43 | ||
|
|
5c4dfa63f7 | ||
|
|
c394fd82fa | ||
|
|
49933594c1 | ||
|
|
94fae25703 | ||
|
|
05cb9e6854 | ||
|
|
1a5734e0b5 | ||
|
|
24e5712294 | ||
|
|
8f6bafc333 | ||
|
|
c717a84b5d | ||
|
|
d537304b20 | ||
|
|
ca90ecf2cb | ||
|
|
4bb1d3ef20 |
101
Gemfile
Normal file
101
Gemfile
Normal file
@@ -0,0 +1,101 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gemspec
|
||||
|
||||
if ENV['AREL']
|
||||
gem 'arel', :path => ENV['AREL']
|
||||
else
|
||||
gem 'arel'
|
||||
end
|
||||
|
||||
gem 'bcrypt-ruby', '~> 3.0.0'
|
||||
#gem 'jquery-rails'
|
||||
|
||||
if ENV['JOURNEY']
|
||||
gem 'journey', :path => ENV['JOURNEY']
|
||||
else
|
||||
gem 'journey', :git => 'git://github.com/rails/journey.git', :branch => '1-0-stable'
|
||||
end
|
||||
|
||||
# This needs to be with require false to avoid
|
||||
# it being automatically loaded by sprockets
|
||||
#gem 'uglifier', '>= 1.0.3', :require => false
|
||||
|
||||
gem 'rake', '>= 0.8.7'
|
||||
gem 'mocha', '>= 0.13.0', :require => false
|
||||
|
||||
group :doc do
|
||||
# The current sdoc cannot generate GitHub links due
|
||||
# to a bug, but the PR that fixes it has been there
|
||||
# for some weeks unapplied. As a temporary solution
|
||||
# this is our own fork with the fix.
|
||||
gem 'sdoc', :git => 'git://github.com/fxn/sdoc.git'
|
||||
gem 'RedCloth', '~> 4.2'
|
||||
gem 'w3c_validators'
|
||||
end
|
||||
|
||||
# AS
|
||||
gem 'memcache-client', '>= 1.8.5'
|
||||
|
||||
platforms :mri_18 do
|
||||
gem 'system_timer'
|
||||
gem 'json'
|
||||
end
|
||||
|
||||
# Add your own local bundler stuff
|
||||
instance_eval File.read '.Gemfile' if File.exists? '.Gemfile'
|
||||
|
||||
platforms :mri do
|
||||
group :test do
|
||||
gem 'ruby-prof', '~> 0.11.2' if RUBY_VERSION < '2.0'
|
||||
end
|
||||
end
|
||||
|
||||
platforms :ruby do
|
||||
gem 'yajl-ruby'
|
||||
gem 'nokogiri', '>= 1.4.5'
|
||||
|
||||
# AR
|
||||
gem 'sqlite3', '~> 1.3.5'
|
||||
|
||||
group :db do
|
||||
gem 'pg', '>= 0.11.0'
|
||||
gem 'mysql', '>= 2.8.1'
|
||||
gem 'mysql2', '>= 0.3.10'
|
||||
end
|
||||
end
|
||||
|
||||
platforms :jruby do
|
||||
gem 'json'
|
||||
gem 'activerecord-jdbcsqlite3-adapter', '>= 1.2.0'
|
||||
|
||||
# This is needed by now to let tests work on JRuby
|
||||
# TODO: When the JRuby guys merge jruby-openssl in
|
||||
# jruby this will be removed
|
||||
gem 'jruby-openssl'
|
||||
|
||||
group :db do
|
||||
gem 'activerecord-jdbcmysql-adapter', '>= 1.2.0'
|
||||
gem 'activerecord-jdbcpostgresql-adapter', '>= 1.2.0'
|
||||
end
|
||||
end
|
||||
|
||||
# gems that are necessary for ActiveRecord tests with Oracle database
|
||||
if ENV['ORACLE_ENHANCED_PATH'] || ENV['ORACLE_ENHANCED']
|
||||
platforms :ruby do
|
||||
gem 'ruby-oci8', '>= 2.0.4'
|
||||
end
|
||||
if ENV['ORACLE_ENHANCED_PATH']
|
||||
gem 'activerecord-oracle_enhanced-adapter', :path => ENV['ORACLE_ENHANCED_PATH']
|
||||
else
|
||||
gem 'activerecord-oracle_enhanced-adapter', :git => 'git://github.com/rsim/oracle-enhanced.git'
|
||||
end
|
||||
end
|
||||
|
||||
# A gem necessary for ActiveRecord tests with IBM DB
|
||||
gem 'ibm_db' if ENV['IBM_DB']
|
||||
|
||||
gem 'benchmark-ips'
|
||||
|
||||
gem "tzinfo"
|
||||
gem "builder"
|
||||
16
Gemfile.sh
16
Gemfile.sh
@@ -1,7 +1,9 @@
|
||||
gem install mocha -v=0.13.1
|
||||
gem install rake -v=10.1.0
|
||||
gem install rdoc -v=4.0.1
|
||||
gem install sqlite3 -v=1.3.7
|
||||
gem install rack -v=1.4.5
|
||||
gem install erubis -v=2.7.0
|
||||
gem install json -v=1.8.0
|
||||
gem install mocha -v=0.13.1
|
||||
gem install rake -v=10.1.0
|
||||
gem install rdoc -v=4.0.1
|
||||
gem install sqlite3 -v=1.3.7
|
||||
gem install rack -v=1.4.5
|
||||
gem install erubis -v=2.7.0
|
||||
gem install json -v=1.8.0
|
||||
gem install multi_json -v=1.8.2
|
||||
gem install i18n -v=0.6.5
|
||||
|
||||
1
RAILS_VERSION
Normal file
1
RAILS_VERSION
Normal file
@@ -0,0 +1 @@
|
||||
2.3.14.github30
|
||||
7
Rakefile
7
Rakefile
@@ -3,7 +3,7 @@ require 'rdoc/task'
|
||||
|
||||
env = %(PKG_BUILD="#{ENV['PKG_BUILD']}") if ENV['PKG_BUILD']
|
||||
|
||||
PROJECTS = %w(activesupport railties actionpack actionmailer activeresource activerecord)
|
||||
PROJECTS = %w(activesupport railties actionpack actionmailer activerecord)
|
||||
|
||||
Dir["#{File.dirname(__FILE__)}/*/lib/*/version.rb"].each do |version_path|
|
||||
require version_path
|
||||
@@ -48,11 +48,6 @@ RDoc::Task.new do |rdoc|
|
||||
rdoc.rdoc_files.include('activerecord/lib/active_record/**/*.rb')
|
||||
rdoc.rdoc_files.exclude('activerecord/lib/active_record/vendor/*')
|
||||
|
||||
rdoc.rdoc_files.include('activeresource/README')
|
||||
rdoc.rdoc_files.include('activeresource/CHANGELOG')
|
||||
rdoc.rdoc_files.include('activeresource/lib/active_resource.rb')
|
||||
rdoc.rdoc_files.include('activeresource/lib/active_resource/*')
|
||||
|
||||
rdoc.rdoc_files.include('actionpack/README')
|
||||
rdoc.rdoc_files.include('actionpack/CHANGELOG')
|
||||
rdoc.rdoc_files.include('actionpack/lib/action_controller/**/*.rb')
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).chomp
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'actionmailer'
|
||||
s.version = '2.3.18'
|
||||
s.version = version
|
||||
s.summary = 'Service layer for easy email delivery and testing.'
|
||||
s.description = 'Makes it trivial to test and deliver emails sent from a single service layer.'
|
||||
|
||||
@@ -10,5 +12,5 @@ Gem::Specification.new do |s|
|
||||
|
||||
s.require_path = 'lib'
|
||||
|
||||
s.add_dependency 'actionpack', '= 2.3.18'
|
||||
s.add_dependency 'actionpack', "= #{version}"
|
||||
end
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).chomp
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'actionpack'
|
||||
s.version = '2.3.18'
|
||||
s.version = version
|
||||
s.summary = 'Web-flow and rendering framework putting the VC in MVC.'
|
||||
s.description = 'Eases web-request routing, handling, and response as a half-way front, half-way page controller. Implemented with specific emphasis on enabling easy unit/integration testing that doesn\'t require a browser.'
|
||||
|
||||
@@ -10,6 +12,7 @@ Gem::Specification.new do |s|
|
||||
|
||||
s.require_path = 'lib'
|
||||
|
||||
s.add_dependency 'activesupport', '= 2.3.18'
|
||||
s.add_dependency 'activesupport', "= #{version}"
|
||||
s.add_dependency 'rack', '~> 1.4'
|
||||
s.add_dependency 'erubis', '~> 2.7.0'
|
||||
end
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
rescue LoadError
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
if File.directory?(activesupport_path)
|
||||
$:.unshift activesupport_path
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ module ActionController #:nodoc:
|
||||
# Sets the token value for the current session. Pass a <tt>:secret</tt> option
|
||||
# in +protect_from_forgery+ to add a custom salt to the hash.
|
||||
def form_authenticity_token
|
||||
session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32)
|
||||
session[:_csrf_token] ||= SecureRandom.base64(32)
|
||||
end
|
||||
|
||||
def protect_against_forgery?
|
||||
|
||||
@@ -212,7 +212,7 @@ module ActionController
|
||||
end
|
||||
|
||||
def generate_sid
|
||||
ActiveSupport::SecureRandom.hex(16)
|
||||
SecureRandom.hex(16)
|
||||
end
|
||||
|
||||
def load_session(env)
|
||||
|
||||
@@ -201,7 +201,7 @@ module ActionController
|
||||
|
||||
if secret.length < SECRET_MIN_LENGTH
|
||||
raise ArgumentError, "Secret should be something secure, " +
|
||||
"like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " +
|
||||
"like \"#{SecureRandom.hex(16)}\". The value you " +
|
||||
"provided, \"#{secret}\", is shorter than the minimum length " +
|
||||
"of #{SECRET_MIN_LENGTH} characters"
|
||||
end
|
||||
@@ -213,7 +213,7 @@ module ActionController
|
||||
end
|
||||
|
||||
def generate_sid
|
||||
ActiveSupport::SecureRandom.hex(16)
|
||||
SecureRandom.hex(16)
|
||||
end
|
||||
|
||||
def destroy(env)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
require 'active_support/core_ext/string/bytesize'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
# Methods for sending arbitrary data and for streaming files to the browser,
|
||||
# instead of rendering.
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
rescue LoadError
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
if File.directory?(activesupport_path)
|
||||
$:.unshift activesupport_path
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
$:.unshift File.expand_path('../../lib', __FILE__)
|
||||
$:.unshift File.expand_path('../../../activesupport/lib', __FILE__)
|
||||
$:.unshift File.expand_path('../fixtures/helpers', __FILE__)
|
||||
$:.unshift File.expand_path('../fixtures/alternate_helpers', __FILE__)
|
||||
begin
|
||||
old, $VERBOSE = $VERBOSE, nil
|
||||
require File.expand_path('../../../load_paths', __FILE__)
|
||||
ensure
|
||||
$VERBOSE = old
|
||||
end
|
||||
|
||||
require 'rubygems'
|
||||
require 'yaml'
|
||||
|
||||
@@ -79,7 +79,7 @@ module RequestForgeryProtectionTests
|
||||
def setup
|
||||
@token = "cf50faa3fe97702ca1ae"
|
||||
|
||||
ActiveSupport::SecureRandom.stubs(:base64).returns(@token)
|
||||
SecureRandom.stubs(:base64).returns(@token)
|
||||
ActionController::Base.request_forgery_protection_token = :authenticity_token
|
||||
end
|
||||
|
||||
@@ -186,7 +186,7 @@ class RequestForgeryProtectionControllerTest < ActionController::TestCase
|
||||
include RequestForgeryProtectionTests
|
||||
|
||||
test 'should emit a csrf-token meta tag' do
|
||||
ActiveSupport::SecureRandom.stubs(:base64).returns(@token + '<=?')
|
||||
SecureRandom.stubs(:base64).returns(@token + '<=?')
|
||||
get :meta
|
||||
assert_equal %(<meta name="csrf-param" content="authenticity_token"/>\n<meta name="csrf-token" content="cf50faa3fe97702ca1ae<=?"/>), @response.body
|
||||
end
|
||||
@@ -208,7 +208,7 @@ class FreeCookieControllerTest < ActionController::TestCase
|
||||
@response = ActionController::TestResponse.new
|
||||
@token = "cf50faa3fe97702ca1ae"
|
||||
|
||||
ActiveSupport::SecureRandom.stubs(:base64).returns(@token)
|
||||
SecureRandom.stubs(:base64).returns(@token)
|
||||
end
|
||||
|
||||
def test_should_not_render_form_with_token_tag
|
||||
|
||||
@@ -8,7 +8,7 @@ require 'active_model'
|
||||
require 'active_model/state_machine'
|
||||
|
||||
$:.unshift File.expand_path('../../../activesupport/lib', __FILE__)
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
require 'active_support/test_case'
|
||||
|
||||
class ActiveModel::TestCase < ActiveSupport::TestCase
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).chomp
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'activerecord'
|
||||
s.version = '2.3.18'
|
||||
s.version = version
|
||||
s.summary = 'Implements the ActiveRecord pattern for ORM.'
|
||||
s.description = 'Implements the ActiveRecord pattern (Fowler, PoEAA) for ORM. It ties database tables and classes together for business objects, like Customer or Subscription, that can find, save, and destroy themselves without resorting to manual SQL.'
|
||||
|
||||
@@ -13,5 +15,5 @@ Gem::Specification.new do |s|
|
||||
s.rdoc_options = ['--main', 'README']
|
||||
s.extra_rdoc_files = ['README']
|
||||
|
||||
s.add_dependency 'activesupport', '= 2.3.18'
|
||||
s.add_dependency 'activesupport', "= #{version}"
|
||||
end
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
rescue LoadError
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
if File.directory?(activesupport_path)
|
||||
$:.unshift activesupport_path
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,6 +56,7 @@ module ActiveRecord
|
||||
autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
|
||||
autoload :Migration, 'active_record/migration'
|
||||
autoload :Migrator, 'active_record/migration'
|
||||
autoload :ModelName, 'active_record/model_name'
|
||||
autoload :NamedScope, 'active_record/named_scope'
|
||||
autoload :NestedAttributes, 'active_record/nested_attributes'
|
||||
autoload :Observing, 'active_record/observer'
|
||||
|
||||
@@ -47,14 +47,29 @@ module ActiveRecord
|
||||
# instantiation of the actual post records.
|
||||
class AssociationProxy #:nodoc:
|
||||
alias_method :proxy_respond_to?, :respond_to?
|
||||
alias_method :proxy_extend, :extend
|
||||
delegate :to_param, :to => :proxy_target
|
||||
instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id)$|^__|^respond_to_missing|proxy_/ }
|
||||
|
||||
def self.new(owner, reflection)
|
||||
klass =
|
||||
reflection.cached_extend_class ||=
|
||||
if reflection.options[:extend]
|
||||
const_name = "AR_CACHED_EXTEND_CLASS_#{reflection.name}_#{reflection.options[:extend].join("_").gsub("::","_")}"
|
||||
reflection.active_record.const_set(const_name, Class.new(self) do
|
||||
include *reflection.options[:extend]
|
||||
end)
|
||||
else
|
||||
self
|
||||
end
|
||||
|
||||
proxy = klass.allocate
|
||||
proxy.send(:initialize, owner, reflection)
|
||||
proxy
|
||||
end
|
||||
|
||||
def initialize(owner, reflection)
|
||||
@owner, @reflection = owner, reflection
|
||||
reflection.check_validity!
|
||||
Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
|
||||
reset
|
||||
end
|
||||
|
||||
|
||||
@@ -2487,6 +2487,12 @@ module ActiveRecord #:nodoc:
|
||||
result
|
||||
end
|
||||
|
||||
# Returns an ActiveRecord::ModelName object for module. It can be
|
||||
# used to retrieve all kinds of naming-related information.
|
||||
def model_name
|
||||
@model_name ||= ::ActiveRecord::ModelName.new(name)
|
||||
end
|
||||
|
||||
# A model instance's primary key is always available as model.id
|
||||
# whether you name it the default 'id' or set it to something else.
|
||||
def id
|
||||
|
||||
25
activerecord/lib/active_record/model_name.rb
Normal file
25
activerecord/lib/active_record/model_name.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module ActiveRecord
|
||||
class ModelName < String
|
||||
alias_method :cache_key, :collection
|
||||
|
||||
def singular
|
||||
@singular ||= ActiveSupport::Inflector.underscore(self).tr('/', '_').freeze
|
||||
end
|
||||
|
||||
def plural
|
||||
@plural ||= ActiveSupport::Inflector.pluralize(singular).freeze
|
||||
end
|
||||
|
||||
def element
|
||||
@element ||= ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze
|
||||
end
|
||||
|
||||
def collection
|
||||
@collection ||= ActiveSupport::Inflector.tableize(self).freeze
|
||||
end
|
||||
|
||||
def partial_path
|
||||
@partial_path ||= "#{collection}/#{element}".freeze
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -77,6 +77,7 @@ module ActiveRecord
|
||||
# those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
|
||||
class MacroReflection
|
||||
attr_reader :active_record
|
||||
attr_accessor :cached_extend_class
|
||||
|
||||
def initialize(macro, name, options, active_record)
|
||||
@macro, @name, @options, @active_record = macro, name, options, active_record
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require 'active_support/json'
|
||||
require 'active_support/core_ext/module/model_naming'
|
||||
|
||||
module ActiveRecord #:nodoc:
|
||||
module Serialization
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
$:.unshift(File.dirname(__FILE__) + '/../../lib')
|
||||
$:.unshift(File.dirname(__FILE__) + '/../../../activesupport/lib')
|
||||
begin
|
||||
old, $VERBOSE = $VERBOSE, nil
|
||||
require File.expand_path('../../../load_paths', __FILE__)
|
||||
ensure
|
||||
$VERBOSE = old
|
||||
end
|
||||
|
||||
require 'config'
|
||||
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
*2.3.11 (February 9, 2011)*
|
||||
*2.3.10 (October 15, 2010)*
|
||||
*2.3.9 (September 4, 2010)*
|
||||
*2.3.8 (May 24, 2010)*
|
||||
*2.3.7 (May 24, 2010)*
|
||||
|
||||
* Version bump.
|
||||
|
||||
|
||||
*2.3.6 (May 23, 2010)*
|
||||
|
||||
* No changes, just a version bump.
|
||||
|
||||
|
||||
*2.3.5 (November 25, 2009)*
|
||||
|
||||
* Minor Bug Fixes and deprecation warnings
|
||||
|
||||
* More flexible content type handling when parsing responses.
|
||||
|
||||
Ensures that ARes will handle responses like test/xml, or content types
|
||||
with charsets included.
|
||||
|
||||
*2.3.4 (September 4, 2009)*
|
||||
|
||||
* Add support for errors in JSON format. #1956 [Fabien Jakimowicz]
|
||||
|
||||
* Recognizes 410 as Resource Gone. #2316 [Jordan Brough, Jatinder Singh]
|
||||
|
||||
* More thorough SSL support. #2370 [Roy Nicholson]
|
||||
|
||||
* HTTP proxy support. #2133 [Marshall Huss, Sébastien Dabet]
|
||||
|
||||
|
||||
*2.3.3 (July 12, 2009)*
|
||||
|
||||
* No changes, just a version bump.
|
||||
|
||||
|
||||
*2.3.2 [Final] (March 15, 2009)*
|
||||
|
||||
* Nothing new, just included in 2.3.2
|
||||
|
||||
|
||||
*2.2.1 [RC2] (November 14th, 2008)*
|
||||
|
||||
* Fixed that ActiveResource#post would post an empty string when it shouldn't be posting anything #525 [Paolo Angelini]
|
||||
|
||||
|
||||
*2.2.0 [RC1] (October 24th, 2008)*
|
||||
|
||||
* Add ActiveResource::Base#to_xml and ActiveResource::Base#to_json. #1011 [Rasik Pandey, Cody Fauser]
|
||||
|
||||
* Add ActiveResource::Base.find(:last). [#754 state:resolved] (Adrian Mugnolo)
|
||||
|
||||
* Fixed problems with the logger used if the logging string included %'s [#840 state:resolved] (Jamis Buck)
|
||||
|
||||
* Fixed Base#exists? to check status code as integer [#299 state:resolved] (Wes Oldenbeuving)
|
||||
|
||||
|
||||
*2.1.0 (May 31st, 2008)*
|
||||
|
||||
* Fixed response logging to use length instead of the entire thing (seangeo) [#27]
|
||||
|
||||
* Fixed that to_param should be used and honored instead of hardcoding the id #11406 [gspiers]
|
||||
|
||||
* Improve documentation. [Ryan Bigg, Jan De Poorter, Cheah Chu Yeow, Xavier Shay, Jack Danger Canty, Emilio Tagua, Xavier Noria, Sunny Ripert]
|
||||
|
||||
* Use HEAD instead of GET in exists? [bscofield]
|
||||
|
||||
* Fix small documentation typo. Closes #10670 [Luca Guidi]
|
||||
|
||||
* find_or_create_resource_for handles module nesting. #10646 [xavier]
|
||||
|
||||
* Allow setting ActiveResource::Base#format before #site. [Rick Olson]
|
||||
|
||||
* Support agnostic formats when calling custom methods. Closes #10635 [joerichsen]
|
||||
|
||||
* Document custom methods. #10589 [Cheah Chu Yeow]
|
||||
|
||||
* Ruby 1.9 compatibility. [Jeremy Kemper]
|
||||
|
||||
|
||||
*2.0.2* (December 16th, 2007)
|
||||
|
||||
* Added more specific exceptions for 400, 401, and 403 (all descending from ClientError so existing rescues will work) #10326 [trek]
|
||||
|
||||
* Correct empty response handling. #10445 [seangeo]
|
||||
|
||||
|
||||
*2.0.1* (December 7th, 2007)
|
||||
|
||||
* Don't cache net/http object so that ActiveResource is more thread-safe. Closes #10142 [kou]
|
||||
|
||||
* Update XML documentation examples to include explicit type attributes. Closes #9754 [Josh Susser]
|
||||
|
||||
* Added one-off declarations of mock behavior [David Heinemeier Hansson]. Example:
|
||||
|
||||
Before:
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/1.xml", {}, "<person><name>David</name></person>"
|
||||
end
|
||||
|
||||
Now:
|
||||
ActiveResource::HttpMock.respond_to.get "/people/1.xml", {}, "<person><name>David</name></person>"
|
||||
|
||||
* Added ActiveResource.format= which defaults to :xml but can also be set to :json [David Heinemeier Hansson]. Example:
|
||||
|
||||
class Person < ActiveResource::Base
|
||||
self.site = "http://app/"
|
||||
self.format = :json
|
||||
end
|
||||
|
||||
person = Person.find(1) # => GET http://app/people/1.json
|
||||
person.name = "David"
|
||||
person.save # => PUT http://app/people/1.json {name: "David"}
|
||||
|
||||
Person.format = :xml
|
||||
person.name = "Mary"
|
||||
person.save # => PUT http://app/people/1.json <person><name>Mary</name></person>
|
||||
|
||||
* Fix reload error when path prefix is used. #8727 [Ian Warshak]
|
||||
|
||||
* Remove ActiveResource::Struct because it hasn't proven very useful. Creating a new ActiveResource::Base subclass is often less code and always clearer. #8612 [Josh Peek]
|
||||
|
||||
* Fix query methods on resources. [Cody Fauser]
|
||||
|
||||
* pass the prefix_options to the instantiated record when using find without a specific id. Closes #8544 [Eloy Duran]
|
||||
|
||||
* Recognize and raise an exception on 405 Method Not Allowed responses. #7692 [Josh Peek]
|
||||
|
||||
* Handle string and symbol param keys when splitting params into prefix params and query params.
|
||||
|
||||
Comment.find(:all, :params => { :article_id => 5, :page => 2 }) or Comment.find(:all, :params => { 'article_id' => 5, :page => 2 })
|
||||
|
||||
* Added find-one with symbol [David Heinemeier Hansson]. Example: Person.find(:one, :from => :leader) # => GET /people/leader.xml
|
||||
|
||||
* BACKWARDS INCOMPATIBLE: Changed the finder API to be more extensible with :params and more strict usage of scopes [David Heinemeier Hansson]. Changes:
|
||||
|
||||
Person.find(:all, :title => "CEO") ...becomes: Person.find(:all, :params => { :title => "CEO" })
|
||||
Person.find(:managers) ...becomes: Person.find(:all, :from => :managers)
|
||||
Person.find("/companies/1/manager.xml") ...becomes: Person.find(:one, :from => "/companies/1/manager.xml")
|
||||
|
||||
* Add support for setting custom headers per Active Resource model [Rick Olson]
|
||||
|
||||
class Project
|
||||
headers['X-Token'] = 'foo'
|
||||
end
|
||||
|
||||
# makes the GET request with the custom X-Token header
|
||||
Project.find(:all)
|
||||
|
||||
* Added find-by-path options to ActiveResource::Base.find [David Heinemeier Hansson]. Examples:
|
||||
|
||||
employees = Person.find(:all, :from => "/companies/1/people.xml") # => GET /companies/1/people.xml
|
||||
manager = Person.find("/companies/1/manager.xml") # => GET /companies/1/manager.xml
|
||||
|
||||
|
||||
* Added support for using classes from within a single nested module [David Heinemeier Hansson]. Example:
|
||||
|
||||
module Highrise
|
||||
class Note < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
|
||||
class Comment < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
end
|
||||
|
||||
assert_kind_of Highrise::Comment, Note.find(1).comments.first
|
||||
|
||||
|
||||
* Added load_attributes_from_response as a way of loading attributes from other responses than just create [David Heinemeier Hansson]
|
||||
|
||||
class Highrise::Task < ActiveResource::Base
|
||||
def complete
|
||||
load_attributes_from_response(post(:complete))
|
||||
end
|
||||
end
|
||||
|
||||
...will set "done_at" when complete is called.
|
||||
|
||||
|
||||
* Added support for calling custom methods #6979 [rwdaigle]
|
||||
|
||||
Person.find(:managers) # => GET /people/managers.xml
|
||||
Kase.find(1).post(:close) # => POST /kases/1/close.xml
|
||||
|
||||
* Remove explicit prefix_options parameter for ActiveResource::Base#initialize. [Rick Olson]
|
||||
ActiveResource splits the prefix_options from it automatically.
|
||||
|
||||
* Allow ActiveResource::Base.delete with custom prefix. [Rick Olson]
|
||||
|
||||
* Add ActiveResource::Base#dup [Rick Olson]
|
||||
|
||||
* Fixed constant warning when fetching the same object multiple times [David Heinemeier Hansson]
|
||||
|
||||
* Added that saves which get a body response (and not just a 201) will use that response to update themselves [David Heinemeier Hansson]
|
||||
|
||||
* Disregard namespaces from the default element name, so Highrise::Person will just try to fetch from "/people", not "/highrise/people" [David Heinemeier Hansson]
|
||||
|
||||
* Allow array and hash query parameters. #7756 [Greg Spurrier]
|
||||
|
||||
* Loading a resource preserves its prefix_options. #7353 [Ryan Daigle]
|
||||
|
||||
* Carry over the convenience of #create from ActiveRecord. Closes #7340. [Ryan Daigle]
|
||||
|
||||
* Increase ActiveResource::Base test coverage. Closes #7173, #7174 [Rich Collins]
|
||||
|
||||
* Interpret 422 Unprocessable Entity as ResourceInvalid. #7097 [dkubb]
|
||||
|
||||
* Mega documentation patches. #7025, #7069 [rwdaigle]
|
||||
|
||||
* Base.exists?(id, options) and Base#exists? check whether the resource is found. #6970 [rwdaigle]
|
||||
|
||||
* Query string support. [untext, Jeremy Kemper]
|
||||
# GET /forums/1/topics.xml?sort=created_at
|
||||
Topic.find(:all, :forum_id => 1, :sort => 'created_at')
|
||||
|
||||
* Base#==, eql?, and hash methods. == returns true if its argument is identical to self or if it's an instance of the same class, is not new?, and has the same id. eql? is an alias for ==. hash delegates to id. [Jeremy Kemper]
|
||||
|
||||
* Allow subclassed resources to share the site info [Rick Olson, Jeremy Kemper]
|
||||
d
|
||||
class BeastResource < ActiveResource::Base
|
||||
self.site = 'http://beast.caboo.se'
|
||||
end
|
||||
|
||||
class Forum < BeastResource
|
||||
# taken from BeastResource
|
||||
# self.site = 'http://beast.caboo.se'
|
||||
end
|
||||
|
||||
class Topic < BeastResource
|
||||
self.site += '/forums/:forum_id'
|
||||
end
|
||||
|
||||
* Fix issues with ActiveResource collection handling. Closes #6291. [bmilekic]
|
||||
|
||||
* Use attr_accessor_with_default to dry up attribute initialization. References #6538. [Stuart Halloway]
|
||||
|
||||
* Add basic logging support for logging outgoing requests. [Jamis Buck]
|
||||
|
||||
* Add Base.delete for deleting resources without having to instantiate them first. [Jamis Buck]
|
||||
|
||||
* Make #save behavior mimic AR::Base#save (true on success, false on failure). [Jamis Buck]
|
||||
|
||||
* Add Basic HTTP Authentication to ActiveResource (closes #6305). [jonathan]
|
||||
|
||||
* Extracted #id_from_response as an entry point for customizing how a created resource gets its own ID.
|
||||
By default, it extracts from the Location response header.
|
||||
|
||||
* Optimistic locking: raise ActiveResource::ResourceConflict on 409 Conflict response. [Jeremy Kemper]
|
||||
|
||||
# Example controller action
|
||||
def update
|
||||
@person.save!
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
render :xml => @person.reload.to_xml, :status => '409 Conflict'
|
||||
end
|
||||
|
||||
* Basic validation support [Rick Olson]
|
||||
|
||||
Parses the xml response of ActiveRecord::Errors#to_xml with a similar interface to ActiveRecord::Errors.
|
||||
|
||||
render :xml => @person.errors.to_xml, :status => '400 Validation Error'
|
||||
|
||||
* Deep hashes are converted into collections of resources. [Jeremy Kemper]
|
||||
Person.new :name => 'Bob',
|
||||
:address => { :id => 1, :city => 'Portland' },
|
||||
:contacts => [{ :id => 1 }, { :id => 2 }]
|
||||
Looks for Address and Contact resources and creates them if unavailable.
|
||||
So clients can fetch a complex resource in a single request if you e.g.
|
||||
render :xml => @person.to_xml(:include => [:address, :contacts])
|
||||
in your controller action.
|
||||
|
||||
* Major updates [Rick Olson]
|
||||
|
||||
* Add full support for find/create/update/destroy
|
||||
* Add support for specifying prefixes.
|
||||
* Allow overriding of element_name, collection_name, and primary key
|
||||
* Provide simpler HTTP mock interface for testing
|
||||
|
||||
# rails routing code
|
||||
map.resources :posts do |post|
|
||||
post.resources :comments
|
||||
end
|
||||
|
||||
# ActiveResources
|
||||
class Post < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000/"
|
||||
end
|
||||
|
||||
class Comment < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000/posts/:post_id/"
|
||||
end
|
||||
|
||||
@post = Post.find 5
|
||||
@comments = Comment.find :all, :post_id => @post.id
|
||||
|
||||
@comment = Comment.new({:body => 'hello world'}, {:post_id => @post.id})
|
||||
@comment.save
|
||||
|
||||
* Base.site= accepts URIs. 200...400 are valid response codes. PUT and POST request bodies default to ''. [Jeremy Kemper]
|
||||
|
||||
* Initial checkin: object-oriented client for restful HTTP resources which follow the Rails convention. [David Heinemeier Hansson]
|
||||
@@ -1,20 +0,0 @@
|
||||
Copyright (c) 2006-2010 David Heinemeier Hansson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,165 +0,0 @@
|
||||
= Active Resource
|
||||
|
||||
Active Resource (ARes) connects business objects and Representational State Transfer (REST)
|
||||
web services. It implements object-relational mapping for REST webservices to provide transparent
|
||||
proxying capabilities between a client (ActiveResource) and a RESTful service (which is provided by Simply RESTful routing
|
||||
in ActionController::Resources).
|
||||
|
||||
== Philosophy
|
||||
|
||||
Active Resource attempts to provide a coherent wrapper object-relational mapping for REST
|
||||
web services. It follows the same philosophy as Active Record, in that one of its prime aims
|
||||
is to reduce the amount of code needed to map to these resources. This is made possible
|
||||
by relying on a number of code- and protocol-based conventions that make it easy for Active Resource
|
||||
to infer complex relations and structures. These conventions are outlined in detail in the documentation
|
||||
for ActiveResource::Base.
|
||||
|
||||
== Overview
|
||||
|
||||
Model classes are mapped to remote REST resources by Active Resource much the same way Active Record maps model classes to database
|
||||
tables. When a request is made to a remote resource, a REST XML request is generated, transmitted, and the result
|
||||
received and serialized into a usable Ruby object.
|
||||
|
||||
=== Configuration and Usage
|
||||
|
||||
Putting ActiveResource to use is very similar to ActiveRecord. It's as simple as creating a model class
|
||||
that inherits from ActiveResource::Base and providing a <tt>site</tt> class variable to it:
|
||||
|
||||
class Person < ActiveResource::Base
|
||||
self.site = "http://api.people.com:3000/"
|
||||
end
|
||||
|
||||
Now the Person class is REST enabled and can invoke REST services very similarly to how ActiveRecord invokes
|
||||
lifecycle methods that operate against a persistent store.
|
||||
|
||||
# Find a person with id = 1
|
||||
ryan = Person.find(1)
|
||||
Person.exists?(1) #=> true
|
||||
|
||||
As you can see, the methods are quite similar to Active Record's methods for dealing with database
|
||||
records. But rather than dealing directly with a database record, you're dealing with HTTP resources (which may or may not be database records).
|
||||
|
||||
==== Protocol
|
||||
|
||||
Active Resource is built on a standard XML format for requesting and submitting resources over HTTP. It mirrors the RESTful routing
|
||||
built into ActionController but will also work with any other REST service that properly implements the protocol.
|
||||
REST uses HTTP, but unlike "typical" web applications, it makes use of all the verbs available in the HTTP specification:
|
||||
|
||||
* GET requests are used for finding and retrieving resources.
|
||||
* POST requests are used to create new resources.
|
||||
* PUT requests are used to update existing resources.
|
||||
* DELETE requests are used to delete resources.
|
||||
|
||||
For more information on how this protocol works with Active Resource, see the ActiveResource::Base documentation;
|
||||
for more general information on REST web services, see the article here[http://en.wikipedia.org/wiki/Representational_State_Transfer].
|
||||
|
||||
==== Find
|
||||
|
||||
GET Http requests expect the XML form of whatever resource/resources is/are being requested. So,
|
||||
for a request for a single element - the XML of that item is expected in response:
|
||||
|
||||
# Expects a response of
|
||||
#
|
||||
# <person><id type="integer">1</id><attribute1>value1</attribute1><attribute2>..</attribute2></person>
|
||||
#
|
||||
# for GET http://api.people.com:3000/people/1.xml
|
||||
#
|
||||
ryan = Person.find(1)
|
||||
|
||||
The XML document that is received is used to build a new object of type Person, with each
|
||||
XML element becoming an attribute on the object.
|
||||
|
||||
ryan.is_a? Person #=> true
|
||||
ryan.attribute1 #=> 'value1'
|
||||
|
||||
Any complex element (one that contains other elements) becomes its own object:
|
||||
|
||||
# With this response:
|
||||
#
|
||||
# <person><id>1</id><attribute1>value1</attribute1><complex><attribute2>value2</attribute2></complex></person>
|
||||
#
|
||||
# for GET http://api.people.com:3000/people/1.xml
|
||||
#
|
||||
ryan = Person.find(1)
|
||||
ryan.complex #=> <Person::Complex::xxxxx>
|
||||
ryan.complex.attribute2 #=> 'value2'
|
||||
|
||||
Collections can also be requested in a similar fashion
|
||||
|
||||
# Expects a response of
|
||||
#
|
||||
# <people type="array">
|
||||
# <person><id type="integer">1</id><first>Ryan</first></person>
|
||||
# <person><id type="integer">2</id><first>Jim</first></person>
|
||||
# </people>
|
||||
#
|
||||
# for GET http://api.people.com:3000/people.xml
|
||||
#
|
||||
people = Person.find(:all)
|
||||
people.first #=> <Person::xxx 'first' => 'Ryan' ...>
|
||||
people.last #=> <Person::xxx 'first' => 'Jim' ...>
|
||||
|
||||
==== Create
|
||||
|
||||
Creating a new resource submits the xml form of the resource as the body of the request and expects
|
||||
a 'Location' header in the response with the RESTful URL location of the newly created resource. The
|
||||
id of the newly created resource is parsed out of the Location response header and automatically set
|
||||
as the id of the ARes object.
|
||||
|
||||
# <person><first>Ryan</first></person>
|
||||
#
|
||||
# is submitted as the body on
|
||||
#
|
||||
# POST http://api.people.com:3000/people.xml
|
||||
#
|
||||
# when save is called on a new Person object. An empty response is
|
||||
# is expected with a 'Location' header value:
|
||||
#
|
||||
# Response (201): Location: http://api.people.com:3000/people/2
|
||||
#
|
||||
ryan = Person.new(:first => 'Ryan')
|
||||
ryan.new? #=> true
|
||||
ryan.save #=> true
|
||||
ryan.new? #=> false
|
||||
ryan.id #=> 2
|
||||
|
||||
==== Update
|
||||
|
||||
'save' is also used to update an existing resource - and follows the same protocol as creating a resource
|
||||
with the exception that no response headers are needed - just an empty response when the update on the
|
||||
server side was successful.
|
||||
|
||||
# <person><first>Ryan</first></person>
|
||||
#
|
||||
# is submitted as the body on
|
||||
#
|
||||
# PUT http://api.people.com:3000/people/1.xml
|
||||
#
|
||||
# when save is called on an existing Person object. An empty response is
|
||||
# is expected with code (204)
|
||||
#
|
||||
ryan = Person.find(1)
|
||||
ryan.first #=> 'Ryan'
|
||||
ryan.first = 'Rizzle'
|
||||
ryan.save #=> true
|
||||
|
||||
==== Delete
|
||||
|
||||
Destruction of a resource can be invoked as a class and instance method of the resource.
|
||||
|
||||
# A request is made to
|
||||
#
|
||||
# DELETE http://api.people.com:3000/people/1.xml
|
||||
#
|
||||
# for both of these forms. An empty response with
|
||||
# is expected with response code (200)
|
||||
#
|
||||
ryan = Person.find(1)
|
||||
ryan.destroy #=> true
|
||||
ryan.exists? #=> false
|
||||
Person.delete(2) #=> true
|
||||
Person.exists?(2) #=> false
|
||||
|
||||
|
||||
You can find more usage information in the ActiveResource::Base documentation.
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
require 'rubygems'
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rdoc/task'
|
||||
require 'rake/packagetask'
|
||||
require 'rubygems/package_task'
|
||||
|
||||
require File.join(File.dirname(__FILE__), 'lib', 'active_resource', 'version')
|
||||
|
||||
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
||||
PKG_NAME = 'activeresource'
|
||||
PKG_VERSION = ActiveResource::VERSION::STRING + PKG_BUILD
|
||||
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
||||
|
||||
RELEASE_NAME = "REL #{PKG_VERSION}"
|
||||
|
||||
RUBY_FORGE_PROJECT = "activerecord"
|
||||
RUBY_FORGE_USER = "webster132"
|
||||
|
||||
PKG_FILES = FileList[
|
||||
"lib/**/*", "test/**/*", "[A-Z]*", "Rakefile"
|
||||
].exclude(/\bCVS\b|~$/)
|
||||
|
||||
desc "Default Task"
|
||||
task :default => [ :test ]
|
||||
|
||||
# Run the unit tests
|
||||
|
||||
Rake::TestTask.new { |t|
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../activesupport/lib"
|
||||
t.libs << activesupport_path if File.directory?(activesupport_path)
|
||||
t.libs << "test"
|
||||
t.pattern = 'test/**/*_test.rb'
|
||||
}
|
||||
|
||||
|
||||
# Generate the RDoc documentation
|
||||
|
||||
RDoc::Task.new { |rdoc|
|
||||
rdoc.rdoc_dir = 'doc'
|
||||
rdoc.title = "Active Resource -- Object-oriented REST services"
|
||||
rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
|
||||
rdoc.options << '--charset' << 'utf-8'
|
||||
rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo'
|
||||
rdoc.rdoc_files.include('README', 'CHANGELOG')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
rdoc.rdoc_files.exclude('lib/activeresource.rb')
|
||||
}
|
||||
|
||||
|
||||
# Create compressed packages
|
||||
|
||||
dist_dirs = [ "lib", "test", "examples", "dev-utils" ]
|
||||
|
||||
spec = Gem::Specification.new do |s|
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.name = PKG_NAME
|
||||
s.version = PKG_VERSION
|
||||
s.summary = "Think Active Record for web resources."
|
||||
s.description = %q{Wraps web resources in model classes that can be manipulated through XML over REST.}
|
||||
|
||||
s.files = [ "Rakefile", "README", "CHANGELOG" ]
|
||||
dist_dirs.each do |dir|
|
||||
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
end
|
||||
|
||||
s.add_dependency('activesupport', '= 2.3.14' + PKG_BUILD)
|
||||
|
||||
s.require_path = 'lib'
|
||||
|
||||
s.extra_rdoc_files = %w( README )
|
||||
s.rdoc_options.concat ['--main', 'README']
|
||||
|
||||
s.author = "David Heinemeier Hansson"
|
||||
s.email = "david@loudthinking.com"
|
||||
s.homepage = "http://www.rubyonrails.org"
|
||||
s.rubyforge_project = "activeresource"
|
||||
end
|
||||
|
||||
Gem::PackageTask.new(spec) do |p|
|
||||
p.gem_spec = spec
|
||||
p.need_tar = true
|
||||
p.need_zip = true
|
||||
end
|
||||
|
||||
task :lines do
|
||||
lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
|
||||
|
||||
for file_name in FileList["lib/active_resource/**/*.rb"]
|
||||
next if file_name =~ /vendor/
|
||||
f = File.open(file_name)
|
||||
|
||||
while line = f.gets
|
||||
lines += 1
|
||||
next if line =~ /^\s*$/
|
||||
next if line =~ /^\s*#/
|
||||
codelines += 1
|
||||
end
|
||||
puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
|
||||
|
||||
total_lines += lines
|
||||
total_codelines += codelines
|
||||
|
||||
lines, codelines = 0, 0
|
||||
end
|
||||
|
||||
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
|
||||
end
|
||||
|
||||
|
||||
# Publishing ------------------------------------------------------
|
||||
|
||||
desc "Publish the beta gem"
|
||||
task :pgem => [:package] do
|
||||
require 'rake/contrib/sshpublisher'
|
||||
Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
|
||||
`ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
|
||||
end
|
||||
|
||||
desc "Publish the API documentation"
|
||||
task :pdoc => [:rdoc] do
|
||||
require 'rake/contrib/sshpublisher'
|
||||
Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/ar", "doc").upload
|
||||
end
|
||||
|
||||
desc "Publish the release files to RubyForge."
|
||||
task :release => [ :package ] do
|
||||
`rubyforge login`
|
||||
|
||||
for ext in %w( gem tgz zip )
|
||||
release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}"
|
||||
puts release_command
|
||||
system(release_command)
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'activeresource'
|
||||
s.version = '2.3.18'
|
||||
s.summary = 'Think Active Record for web resources.'
|
||||
s.description = 'Wraps web resources in model classes that can be manipulated through XML over REST.'
|
||||
|
||||
s.author = 'David Heinemeier Hansson'
|
||||
s.email = 'david@loudthinking.com'
|
||||
s.homepage = 'http://www.rubyonrails.org'
|
||||
|
||||
s.require_path = 'lib'
|
||||
s.files = ['README']
|
||||
s.rdoc_options = ['--main', 'README']
|
||||
s.extra_rdoc_files = ['README']
|
||||
|
||||
s.add_dependency 'activesupport', '= 2.3.18'
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
#--
|
||||
# Copyright (c) 2006 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'active_support'
|
||||
rescue LoadError
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
if File.directory?(activesupport_path)
|
||||
$:.unshift activesupport_path
|
||||
require 'active_support'
|
||||
end
|
||||
end
|
||||
|
||||
require 'active_resource/formats'
|
||||
require 'active_resource/base'
|
||||
require 'active_resource/validations'
|
||||
require 'active_resource/custom_methods'
|
||||
|
||||
module ActiveResource
|
||||
Base.class_eval do
|
||||
include Validations
|
||||
include CustomMethods
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,283 +0,0 @@
|
||||
require 'net/https'
|
||||
require 'date'
|
||||
require 'time'
|
||||
require 'uri'
|
||||
require 'benchmark'
|
||||
|
||||
module ActiveResource
|
||||
class ConnectionError < StandardError # :nodoc:
|
||||
attr_reader :response
|
||||
|
||||
def initialize(response, message = nil)
|
||||
@response = response
|
||||
@message = message
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Raised when a Timeout::Error occurs.
|
||||
class TimeoutError < ConnectionError
|
||||
def initialize(message)
|
||||
@message = message
|
||||
end
|
||||
def to_s; @message ;end
|
||||
end
|
||||
|
||||
# Raised when a OpenSSL::SSL::SSLError occurs.
|
||||
class SSLError < ConnectionError
|
||||
def initialize(message)
|
||||
@message = message
|
||||
end
|
||||
def to_s; @message ;end
|
||||
end
|
||||
|
||||
# 3xx Redirection
|
||||
class Redirection < ConnectionError # :nodoc:
|
||||
def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
|
||||
end
|
||||
|
||||
# 4xx Client Error
|
||||
class ClientError < ConnectionError; end # :nodoc:
|
||||
|
||||
# 400 Bad Request
|
||||
class BadRequest < ClientError; end # :nodoc
|
||||
|
||||
# 401 Unauthorized
|
||||
class UnauthorizedAccess < ClientError; end # :nodoc
|
||||
|
||||
# 403 Forbidden
|
||||
class ForbiddenAccess < ClientError; end # :nodoc
|
||||
|
||||
# 404 Not Found
|
||||
class ResourceNotFound < ClientError; end # :nodoc:
|
||||
|
||||
# 409 Conflict
|
||||
class ResourceConflict < ClientError; end # :nodoc:
|
||||
|
||||
# 410 Gone
|
||||
class ResourceGone < ClientError; end # :nodoc:
|
||||
|
||||
# 5xx Server Error
|
||||
class ServerError < ConnectionError; end # :nodoc:
|
||||
|
||||
# 405 Method Not Allowed
|
||||
class MethodNotAllowed < ClientError # :nodoc:
|
||||
def allowed_methods
|
||||
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
|
||||
end
|
||||
end
|
||||
|
||||
# Class to handle connections to remote web services.
|
||||
# This class is used by ActiveResource::Base to interface with REST
|
||||
# services.
|
||||
class Connection
|
||||
|
||||
HTTP_FORMAT_HEADER_NAMES = { :get => 'Accept',
|
||||
:put => 'Content-Type',
|
||||
:post => 'Content-Type',
|
||||
:delete => 'Accept',
|
||||
:head => 'Accept'
|
||||
}
|
||||
|
||||
attr_reader :site, :user, :password, :timeout, :proxy, :ssl_options
|
||||
attr_accessor :format
|
||||
|
||||
class << self
|
||||
def requests
|
||||
@@requests ||= []
|
||||
end
|
||||
end
|
||||
|
||||
# The +site+ parameter is required and will set the +site+
|
||||
# attribute to the URI for the remote resource service.
|
||||
def initialize(site, format = ActiveResource::Formats[:xml])
|
||||
raise ArgumentError, 'Missing site URI' unless site
|
||||
@user = @password = nil
|
||||
self.site = site
|
||||
self.format = format
|
||||
end
|
||||
|
||||
# Set URI for remote service.
|
||||
def site=(site)
|
||||
@site = site.is_a?(URI) ? site : URI.parse(site)
|
||||
@user = URI::DEFAULT_PARSER.unescape(@site.user) if @site.user
|
||||
@password = URI::DEFAULT_PARSER.unescape(@site.password) if @site.password
|
||||
end
|
||||
|
||||
# Set the proxy for remote service.
|
||||
def proxy=(proxy)
|
||||
@proxy = proxy.is_a?(URI) ? proxy : URI.parse(proxy)
|
||||
end
|
||||
|
||||
# Set the user for remote service.
|
||||
def user=(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
# Set password for remote service.
|
||||
def password=(password)
|
||||
@password = password
|
||||
end
|
||||
|
||||
# Set the number of seconds after which HTTP requests to the remote service should time out.
|
||||
def timeout=(timeout)
|
||||
@timeout = timeout
|
||||
end
|
||||
|
||||
# Hash of options applied to Net::HTTP instance when +site+ protocol is 'https'.
|
||||
def ssl_options=(opts={})
|
||||
@ssl_options = opts
|
||||
end
|
||||
|
||||
# Execute a GET request.
|
||||
# Used to get (find) resources.
|
||||
def get(path, headers = {})
|
||||
format.decode(request(:get, path, build_request_headers(headers, :get)).body)
|
||||
end
|
||||
|
||||
# Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
|
||||
# Used to delete resources.
|
||||
def delete(path, headers = {})
|
||||
request(:delete, path, build_request_headers(headers, :delete))
|
||||
end
|
||||
|
||||
# Execute a PUT request (see HTTP protocol documentation if unfamiliar).
|
||||
# Used to update resources.
|
||||
def put(path, body = '', headers = {})
|
||||
request(:put, path, body.to_s, build_request_headers(headers, :put))
|
||||
end
|
||||
|
||||
# Execute a POST request.
|
||||
# Used to create new resources.
|
||||
def post(path, body = '', headers = {})
|
||||
request(:post, path, body.to_s, build_request_headers(headers, :post))
|
||||
end
|
||||
|
||||
# Execute a HEAD request.
|
||||
# Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
|
||||
def head(path, headers = {})
|
||||
request(:head, path, build_request_headers(headers, :head))
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
# Makes request to remote service.
|
||||
def request(method, path, *arguments)
|
||||
logger.info "#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}" if logger
|
||||
result = nil
|
||||
ms = Benchmark.ms { result = http.send(method, path, *arguments) }
|
||||
logger.info "--> %d %s (%d %.0fms)" % [result.code, result.message, result.body ? result.body.length : 0, ms] if logger
|
||||
handle_response(result)
|
||||
rescue Timeout::Error => e
|
||||
raise TimeoutError.new(e.message)
|
||||
rescue OpenSSL::SSL::SSLError => e
|
||||
raise SSLError.new(e.message)
|
||||
end
|
||||
|
||||
# Handles response and error codes from remote service.
|
||||
def handle_response(response)
|
||||
case response.code.to_i
|
||||
when 301,302
|
||||
raise(Redirection.new(response))
|
||||
when 200...400
|
||||
response
|
||||
when 400
|
||||
raise(BadRequest.new(response))
|
||||
when 401
|
||||
raise(UnauthorizedAccess.new(response))
|
||||
when 403
|
||||
raise(ForbiddenAccess.new(response))
|
||||
when 404
|
||||
raise(ResourceNotFound.new(response))
|
||||
when 405
|
||||
raise(MethodNotAllowed.new(response))
|
||||
when 409
|
||||
raise(ResourceConflict.new(response))
|
||||
when 410
|
||||
raise(ResourceGone.new(response))
|
||||
when 422
|
||||
raise(ResourceInvalid.new(response))
|
||||
when 401...500
|
||||
raise(ClientError.new(response))
|
||||
when 500...600
|
||||
raise(ServerError.new(response))
|
||||
else
|
||||
raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
|
||||
end
|
||||
end
|
||||
|
||||
# Creates new Net::HTTP instance for communication with
|
||||
# remote service and resources.
|
||||
def http
|
||||
configure_http(new_http)
|
||||
end
|
||||
|
||||
def new_http
|
||||
if @proxy
|
||||
Net::HTTP.new(@site.host, @site.port, @proxy.host, @proxy.port, @proxy.user, @proxy.password)
|
||||
else
|
||||
Net::HTTP.new(@site.host, @site.port)
|
||||
end
|
||||
end
|
||||
|
||||
def configure_http(http)
|
||||
http = apply_ssl_options(http)
|
||||
|
||||
# Net::HTTP timeouts default to 60 seconds.
|
||||
if @timeout
|
||||
http.open_timeout = @timeout
|
||||
http.read_timeout = @timeout
|
||||
end
|
||||
|
||||
http
|
||||
end
|
||||
|
||||
def apply_ssl_options(http)
|
||||
return http unless @site.is_a?(URI::HTTPS)
|
||||
|
||||
http.use_ssl = true
|
||||
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||
return http unless defined?(@ssl_options)
|
||||
|
||||
http.ca_path = @ssl_options[:ca_path] if @ssl_options[:ca_path]
|
||||
http.ca_file = @ssl_options[:ca_file] if @ssl_options[:ca_file]
|
||||
|
||||
http.cert = @ssl_options[:cert] if @ssl_options[:cert]
|
||||
http.key = @ssl_options[:key] if @ssl_options[:key]
|
||||
|
||||
http.cert_store = @ssl_options[:cert_store] if @ssl_options[:cert_store]
|
||||
http.ssl_timeout = @ssl_options[:ssl_timeout] if @ssl_options[:ssl_timeout]
|
||||
|
||||
http.verify_mode = @ssl_options[:verify_mode] if @ssl_options[:verify_mode]
|
||||
http.verify_callback = @ssl_options[:verify_callback] if @ssl_options[:verify_callback]
|
||||
http.verify_depth = @ssl_options[:verify_depth] if @ssl_options[:verify_depth]
|
||||
|
||||
http
|
||||
end
|
||||
|
||||
def default_header
|
||||
@default_header ||= {}
|
||||
end
|
||||
|
||||
# Builds headers for request to remote service.
|
||||
def build_request_headers(headers, http_method=nil)
|
||||
authorization_header.update(default_header).update(http_format_header(http_method)).update(headers)
|
||||
end
|
||||
|
||||
# Sets authorization header
|
||||
def authorization_header
|
||||
(@user || @password ? { 'Authorization' => 'Basic ' + ["#{@user}:#{ @password}"].pack('m').delete("\r\n") } : {})
|
||||
end
|
||||
|
||||
def http_format_header(http_method)
|
||||
{HTTP_FORMAT_HEADER_NAMES[http_method] => format.mime_type}
|
||||
end
|
||||
|
||||
def logger #:nodoc:
|
||||
Base.logger
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,120 +0,0 @@
|
||||
module ActiveResource
|
||||
# A module to support custom REST methods and sub-resources, allowing you to break out
|
||||
# of the "default" REST methods with your own custom resource requests. For example,
|
||||
# say you use Rails to expose a REST service and configure your routes with:
|
||||
#
|
||||
# map.resources :people, :new => { :register => :post },
|
||||
# :member => { :promote => :put, :deactivate => :delete }
|
||||
# :collection => { :active => :get }
|
||||
#
|
||||
# This route set creates routes for the following HTTP requests:
|
||||
#
|
||||
# POST /people/new/register.xml # PeopleController.register
|
||||
# PUT /people/1/promote.xml # PeopleController.promote with :id => 1
|
||||
# DELETE /people/1/deactivate.xml # PeopleController.deactivate with :id => 1
|
||||
# GET /people/active.xml # PeopleController.active
|
||||
#
|
||||
# Using this module, Active Resource can use these custom REST methods just like the
|
||||
# standard methods.
|
||||
#
|
||||
# class Person < ActiveResource::Base
|
||||
# self.site = "http://37s.sunrise.i:3000"
|
||||
# end
|
||||
#
|
||||
# Person.new(:name => 'Ryan).post(:register) # POST /people/new/register.xml
|
||||
# # => { :id => 1, :name => 'Ryan' }
|
||||
#
|
||||
# Person.find(1).put(:promote, :position => 'Manager') # PUT /people/1/promote.xml
|
||||
# Person.find(1).delete(:deactivate) # DELETE /people/1/deactivate.xml
|
||||
#
|
||||
# Person.get(:active) # GET /people/active.xml
|
||||
# # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
|
||||
#
|
||||
module CustomMethods
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
extend ActiveResource::CustomMethods::ClassMethods
|
||||
include ActiveResource::CustomMethods::InstanceMethods
|
||||
|
||||
class << self
|
||||
alias :orig_delete :delete
|
||||
|
||||
# Invokes a GET to a given custom REST method. For example:
|
||||
#
|
||||
# Person.get(:active) # GET /people/active.xml
|
||||
# # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
|
||||
#
|
||||
# Person.get(:active, :awesome => true) # GET /people/active.xml?awesome=true
|
||||
# # => [{:id => 1, :name => 'Ryan'}]
|
||||
#
|
||||
# Note: the objects returned from this method are not automatically converted
|
||||
# into ActiveResource::Base instances - they are ordinary Hashes. If you are expecting
|
||||
# ActiveResource::Base instances, use the <tt>find</tt> class method with the
|
||||
# <tt>:from</tt> option. For example:
|
||||
#
|
||||
# Person.find(:all, :from => :active)
|
||||
def get(custom_method_name, options = {})
|
||||
connection.get(custom_method_collection_url(custom_method_name, options), headers)
|
||||
end
|
||||
|
||||
def post(custom_method_name, options = {}, body = '')
|
||||
connection.post(custom_method_collection_url(custom_method_name, options), body, headers)
|
||||
end
|
||||
|
||||
def put(custom_method_name, options = {}, body = '')
|
||||
connection.put(custom_method_collection_url(custom_method_name, options), body, headers)
|
||||
end
|
||||
|
||||
def delete(custom_method_name, options = {})
|
||||
# Need to jump through some hoops to retain the original class 'delete' method
|
||||
if custom_method_name.is_a?(Symbol)
|
||||
connection.delete(custom_method_collection_url(custom_method_name, options), headers)
|
||||
else
|
||||
orig_delete(custom_method_name, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def custom_method_collection_url(method_name, options = {})
|
||||
prefix_options, query_options = split_options(options)
|
||||
"#{prefix(prefix_options)}#{collection_name}/#{method_name}.#{format.extension}#{query_string(query_options)}"
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def get(method_name, options = {})
|
||||
connection.get(custom_method_element_url(method_name, options), self.class.headers)
|
||||
end
|
||||
|
||||
def post(method_name, options = {}, body = nil)
|
||||
request_body = body.blank? ? encode : body
|
||||
if new?
|
||||
connection.post(custom_method_new_element_url(method_name, options), request_body, self.class.headers)
|
||||
else
|
||||
connection.post(custom_method_element_url(method_name, options), request_body, self.class.headers)
|
||||
end
|
||||
end
|
||||
|
||||
def put(method_name, options = {}, body = '')
|
||||
connection.put(custom_method_element_url(method_name, options), body, self.class.headers)
|
||||
end
|
||||
|
||||
def delete(method_name, options = {})
|
||||
connection.delete(custom_method_element_url(method_name, options), self.class.headers)
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def custom_method_element_url(method_name, options = {})
|
||||
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{id}/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}"
|
||||
end
|
||||
|
||||
def custom_method_new_element_url(method_name, options = {})
|
||||
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/new/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,66 +0,0 @@
|
||||
module ActiveResource
|
||||
class ConnectionError < StandardError # :nodoc:
|
||||
attr_reader :response
|
||||
|
||||
def initialize(response, message = nil)
|
||||
@response = response
|
||||
@message = message
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Raised when a Timeout::Error occurs.
|
||||
class TimeoutError < ConnectionError
|
||||
def initialize(message)
|
||||
@message = message
|
||||
end
|
||||
def to_s; @message ;end
|
||||
end
|
||||
|
||||
# Raised when a OpenSSL::SSL::SSLError occurs.
|
||||
class SSLError < ConnectionError
|
||||
def initialize(message)
|
||||
@message = message
|
||||
end
|
||||
def to_s; @message ;end
|
||||
end
|
||||
|
||||
# 3xx Redirection
|
||||
class Redirection < ConnectionError # :nodoc:
|
||||
def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
|
||||
end
|
||||
|
||||
# 4xx Client Error
|
||||
class ClientError < ConnectionError; end # :nodoc:
|
||||
|
||||
# 400 Bad Request
|
||||
class BadRequest < ClientError; end # :nodoc
|
||||
|
||||
# 401 Unauthorized
|
||||
class UnauthorizedAccess < ClientError; end # :nodoc
|
||||
|
||||
# 403 Forbidden
|
||||
class ForbiddenAccess < ClientError; end # :nodoc
|
||||
|
||||
# 404 Not Found
|
||||
class ResourceNotFound < ClientError; end # :nodoc:
|
||||
|
||||
# 409 Conflict
|
||||
class ResourceConflict < ClientError; end # :nodoc:
|
||||
|
||||
# 410 Gone
|
||||
class ResourceGone < ClientError; end # :nodoc:
|
||||
|
||||
# 5xx Server Error
|
||||
class ServerError < ConnectionError; end # :nodoc:
|
||||
|
||||
# 405 Method Not Allowed
|
||||
class MethodNotAllowed < ClientError # :nodoc:
|
||||
def allowed_methods
|
||||
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
module ActiveResource
|
||||
module Formats
|
||||
# Lookup the format class from a mime type reference symbol. Example:
|
||||
#
|
||||
# ActiveResource::Formats[:xml] # => ActiveResource::Formats::XmlFormat
|
||||
# ActiveResource::Formats[:json] # => ActiveResource::Formats::JsonFormat
|
||||
def self.[](mime_type_reference)
|
||||
ActiveResource::Formats.const_get(mime_type_reference.to_s.camelize + "Format")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'active_resource/formats/xml_format'
|
||||
require 'active_resource/formats/json_format'
|
||||
@@ -1,23 +0,0 @@
|
||||
module ActiveResource
|
||||
module Formats
|
||||
module JsonFormat
|
||||
extend self
|
||||
|
||||
def extension
|
||||
"json"
|
||||
end
|
||||
|
||||
def mime_type
|
||||
"application/json"
|
||||
end
|
||||
|
||||
def encode(hash, options = nil)
|
||||
ActiveSupport::JSON.encode(hash, options)
|
||||
end
|
||||
|
||||
def decode(json)
|
||||
ActiveSupport::JSON.decode(json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
module ActiveResource
|
||||
module Formats
|
||||
module XmlFormat
|
||||
extend self
|
||||
|
||||
def extension
|
||||
"xml"
|
||||
end
|
||||
|
||||
def mime_type
|
||||
"application/xml"
|
||||
end
|
||||
|
||||
def encode(hash, options={})
|
||||
hash.to_xml(options)
|
||||
end
|
||||
|
||||
def decode(xml)
|
||||
from_xml_data(Hash.from_xml(xml))
|
||||
end
|
||||
|
||||
private
|
||||
# Manipulate from_xml Hash, because xml_simple is not exactly what we
|
||||
# want for Active Resource.
|
||||
def from_xml_data(data)
|
||||
if data.is_a?(Hash) && data.keys.size == 1
|
||||
data.values.first
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,302 +0,0 @@
|
||||
require 'active_resource/connection'
|
||||
|
||||
module ActiveResource
|
||||
class InvalidRequestError < StandardError; end #:nodoc:
|
||||
|
||||
# One thing that has always been a pain with remote web services is testing. The HttpMock
|
||||
# class makes it easy to test your Active Resource models by creating a set of mock responses to specific
|
||||
# requests.
|
||||
#
|
||||
# To test your Active Resource model, you simply call the ActiveResource::HttpMock.respond_to
|
||||
# method with an attached block. The block declares a set of URIs with expected input, and the output
|
||||
# each request should return. The passed in block has any number of entries in the following generalized
|
||||
# format:
|
||||
#
|
||||
# mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
||||
#
|
||||
# * <tt>http_method</tt> - The HTTP method to listen for. This can be +get+, +post+, +put+, +delete+ or
|
||||
# +head+.
|
||||
# * <tt>path</tt> - A string, starting with a "/", defining the URI that is expected to be
|
||||
# called.
|
||||
# * <tt>request_headers</tt> - Headers that are expected along with the request. This argument uses a
|
||||
# hash format, such as <tt>{ "Content-Type" => "application/xml" }</tt>. This mock will only trigger
|
||||
# if your tests sends a request with identical headers.
|
||||
# * <tt>body</tt> - The data to be returned. This should be a string of Active Resource parseable content,
|
||||
# such as XML.
|
||||
# * <tt>status</tt> - The HTTP response code, as an integer, to return with the response.
|
||||
# * <tt>response_headers</tt> - Headers to be returned with the response. Uses the same hash format as
|
||||
# <tt>request_headers</tt> listed above.
|
||||
#
|
||||
# In order for a mock to deliver its content, the incoming request must match by the <tt>http_method</tt>,
|
||||
# +path+ and <tt>request_headers</tt>. If no match is found an InvalidRequestError exception
|
||||
# will be raised showing you what request it could not find a response for and also what requests and response
|
||||
# pairs have been recorded so you can create a new mock for that request.
|
||||
#
|
||||
# ==== Example
|
||||
# def setup
|
||||
# @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
# ActiveResource::HttpMock.respond_to do |mock|
|
||||
# mock.post "/people.xml", {}, @matz, 201, "Location" => "/people/1.xml"
|
||||
# mock.get "/people/1.xml", {}, @matz
|
||||
# mock.put "/people/1.xml", {}, nil, 204
|
||||
# mock.delete "/people/1.xml", {}, nil, 200
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def test_get_matz
|
||||
# person = Person.find(1)
|
||||
# assert_equal "Matz", person.name
|
||||
# end
|
||||
#
|
||||
class HttpMock
|
||||
class Responder #:nodoc:
|
||||
def initialize(responses)
|
||||
@responses = responses
|
||||
end
|
||||
|
||||
for method in [ :post, :put, :get, :delete, :head ]
|
||||
# def post(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
||||
# @responses[Request.new(:post, path, nil, request_headers)] = Response.new(body || "", status, response_headers)
|
||||
# end
|
||||
module_eval <<-EOE, __FILE__, __LINE__ + 1
|
||||
def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
||||
@responses << [Request.new(:#{method}, path, nil, request_headers), Response.new(body || "", status, response_headers)]
|
||||
end
|
||||
EOE
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
|
||||
# Returns an array of all request objects that have been sent to the mock. You can use this to check
|
||||
# if your model actually sent an HTTP request.
|
||||
#
|
||||
# ==== Example
|
||||
# def setup
|
||||
# @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
# ActiveResource::HttpMock.respond_to do |mock|
|
||||
# mock.get "/people/1.xml", {}, @matz
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def test_should_request_remote_service
|
||||
# person = Person.find(1) # Call the remote service
|
||||
#
|
||||
# # This request object has the same HTTP method and path as declared by the mock
|
||||
# expected_request = ActiveResource::Request.new(:get, "/people/1.xml")
|
||||
#
|
||||
# # Assert that the mock received, and responded to, the expected request from the model
|
||||
# assert ActiveResource::HttpMock.requests.include?(expected_request)
|
||||
# end
|
||||
def requests
|
||||
@@requests ||= []
|
||||
end
|
||||
|
||||
# Returns the list of requests and their mocked responses. Look up a
|
||||
# response for a request using responses.assoc(request).
|
||||
def responses
|
||||
@@responses ||= []
|
||||
end
|
||||
|
||||
# Accepts a block which declares a set of requests and responses for the HttpMock to respond to in
|
||||
# the following format:
|
||||
#
|
||||
# mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
# ActiveResource::HttpMock.respond_to do |mock|
|
||||
# mock.post "/people.xml", {}, @matz, 201, "Location" => "/people/1.xml"
|
||||
# mock.get "/people/1.xml", {}, @matz
|
||||
# mock.put "/people/1.xml", {}, nil, 204
|
||||
# mock.delete "/people/1.xml", {}, nil, 200
|
||||
# end
|
||||
#
|
||||
# Alternatively, accepts a hash of <tt>{Request => Response}</tt> pairs allowing you to generate
|
||||
# these the following format:
|
||||
#
|
||||
# ActiveResource::Request.new(method, path, body, request_headers)
|
||||
# ActiveResource::Response.new(body, status, response_headers)
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# Request.new(:#{method}, path, nil, request_headers)
|
||||
#
|
||||
# @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
#
|
||||
# create_matz = ActiveResource::Request.new(:post, '/people.xml', @matz, {})
|
||||
# created_response = ActiveResource::Response.new("", 201, {"Location" => "/people/1.xml"})
|
||||
# get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil)
|
||||
# ok_response = ActiveResource::Response.new("", 200, {})
|
||||
#
|
||||
# pairs = {create_matz => created_response, get_matz => ok_response}
|
||||
#
|
||||
# ActiveResource::HttpMock.respond_to(pairs)
|
||||
#
|
||||
# Note, by default, every time you call +respond_to+, any previous request and response pairs stored
|
||||
# in HttpMock will be deleted giving you a clean slate to work on.
|
||||
#
|
||||
# If you want to override this behaviour, pass in +false+ as the last argument to +respond_to+
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# ActiveResource::HttpMock.respond_to do |mock|
|
||||
# mock.send(:get, "/people/1", {}, "XML1")
|
||||
# end
|
||||
# ActiveResource::HttpMock.responses.length #=> 1
|
||||
#
|
||||
# ActiveResource::HttpMock.respond_to(false) do |mock|
|
||||
# mock.send(:get, "/people/2", {}, "XML2")
|
||||
# end
|
||||
# ActiveResource::HttpMock.responses.length #=> 2
|
||||
#
|
||||
# This also works with passing in generated pairs of requests and responses, again, just pass in false
|
||||
# as the last argument:
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# ActiveResource::HttpMock.respond_to do |mock|
|
||||
# mock.send(:get, "/people/1", {}, "XML1")
|
||||
# end
|
||||
# ActiveResource::HttpMock.responses.length #=> 1
|
||||
#
|
||||
# get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil)
|
||||
# ok_response = ActiveResource::Response.new("", 200, {})
|
||||
#
|
||||
# pairs = {get_matz => ok_response}
|
||||
#
|
||||
# ActiveResource::HttpMock.respond_to(pairs, false)
|
||||
# ActiveResource::HttpMock.responses.length #=> 2
|
||||
def respond_to(*args) #:yields: mock
|
||||
pairs = args.first || {}
|
||||
reset! if args.last.class != FalseClass
|
||||
responses.concat pairs.to_a
|
||||
if block_given?
|
||||
yield Responder.new(responses)
|
||||
else
|
||||
Responder.new(responses)
|
||||
end
|
||||
end
|
||||
|
||||
# Deletes all logged requests and responses.
|
||||
def reset!
|
||||
requests.clear
|
||||
responses.clear
|
||||
end
|
||||
end
|
||||
|
||||
# body? methods
|
||||
{ true => %w(post put),
|
||||
false => %w(get delete head) }.each do |has_body, methods|
|
||||
methods.each do |method|
|
||||
# def post(path, body, headers)
|
||||
# request = ActiveResource::Request.new(:post, path, body, headers)
|
||||
# self.class.requests << request
|
||||
# if response = self.class.responses.assoc(request)
|
||||
# response[1]
|
||||
# else
|
||||
# raise InvalidRequestError.new("Could not find a response recorded for #{request.to_s} - Responses recorded are: - #{inspect_responses}")
|
||||
# end
|
||||
# end
|
||||
module_eval <<-EOE, __FILE__, __LINE__ + 1
|
||||
def #{method}(path, #{'body, ' if has_body}headers)
|
||||
request = ActiveResource::Request.new(:#{method}, path, #{has_body ? 'body, ' : 'nil, '}headers)
|
||||
self.class.requests << request
|
||||
if response = self.class.responses.assoc(request)
|
||||
response[1]
|
||||
else
|
||||
raise InvalidRequestError.new("Could not find a response recorded for \#{request.to_s} - Responses recorded are: \#{inspect_responses}")
|
||||
end
|
||||
end
|
||||
EOE
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(site) #:nodoc:
|
||||
@site = site
|
||||
end
|
||||
|
||||
def inspect_responses #:nodoc:
|
||||
self.class.responses.map { |r| r[0].to_s }.inspect
|
||||
end
|
||||
end
|
||||
|
||||
class Request
|
||||
attr_accessor :path, :method, :body, :headers
|
||||
|
||||
def initialize(method, path, body = nil, headers = {})
|
||||
@method, @path, @body, @headers = method, path, body, headers
|
||||
end
|
||||
|
||||
def ==(req)
|
||||
path == req.path && method == req.method && headers_match?(req)
|
||||
end
|
||||
|
||||
def to_s
|
||||
"<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def headers_match?(req)
|
||||
# Ignore format header on equality if it's not defined
|
||||
format_header = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[method]
|
||||
if headers[format_header].present? || req.headers[format_header].blank?
|
||||
headers == req.headers
|
||||
else
|
||||
headers.dup.merge(format_header => req.headers[format_header]) == req.headers
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class Response
|
||||
attr_accessor :body, :message, :code, :headers
|
||||
|
||||
def initialize(body, message = 200, headers = {})
|
||||
@body, @message, @headers = body, message.to_s, headers
|
||||
@code = @message[0,3].to_i
|
||||
|
||||
resp_cls = Net::HTTPResponse::CODE_TO_OBJ[@code.to_s]
|
||||
if resp_cls && !resp_cls.body_permitted?
|
||||
@body = nil
|
||||
end
|
||||
|
||||
if @body.nil?
|
||||
self['Content-Length'] = "0"
|
||||
else
|
||||
self['Content-Length'] = body.size.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def success?
|
||||
(200..299).include?(code)
|
||||
end
|
||||
|
||||
def [](key)
|
||||
headers[key]
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
headers[key] = value
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
if (other.is_a?(Response))
|
||||
other.body == body && other.message == message && other.headers == headers
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Connection
|
||||
private
|
||||
silence_warnings do
|
||||
def http
|
||||
@http ||= HttpMock.new(@site)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,290 +0,0 @@
|
||||
module ActiveResource
|
||||
class ResourceInvalid < ClientError #:nodoc:
|
||||
end
|
||||
|
||||
# Active Resource validation is reported to and from this object, which is used by Base#save
|
||||
# to determine whether the object in a valid state to be saved. See usage example in Validations.
|
||||
class Errors
|
||||
include Enumerable
|
||||
attr_reader :errors
|
||||
|
||||
delegate :empty?, :to => :errors
|
||||
|
||||
def initialize(base) # :nodoc:
|
||||
@base, @errors = base, {}
|
||||
end
|
||||
|
||||
# Add an error to the base Active Resource object rather than an attribute.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_folder = Folder.find(1)
|
||||
# my_folder.errors.add_to_base("You can't edit an existing folder")
|
||||
# my_folder.errors.on_base
|
||||
# # => "You can't edit an existing folder"
|
||||
#
|
||||
# my_folder.errors.add_to_base("This folder has been tagged as frozen")
|
||||
# my_folder.valid?
|
||||
# # => false
|
||||
# my_folder.errors.on_base
|
||||
# # => ["You can't edit an existing folder", "This folder has been tagged as frozen"]
|
||||
#
|
||||
def add_to_base(msg)
|
||||
add(:base, msg)
|
||||
end
|
||||
|
||||
# Adds an error to an Active Resource object's attribute (named for the +attribute+ parameter)
|
||||
# with the error message in +msg+.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_resource = Node.find(1)
|
||||
# my_resource.errors.add('name', 'can not be "base"') if my_resource.name == 'base'
|
||||
# my_resource.errors.on('name')
|
||||
# # => 'can not be "base"!'
|
||||
#
|
||||
# my_resource.errors.add('desc', 'can not be blank') if my_resource.desc == ''
|
||||
# my_resource.valid?
|
||||
# # => false
|
||||
# my_resource.errors.on('desc')
|
||||
# # => 'can not be blank!'
|
||||
#
|
||||
def add(attribute, msg)
|
||||
@errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
|
||||
@errors[attribute.to_s] << msg
|
||||
end
|
||||
|
||||
# Returns true if the specified +attribute+ has errors associated with it.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_resource = Disk.find(1)
|
||||
# my_resource.errors.add('location', 'must be Main') unless my_resource.location == 'Main'
|
||||
# my_resource.errors.on('location')
|
||||
# # => 'must be Main!'
|
||||
#
|
||||
# my_resource.errors.invalid?('location')
|
||||
# # => true
|
||||
# my_resource.errors.invalid?('name')
|
||||
# # => false
|
||||
def invalid?(attribute)
|
||||
!@errors[attribute.to_s].nil?
|
||||
end
|
||||
|
||||
# A method to return the errors associated with +attribute+, which returns nil, if no errors are
|
||||
# associated with the specified +attribute+, the error message if one error is associated with the specified +attribute+,
|
||||
# or an array of error messages if more than one error is associated with the specified +attribute+.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_person = Person.new(params[:person])
|
||||
# my_person.errors.on('login')
|
||||
# # => nil
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||
# my_person.errors.on('login')
|
||||
# # => 'can not be empty'
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be longer than 10 characters') if my_person.login.length > 10
|
||||
# my_person.errors.on('login')
|
||||
# # => ['can not be empty', 'can not be longer than 10 characters']
|
||||
def on(attribute)
|
||||
errors = @errors[attribute.to_s]
|
||||
return nil if errors.nil?
|
||||
errors.size == 1 ? errors.first : errors
|
||||
end
|
||||
|
||||
alias :[] :on
|
||||
|
||||
# A method to return errors assigned to +base+ object through add_to_base, which returns nil, if no errors are
|
||||
# associated with the specified +attribute+, the error message if one error is associated with the specified +attribute+,
|
||||
# or an array of error messages if more than one error is associated with the specified +attribute+.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_account = Account.find(1)
|
||||
# my_account.errors.on_base
|
||||
# # => nil
|
||||
#
|
||||
# my_account.errors.add_to_base("This account is frozen")
|
||||
# my_account.errors.on_base
|
||||
# # => "This account is frozen"
|
||||
#
|
||||
# my_account.errors.add_to_base("This account has been closed")
|
||||
# my_account.errors.on_base
|
||||
# # => ["This account is frozen", "This account has been closed"]
|
||||
#
|
||||
def on_base
|
||||
on(:base)
|
||||
end
|
||||
|
||||
# Yields each attribute and associated message per error added.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_person = Person.new(params[:person])
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||
# my_person.errors.add('password', 'can not be empty') if my_person.password == ''
|
||||
# messages = ''
|
||||
# my_person.errors.each {|attr, msg| messages += attr.humanize + " " + msg + "<br />"}
|
||||
# messages
|
||||
# # => "Login can not be empty<br />Password can not be empty<br />"
|
||||
#
|
||||
def each
|
||||
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
|
||||
end
|
||||
|
||||
# Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
|
||||
# through iteration as "First name can't be empty".
|
||||
#
|
||||
# ==== Examples
|
||||
# my_person = Person.new(params[:person])
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||
# my_person.errors.add('password', 'can not be empty') if my_person.password == ''
|
||||
# messages = ''
|
||||
# my_person.errors.each_full {|msg| messages += msg + "<br/>"}
|
||||
# messages
|
||||
# # => "Login can not be empty<br />Password can not be empty<br />"
|
||||
#
|
||||
def each_full
|
||||
full_messages.each { |msg| yield msg }
|
||||
end
|
||||
|
||||
# Returns all the full error messages in an array.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_person = Person.new(params[:person])
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||
# my_person.errors.add('password', 'can not be empty') if my_person.password == ''
|
||||
# messages = ''
|
||||
# my_person.errors.full_messages.each {|msg| messages += msg + "<br/>"}
|
||||
# messages
|
||||
# # => "Login can not be empty<br />Password can not be empty<br />"
|
||||
#
|
||||
def full_messages
|
||||
full_messages = []
|
||||
|
||||
@errors.each_key do |attr|
|
||||
@errors[attr].each do |msg|
|
||||
next if msg.nil?
|
||||
|
||||
if attr == "base"
|
||||
full_messages << msg
|
||||
else
|
||||
full_messages << [attr.humanize, msg].join(' ')
|
||||
end
|
||||
end
|
||||
end
|
||||
full_messages
|
||||
end
|
||||
|
||||
def clear
|
||||
@errors = {}
|
||||
end
|
||||
|
||||
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such
|
||||
# with this as well.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_person = Person.new(params[:person])
|
||||
# my_person.errors.size
|
||||
# # => 0
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||
# my_person.errors.add('password', 'can not be empty') if my_person.password == ''
|
||||
# my_person.error.size
|
||||
# # => 2
|
||||
#
|
||||
def size
|
||||
@errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
|
||||
end
|
||||
|
||||
alias_method :count, :size
|
||||
alias_method :length, :size
|
||||
|
||||
# Grabs errors from an array of messages (like ActiveRecord::Validations)
|
||||
def from_array(messages)
|
||||
clear
|
||||
humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) }
|
||||
messages.each do |message|
|
||||
attr_message = humanized_attributes.keys.detect do |attr_name|
|
||||
if message[0, attr_name.size + 1] == "#{attr_name} "
|
||||
add humanized_attributes[attr_name], message[(attr_name.size + 1)..-1]
|
||||
end
|
||||
end
|
||||
|
||||
add_to_base message if attr_message.nil?
|
||||
end
|
||||
end
|
||||
|
||||
# Grabs errors from the json response.
|
||||
def from_json(json)
|
||||
array = ActiveSupport::JSON.decode(json)['errors'] rescue []
|
||||
from_array array
|
||||
end
|
||||
|
||||
# Grabs errors from the XML response.
|
||||
def from_xml(xml)
|
||||
array = Array.wrap(Hash.from_xml(xml)['errors']['error']) rescue []
|
||||
from_array array
|
||||
end
|
||||
end
|
||||
|
||||
# Module to support validation and errors with Active Resource objects. The module overrides
|
||||
# Base#save to rescue ActiveResource::ResourceInvalid exceptions and parse the errors returned
|
||||
# in the web service response. The module also adds an +errors+ collection that mimics the interface
|
||||
# of the errors provided by ActiveRecord::Errors.
|
||||
#
|
||||
# ==== Example
|
||||
#
|
||||
# Consider a Person resource on the server requiring both a +first_name+ and a +last_name+ with a
|
||||
# <tt>validates_presence_of :first_name, :last_name</tt> declaration in the model:
|
||||
#
|
||||
# person = Person.new(:first_name => "Jim", :last_name => "")
|
||||
# person.save # => false (server returns an HTTP 422 status code and errors)
|
||||
# person.valid? # => false
|
||||
# person.errors.empty? # => false
|
||||
# person.errors.count # => 1
|
||||
# person.errors.full_messages # => ["Last name can't be empty"]
|
||||
# person.errors.on(:last_name) # => "can't be empty"
|
||||
# person.last_name = "Halpert"
|
||||
# person.save # => true (and person is now saved to the remote service)
|
||||
#
|
||||
module Validations
|
||||
def self.included(base) # :nodoc:
|
||||
base.class_eval do
|
||||
alias_method_chain :save, :validation
|
||||
end
|
||||
end
|
||||
|
||||
# Validate a resource and save (POST) it to the remote web service.
|
||||
def save_with_validation
|
||||
save_without_validation
|
||||
true
|
||||
rescue ResourceInvalid => error
|
||||
case self.class.format
|
||||
when ActiveResource::Formats[:xml]
|
||||
errors.from_xml(error.response.body)
|
||||
when ActiveResource::Formats[:json]
|
||||
errors.from_json(error.response.body)
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Checks for errors on an object (i.e., is resource.errors empty?).
|
||||
#
|
||||
# ==== Examples
|
||||
# my_person = Person.create(params[:person])
|
||||
# my_person.valid?
|
||||
# # => true
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||
# my_person.valid?
|
||||
# # => false
|
||||
def valid?
|
||||
errors.empty?
|
||||
end
|
||||
|
||||
# Returns the Errors object that holds all information about attribute error messages.
|
||||
def errors
|
||||
@errors ||= Errors.new(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
module ActiveResource
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 14
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
require 'active_resource'
|
||||
ActiveSupport::Deprecation.warn 'require "activeresource" is deprecated and will be removed in Rails 3. Use require "active_resource" instead.'
|
||||
@@ -1,21 +0,0 @@
|
||||
require 'rubygems'
|
||||
require 'test/unit'
|
||||
require 'active_support/test_case'
|
||||
|
||||
$:.unshift File.expand_path('../../lib', __FILE__)
|
||||
$:.unshift File.expand_path('../../../activesupport/lib', __FILE__)
|
||||
require 'active_resource'
|
||||
require 'active_resource/http_mock'
|
||||
|
||||
$:.unshift "#{File.dirname(__FILE__)}/../test"
|
||||
require 'setter_trap'
|
||||
|
||||
ActiveResource::Base.logger = Logger.new("#{File.dirname(__FILE__)}/debug.log")
|
||||
|
||||
def uses_gem(gem_name, test_name, version = '> 0')
|
||||
gem gem_name.to_s, version
|
||||
require gem_name.to_s
|
||||
yield
|
||||
rescue LoadError
|
||||
$stderr.puts "Skipping #{test_name} tests. `gem install #{gem_name}` and try again."
|
||||
end
|
||||
@@ -1,122 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
class AuthorizationTest < Test::Unit::TestCase
|
||||
Response = Struct.new(:code)
|
||||
|
||||
def setup
|
||||
@conn = ActiveResource::Connection.new('http://localhost')
|
||||
@matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
|
||||
@david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
|
||||
@authenticated_conn = ActiveResource::Connection.new("http://david:test123@localhost")
|
||||
@authorization_request_header = { 'Authorization' => 'Basic ZGF2aWQ6dGVzdDEyMw==' }
|
||||
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/2.xml", @authorization_request_header, @david
|
||||
mock.put "/people/2.xml", @authorization_request_header, nil, 204
|
||||
mock.delete "/people/2.xml", @authorization_request_header, nil, 200
|
||||
mock.post "/people/2/addresses.xml", @authorization_request_header, nil, 201, 'Location' => '/people/1/addresses/5'
|
||||
end
|
||||
end
|
||||
|
||||
def test_authorization_header
|
||||
authorization_header = @authenticated_conn.__send__(:authorization_header)
|
||||
assert_equal @authorization_request_header['Authorization'], authorization_header['Authorization']
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["david", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_with_username_but_no_password
|
||||
@conn = ActiveResource::Connection.new("http://david:@localhost")
|
||||
authorization_header = @conn.__send__(:authorization_header)
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["david"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_with_password_but_no_username
|
||||
@conn = ActiveResource::Connection.new("http://:test123@localhost")
|
||||
authorization_header = @conn.__send__(:authorization_header)
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_with_decoded_credentials_from_url
|
||||
@conn = ActiveResource::Connection.new("http://my%40email.com:%31%32%33@localhost")
|
||||
authorization_header = @conn.__send__(:authorization_header)
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["my@email.com", "123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_explicitly_setting_username_and_password
|
||||
@authenticated_conn = ActiveResource::Connection.new("http://@localhost")
|
||||
@authenticated_conn.user = 'david'
|
||||
@authenticated_conn.password = 'test123'
|
||||
authorization_header = @authenticated_conn.__send__(:authorization_header)
|
||||
assert_equal @authorization_request_header['Authorization'], authorization_header['Authorization']
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["david", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_explicitly_setting_username_but_no_password
|
||||
@conn = ActiveResource::Connection.new("http://@localhost")
|
||||
@conn.user = "david"
|
||||
authorization_header = @conn.__send__(:authorization_header)
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["david"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_explicitly_setting_password_but_no_username
|
||||
@conn = ActiveResource::Connection.new("http://@localhost")
|
||||
@conn.password = "test123"
|
||||
authorization_header = @conn.__send__(:authorization_header)
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_get
|
||||
david = @authenticated_conn.get("/people/2.xml")
|
||||
assert_equal "David", david["name"]
|
||||
end
|
||||
|
||||
def test_post
|
||||
response = @authenticated_conn.post("/people/2/addresses.xml")
|
||||
assert_equal "/people/1/addresses/5", response["Location"]
|
||||
end
|
||||
|
||||
def test_put
|
||||
response = @authenticated_conn.put("/people/2.xml")
|
||||
assert_equal 204, response.code
|
||||
end
|
||||
|
||||
def test_delete
|
||||
response = @authenticated_conn.delete("/people/2.xml")
|
||||
assert_equal 200, response.code
|
||||
end
|
||||
|
||||
def test_raises_invalid_request_on_unauthorized_requests
|
||||
assert_raise(ActiveResource::InvalidRequestError) { @conn.post("/people/2.xml") }
|
||||
assert_raise(ActiveResource::InvalidRequestError) { @conn.post("/people/2/addresses.xml") }
|
||||
assert_raise(ActiveResource::InvalidRequestError) { @conn.put("/people/2.xml") }
|
||||
assert_raise(ActiveResource::InvalidRequestError) { @conn.delete("/people/2.xml") }
|
||||
end
|
||||
|
||||
protected
|
||||
def assert_response_raises(klass, code)
|
||||
assert_raise(klass, "Expected response code #{code} to raise #{klass}") do
|
||||
@conn.__send__(:handle_response, Response.new(code))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,100 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
require 'fixtures/person'
|
||||
require 'fixtures/street_address'
|
||||
|
||||
class CustomMethodsTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
|
||||
@matz_deep = { :id => 1, :name => 'Matz', :other => 'other' }.to_xml(:root => 'person')
|
||||
@matz_array = [{ :id => 1, :name => 'Matz' }].to_xml(:root => 'people')
|
||||
@ryan = { :name => 'Ryan' }.to_xml(:root => 'person')
|
||||
@addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address')
|
||||
@addy_deep = { :id => 1, :street => '12345 Street', :zip => "27519" }.to_xml(:root => 'address')
|
||||
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/1.xml", {}, @matz
|
||||
mock.get "/people/1/shallow.xml", {}, @matz
|
||||
mock.get "/people/1/deep.xml", {}, @matz_deep
|
||||
mock.get "/people/retrieve.xml?name=Matz", {}, @matz_array
|
||||
mock.get "/people/managers.xml", {}, @matz_array
|
||||
mock.post "/people/hire.xml?name=Matz", {}, nil, 201
|
||||
mock.put "/people/1/promote.xml?position=Manager", {}, nil, 204
|
||||
mock.put "/people/promote.xml?name=Matz", {}, nil, 204, {}
|
||||
mock.put "/people/sort.xml?by=name", {}, nil, 204
|
||||
mock.delete "/people/deactivate.xml?name=Matz", {}, nil, 200
|
||||
mock.delete "/people/1/deactivate.xml", {}, nil, 200
|
||||
mock.post "/people/new/register.xml", {}, @ryan, 201, 'Location' => '/people/5.xml'
|
||||
mock.post "/people/1/register.xml", {}, @matz, 201
|
||||
mock.get "/people/1/addresses/1.xml", {}, @addy
|
||||
mock.get "/people/1/addresses/1/deep.xml", {}, @addy_deep
|
||||
mock.put "/people/1/addresses/1/normalize_phone.xml?locale=US", {}, nil, 204
|
||||
mock.put "/people/1/addresses/sort.xml?by=name", {}, nil, 204
|
||||
mock.post "/people/1/addresses/new/link.xml", {}, { :street => '12345 Street' }.to_xml(:root => 'address'), 201, 'Location' => '/people/1/addresses/2.xml'
|
||||
end
|
||||
|
||||
Person.user = nil
|
||||
Person.password = nil
|
||||
end
|
||||
|
||||
def teardown
|
||||
ActiveResource::HttpMock.reset!
|
||||
end
|
||||
|
||||
def test_custom_collection_method
|
||||
# GET
|
||||
assert_equal([{ "id" => 1, "name" => 'Matz' }], Person.get(:retrieve, :name => 'Matz'))
|
||||
|
||||
# POST
|
||||
assert_equal(ActiveResource::Response.new("", 201, {}), Person.post(:hire, :name => 'Matz'))
|
||||
|
||||
# PUT
|
||||
assert_equal ActiveResource::Response.new("", 204, {}),
|
||||
Person.put(:promote, {:name => 'Matz'}, 'atestbody')
|
||||
assert_equal ActiveResource::Response.new("", 204, {}), Person.put(:sort, :by => 'name')
|
||||
|
||||
# DELETE
|
||||
Person.delete :deactivate, :name => 'Matz'
|
||||
|
||||
# Nested resource
|
||||
assert_equal ActiveResource::Response.new("", 204, {}), StreetAddress.put(:sort, :person_id => 1, :by => 'name')
|
||||
end
|
||||
|
||||
def test_custom_element_method
|
||||
# Test GET against an element URL
|
||||
assert_equal Person.find(1).get(:shallow), {"id" => 1, "name" => 'Matz'}
|
||||
assert_equal Person.find(1).get(:deep), {"id" => 1, "name" => 'Matz', "other" => 'other'}
|
||||
|
||||
# Test PUT against an element URL
|
||||
assert_equal ActiveResource::Response.new("", 204, {}), Person.find(1).put(:promote, {:position => 'Manager'}, 'body')
|
||||
|
||||
# Test DELETE against an element URL
|
||||
assert_equal ActiveResource::Response.new("", 200, {}), Person.find(1).delete(:deactivate)
|
||||
|
||||
# With nested resources
|
||||
assert_equal StreetAddress.find(1, :params => { :person_id => 1 }).get(:deep),
|
||||
{ "id" => 1, "street" => '12345 Street', "zip" => "27519" }
|
||||
assert_equal ActiveResource::Response.new("", 204, {}),
|
||||
StreetAddress.find(1, :params => { :person_id => 1 }).put(:normalize_phone, :locale => 'US')
|
||||
end
|
||||
|
||||
def test_custom_new_element_method
|
||||
# Test POST against a new element URL
|
||||
ryan = Person.new(:name => 'Ryan')
|
||||
assert_equal ActiveResource::Response.new(@ryan, 201, {'Location' => '/people/5.xml'}), ryan.post(:register)
|
||||
expected_request = ActiveResource::Request.new(:post, '/people/new/register.xml', @ryan)
|
||||
assert_equal expected_request.body, ActiveResource::HttpMock.requests.first.body
|
||||
|
||||
# Test POST against a nested collection URL
|
||||
addy = StreetAddress.new(:street => '123 Test Dr.', :person_id => 1)
|
||||
assert_equal ActiveResource::Response.new({ :street => '12345 Street' }.to_xml(:root => 'address'),
|
||||
201, {'Location' => '/people/1/addresses/2.xml'}),
|
||||
addy.post(:link)
|
||||
|
||||
matz = Person.new(:id => 1, :name => 'Matz')
|
||||
assert_equal ActiveResource::Response.new(@matz, 201), matz.post(:register)
|
||||
end
|
||||
|
||||
def test_find_custom_resources
|
||||
assert_equal 'Matz', Person.find(:all, :from => :managers).first.name
|
||||
end
|
||||
end
|
||||
@@ -1,52 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
require "fixtures/person"
|
||||
require "fixtures/street_address"
|
||||
|
||||
class BaseEqualityTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@new = Person.new
|
||||
@one = Person.new(:id => 1)
|
||||
@two = Person.new(:id => 2)
|
||||
@street = StreetAddress.new(:id => 2)
|
||||
end
|
||||
|
||||
def test_should_equal_self
|
||||
assert @new == @new, '@new == @new'
|
||||
assert @one == @one, '@one == @one'
|
||||
end
|
||||
|
||||
def test_shouldnt_equal_new_resource
|
||||
assert @new != @one, '@new != @one'
|
||||
assert @one != @new, '@one != @new'
|
||||
end
|
||||
|
||||
def test_shouldnt_equal_different_class
|
||||
assert @two != @street, 'person != street_address with same id'
|
||||
assert @street != @two, 'street_address != person with same id'
|
||||
end
|
||||
|
||||
def test_eql_should_alias_equals_operator
|
||||
assert_equal @new == @new, @new.eql?(@new)
|
||||
assert_equal @new == @one, @new.eql?(@one)
|
||||
|
||||
assert_equal @one == @one, @one.eql?(@one)
|
||||
assert_equal @one == @new, @one.eql?(@new)
|
||||
|
||||
assert_equal @one == @street, @one.eql?(@street)
|
||||
end
|
||||
|
||||
def test_hash_should_be_id_hash
|
||||
[@new, @one, @two, @street].each do |resource|
|
||||
assert_equal resource.id.hash, resource.hash
|
||||
end
|
||||
end
|
||||
|
||||
def test_with_prefix_options
|
||||
assert_equal @one == @one, @one.eql?(@one)
|
||||
assert_equal @one == @one.dup, @one.eql?(@one.dup)
|
||||
new_one = @one.dup
|
||||
new_one.prefix_options = {:foo => 'bar'}
|
||||
assert_not_equal @one, new_one
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,161 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
require "fixtures/person"
|
||||
require "fixtures/street_address"
|
||||
|
||||
module Highrise
|
||||
class Note < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
|
||||
class Comment < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
|
||||
module Deeply
|
||||
module Nested
|
||||
|
||||
class Note < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
|
||||
class Comment < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
|
||||
module TestDifferentLevels
|
||||
|
||||
class Note < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class BaseLoadTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@matz = { :id => 1, :name => 'Matz' }
|
||||
|
||||
@first_address = { :id => 1, :street => '12345 Street' }
|
||||
@addresses = [@first_address, { :id => 2, :street => '67890 Street' }]
|
||||
@addresses_from_xml = { :street_addresses => @addresses }
|
||||
@addresses_from_xml_single = { :street_addresses => [ @first_address ] }
|
||||
|
||||
@deep = { :id => 1, :street => {
|
||||
:id => 1, :state => { :id => 1, :name => 'Oregon',
|
||||
:notable_rivers => [
|
||||
{ :id => 1, :name => 'Willamette' },
|
||||
{ :id => 2, :name => 'Columbia', :rafted_by => @matz }],
|
||||
:postal_codes => [ 97018, 1234567890 ],
|
||||
:places => [ "Columbia City", "Unknown" ]}}}
|
||||
|
||||
@person = Person.new
|
||||
end
|
||||
|
||||
def test_load_expects_hash
|
||||
assert_raise(ArgumentError) { @person.load nil }
|
||||
assert_raise(ArgumentError) { @person.load '<person id="1"/>' }
|
||||
end
|
||||
|
||||
def test_load_simple_hash
|
||||
assert_equal Hash.new, @person.attributes
|
||||
assert_equal @matz.stringify_keys, @person.load(@matz).attributes
|
||||
end
|
||||
|
||||
def test_load_one_with_existing_resource
|
||||
address = @person.load(:street_address => @first_address).street_address
|
||||
assert_kind_of StreetAddress, address
|
||||
assert_equal @first_address.stringify_keys, address.attributes
|
||||
end
|
||||
|
||||
def test_load_one_with_unknown_resource
|
||||
address = silence_warnings { @person.load(:address => @first_address).address }
|
||||
assert_kind_of Person::Address, address
|
||||
assert_equal @first_address.stringify_keys, address.attributes
|
||||
end
|
||||
|
||||
def test_load_collection_with_existing_resource
|
||||
addresses = @person.load(@addresses_from_xml).street_addresses
|
||||
assert_kind_of Array, addresses
|
||||
addresses.each { |address| assert_kind_of StreetAddress, address }
|
||||
assert_equal @addresses.map(&:stringify_keys), addresses.map(&:attributes)
|
||||
end
|
||||
|
||||
def test_load_collection_with_unknown_resource
|
||||
Person.__send__(:remove_const, :Address) if Person.const_defined?(:Address)
|
||||
assert !Person.const_defined?(:Address), "Address shouldn't exist until autocreated"
|
||||
addresses = silence_warnings { @person.load(:addresses => @addresses).addresses }
|
||||
assert Person.const_defined?(:Address), "Address should have been autocreated"
|
||||
addresses.each { |address| assert_kind_of Person::Address, address }
|
||||
assert_equal @addresses.map(&:stringify_keys), addresses.map(&:attributes)
|
||||
end
|
||||
|
||||
def test_load_collection_with_single_existing_resource
|
||||
addresses = @person.load(@addresses_from_xml_single).street_addresses
|
||||
assert_kind_of Array, addresses
|
||||
addresses.each { |address| assert_kind_of StreetAddress, address }
|
||||
assert_equal [ @first_address ].map(&:stringify_keys), addresses.map(&:attributes)
|
||||
end
|
||||
|
||||
def test_load_collection_with_single_unknown_resource
|
||||
Person.__send__(:remove_const, :Address) if Person.const_defined?(:Address)
|
||||
assert !Person.const_defined?(:Address), "Address shouldn't exist until autocreated"
|
||||
addresses = silence_warnings { @person.load(:addresses => [ @first_address ]).addresses }
|
||||
assert Person.const_defined?(:Address), "Address should have been autocreated"
|
||||
addresses.each { |address| assert_kind_of Person::Address, address }
|
||||
assert_equal [ @first_address ].map(&:stringify_keys), addresses.map(&:attributes)
|
||||
end
|
||||
|
||||
def test_recursively_loaded_collections
|
||||
person = @person.load(@deep)
|
||||
assert_equal @deep[:id], person.id
|
||||
|
||||
street = person.street
|
||||
assert_kind_of Person::Street, street
|
||||
assert_equal @deep[:street][:id], street.id
|
||||
|
||||
state = street.state
|
||||
assert_kind_of Person::Street::State, state
|
||||
assert_equal @deep[:street][:state][:id], state.id
|
||||
|
||||
rivers = state.notable_rivers
|
||||
assert_kind_of Array, rivers
|
||||
assert_kind_of Person::Street::State::NotableRiver, rivers.first
|
||||
assert_equal @deep[:street][:state][:notable_rivers].first[:id], rivers.first.id
|
||||
assert_equal @matz[:id], rivers.last.rafted_by.id
|
||||
|
||||
postal_codes = state.postal_codes
|
||||
assert_kind_of Array, postal_codes
|
||||
assert_equal 2, postal_codes.size
|
||||
assert_kind_of Fixnum, postal_codes.first
|
||||
assert_equal @deep[:street][:state][:postal_codes].first, postal_codes.first
|
||||
assert_kind_of Numeric, postal_codes.last
|
||||
assert_equal @deep[:street][:state][:postal_codes].last, postal_codes.last
|
||||
|
||||
places = state.places
|
||||
assert_kind_of Array, places
|
||||
assert_kind_of String, places.first
|
||||
assert_equal @deep[:street][:state][:places].first, places.first
|
||||
end
|
||||
|
||||
def test_nested_collections_within_the_same_namespace
|
||||
n = Highrise::Note.new(:comments => [{ :name => "1" }])
|
||||
assert_kind_of Highrise::Comment, n.comments.first
|
||||
end
|
||||
|
||||
def test_nested_collections_within_deeply_nested_namespace
|
||||
n = Highrise::Deeply::Nested::Note.new(:comments => [{ :name => "1" }])
|
||||
assert_kind_of Highrise::Deeply::Nested::Comment, n.comments.first
|
||||
end
|
||||
|
||||
def test_nested_collections_in_different_levels_of_namespaces
|
||||
n = Highrise::Deeply::Nested::TestDifferentLevels::Note.new(:comments => [{ :name => "1" }])
|
||||
assert_kind_of Highrise::Deeply::Nested::Comment, n.comments.first
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
@@ -1,98 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
require "fixtures/person"
|
||||
|
||||
class BaseErrorsTest < Test::Unit::TestCase
|
||||
def setup
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.post "/people.xml", {}, %q(<?xml version="1.0" encoding="UTF-8"?><errors><error>Age can't be blank</error><error>Name can't be blank</error><error>Name must start with a letter</error><error>Person quota full for today.</error></errors>), 422, {'Content-Type' => 'application/xml; charset=utf-8'}
|
||||
mock.post "/people.json", {}, %q({"errors":["Age can't be blank","Name can't be blank","Name must start with a letter","Person quota full for today."]}), 422, {'Content-Type' => 'application/json; charset=utf-8'}
|
||||
end
|
||||
@person = Person.new(:name => '', :age => '')
|
||||
assert_equal @person.save, false
|
||||
end
|
||||
|
||||
def test_should_mark_as_invalid
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
assert !@person.valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_parse_xml_errors
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
assert_kind_of ActiveResource::Errors, @person.errors
|
||||
assert_equal 4, @person.errors.size
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_parse_errors_to_individual_attributes
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
assert @person.errors[:name].any?
|
||||
assert_equal "can't be blank", @person.errors[:age]
|
||||
assert_equal ["can't be blank", "must start with a letter"], @person.errors[:name]
|
||||
assert_equal "Person quota full for today.", @person.errors[:base]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_iterate_over_errors
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
errors = []
|
||||
@person.errors.each { |attribute, message| errors << [attribute, message] }
|
||||
assert errors.include?(['name', "can't be blank"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_iterate_over_full_errors
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
errors = []
|
||||
@person.errors.to_a.each { |message| errors << message }
|
||||
assert errors.include?(["name", "can't be blank"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_format_full_errors
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
full = @person.errors.full_messages
|
||||
assert full.include?("Age can't be blank")
|
||||
assert full.include?("Name can't be blank")
|
||||
assert full.include?("Name must start with a letter")
|
||||
assert full.include?("Person quota full for today.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_mark_as_invalid_when_content_type_is_unavailable_in_response_header
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.post "/people.xml", {}, %q(<?xml version="1.0" encoding="UTF-8"?><errors><error>Age can't be blank</error><error>Name can't be blank</error><error>Name must start with a letter</error><error>Person quota full for today.</error></errors>), 422, {}
|
||||
mock.post "/people.json", {}, %q({"errors":["Age can't be blank","Name can't be blank","Name must start with a letter","Person quota full for today."]}), 422, {}
|
||||
end
|
||||
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
assert !@person.valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def invalid_user_using_format(mime_type_reference)
|
||||
previous_format = Person.format
|
||||
Person.format = mime_type_reference
|
||||
@person = Person.new(:name => '', :age => '')
|
||||
assert_equal false, @person.save
|
||||
|
||||
yield
|
||||
ensure
|
||||
Person.format = previous_format
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,238 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
class ConnectionTest < Test::Unit::TestCase
|
||||
ResponseCodeStub = Struct.new(:code)
|
||||
|
||||
def setup
|
||||
@conn = ActiveResource::Connection.new('http://localhost')
|
||||
@matz = { :id => 1, :name => 'Matz' }
|
||||
@david = { :id => 2, :name => 'David' }
|
||||
@people = [ @matz, @david ].to_xml(:root => 'people')
|
||||
@people_single = [ @matz ].to_xml(:root => 'people-single-elements')
|
||||
@people_empty = [ ].to_xml(:root => 'people-empty-elements')
|
||||
@matz = @matz.to_xml(:root => 'person')
|
||||
@david = @david.to_xml(:root => 'person')
|
||||
@header = {'key' => 'value'}.freeze
|
||||
|
||||
@default_request_headers = { 'Content-Type' => 'application/xml' }
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/2.xml", @header, @david
|
||||
mock.get "/people.xml", {}, @people
|
||||
mock.get "/people_single_elements.xml", {}, @people_single
|
||||
mock.get "/people_empty_elements.xml", {}, @people_empty
|
||||
mock.get "/people/1.xml", {}, @matz
|
||||
mock.put "/people/1.xml", {}, nil, 204
|
||||
mock.put "/people/2.xml", {}, @header, 204
|
||||
mock.delete "/people/1.xml", {}, nil, 200
|
||||
mock.delete "/people/2.xml", @header, nil, 200
|
||||
mock.post "/people.xml", {}, nil, 201, 'Location' => '/people/5.xml'
|
||||
mock.post "/members.xml", {}, @header, 201, 'Location' => '/people/6.xml'
|
||||
mock.head "/people/1.xml", {}, nil, 200
|
||||
end
|
||||
end
|
||||
|
||||
def test_handle_response
|
||||
# 2xx and 3xx are valid responses.
|
||||
[200, 299, 300, 399].each do |code|
|
||||
expected = ResponseCodeStub.new(code)
|
||||
assert_equal expected, handle_response(expected)
|
||||
end
|
||||
|
||||
# 400 is a bad request (e.g. malformed URI or missing request parameter)
|
||||
assert_response_raises ActiveResource::BadRequest, 400
|
||||
|
||||
# 401 is an unauthorized request
|
||||
assert_response_raises ActiveResource::UnauthorizedAccess, 401
|
||||
|
||||
# 403 is a forbidden requst (and authorizing will not help)
|
||||
assert_response_raises ActiveResource::ForbiddenAccess, 403
|
||||
|
||||
# 404 is a missing resource.
|
||||
assert_response_raises ActiveResource::ResourceNotFound, 404
|
||||
|
||||
# 405 is a missing not allowed error
|
||||
assert_response_raises ActiveResource::MethodNotAllowed, 405
|
||||
|
||||
# 409 is an optimistic locking error
|
||||
assert_response_raises ActiveResource::ResourceConflict, 409
|
||||
|
||||
# 410 is a removed resource
|
||||
assert_response_raises ActiveResource::ResourceGone, 410
|
||||
|
||||
# 422 is a validation error
|
||||
assert_response_raises ActiveResource::ResourceInvalid, 422
|
||||
|
||||
# 4xx are client errors.
|
||||
[402, 499].each do |code|
|
||||
assert_response_raises ActiveResource::ClientError, code
|
||||
end
|
||||
|
||||
# 5xx are server errors.
|
||||
[500, 599].each do |code|
|
||||
assert_response_raises ActiveResource::ServerError, code
|
||||
end
|
||||
|
||||
# Others are unknown.
|
||||
[199, 600].each do |code|
|
||||
assert_response_raises ActiveResource::ConnectionError, code
|
||||
end
|
||||
end
|
||||
|
||||
ResponseHeaderStub = Struct.new(:code, :message, 'Allow')
|
||||
def test_should_return_allowed_methods_for_method_no_allowed_exception
|
||||
begin
|
||||
handle_response ResponseHeaderStub.new(405, "HTTP Failed...", "GET, POST")
|
||||
rescue ActiveResource::MethodNotAllowed => e
|
||||
assert_equal "Failed with 405 HTTP Failed...", e.message
|
||||
assert_equal [:get, :post], e.allowed_methods
|
||||
end
|
||||
end
|
||||
|
||||
def test_initialize_raises_argument_error_on_missing_site
|
||||
assert_raise(ArgumentError) { ActiveResource::Connection.new(nil) }
|
||||
end
|
||||
|
||||
def test_site_accessor_accepts_uri_or_string_argument
|
||||
site = URI.parse("http://localhost")
|
||||
|
||||
assert_raise(URI::InvalidURIError) { @conn.site = nil }
|
||||
|
||||
assert_nothing_raised { @conn.site = "http://localhost" }
|
||||
assert_equal site, @conn.site
|
||||
|
||||
assert_nothing_raised { @conn.site = site }
|
||||
assert_equal site, @conn.site
|
||||
end
|
||||
|
||||
def test_proxy_accessor_accepts_uri_or_string_argument
|
||||
proxy = URI.parse("http://proxy_user:proxy_password@proxy.local:4242")
|
||||
|
||||
assert_nothing_raised { @conn.proxy = "http://proxy_user:proxy_password@proxy.local:4242" }
|
||||
assert_equal proxy, @conn.proxy
|
||||
|
||||
assert_nothing_raised { @conn.proxy = proxy }
|
||||
assert_equal proxy, @conn.proxy
|
||||
end
|
||||
|
||||
def test_timeout_accessor
|
||||
@conn.timeout = 5
|
||||
assert_equal 5, @conn.timeout
|
||||
end
|
||||
|
||||
def test_get
|
||||
matz = @conn.get("/people/1.xml")
|
||||
assert_equal "Matz", matz["name"]
|
||||
end
|
||||
|
||||
def test_head
|
||||
response = @conn.head("/people/1.xml")
|
||||
assert response.body.blank?
|
||||
assert_equal 200, response.code
|
||||
end
|
||||
|
||||
def test_get_with_header
|
||||
david = @conn.get("/people/2.xml", @header)
|
||||
assert_equal "David", david["name"]
|
||||
end
|
||||
|
||||
def test_get_collection
|
||||
people = @conn.get("/people.xml")
|
||||
assert_equal "Matz", people[0]["name"]
|
||||
assert_equal "David", people[1]["name"]
|
||||
end
|
||||
|
||||
def test_get_collection_single
|
||||
people = @conn.get("/people_single_elements.xml")
|
||||
assert_equal "Matz", people[0]["name"]
|
||||
end
|
||||
|
||||
def test_get_collection_empty
|
||||
people = @conn.get("/people_empty_elements.xml")
|
||||
assert_equal [], people
|
||||
end
|
||||
|
||||
def test_post
|
||||
response = @conn.post("/people.xml")
|
||||
assert_equal "/people/5.xml", response["Location"]
|
||||
end
|
||||
|
||||
def test_post_with_header
|
||||
response = @conn.post("/members.xml", @header)
|
||||
assert_equal "/people/6.xml", response["Location"]
|
||||
end
|
||||
|
||||
def test_put
|
||||
response = @conn.put("/people/1.xml")
|
||||
assert_equal 204, response.code
|
||||
end
|
||||
|
||||
def test_put_with_header
|
||||
response = @conn.put("/people/2.xml", @header)
|
||||
assert_equal 204, response.code
|
||||
end
|
||||
|
||||
def test_delete
|
||||
response = @conn.delete("/people/1.xml")
|
||||
assert_equal 200, response.code
|
||||
end
|
||||
|
||||
def test_delete_with_header
|
||||
response = @conn.delete("/people/2.xml", @header)
|
||||
assert_equal 200, response.code
|
||||
end
|
||||
|
||||
def test_timeout
|
||||
@http = mock('new Net::HTTP')
|
||||
@conn.expects(:http).returns(@http)
|
||||
@http.expects(:get).raises(Timeout::Error, 'execution expired')
|
||||
assert_raise(ActiveResource::TimeoutError) { @conn.get('/people_timeout.xml') }
|
||||
end
|
||||
|
||||
def test_setting_timeout
|
||||
http = Net::HTTP.new('')
|
||||
|
||||
[10, 20].each do |timeout|
|
||||
@conn.timeout = timeout
|
||||
@conn.send(:configure_http, http)
|
||||
assert_equal timeout, http.open_timeout
|
||||
assert_equal timeout, http.read_timeout
|
||||
end
|
||||
end
|
||||
|
||||
def test_accept_http_header
|
||||
@http = mock('new Net::HTTP')
|
||||
@conn.expects(:http).returns(@http)
|
||||
path = '/people/1.xml'
|
||||
@http.expects(:get).with(path, {'Accept' => 'application/xhtml+xml'}).returns(ActiveResource::Response.new(@matz, 200, {'Content-Type' => 'text/xhtml'}))
|
||||
assert_nothing_raised(Mocha::ExpectationError) { @conn.get(path, {'Accept' => 'application/xhtml+xml'}) }
|
||||
end
|
||||
|
||||
def test_ssl_options_get_applied_to_http
|
||||
http = Net::HTTP.new('')
|
||||
@conn.site="https://secure"
|
||||
@conn.ssl_options={:verify_mode => OpenSSL::SSL::VERIFY_PEER}
|
||||
@conn.timeout = 10 # prevent warning about uninitialized.
|
||||
@conn.send(:configure_http, http)
|
||||
|
||||
assert http.use_ssl?
|
||||
assert_equal http.verify_mode, OpenSSL::SSL::VERIFY_PEER
|
||||
end
|
||||
|
||||
def test_ssl_error
|
||||
http = Net::HTTP.new('')
|
||||
@conn.expects(:http).returns(http)
|
||||
http.expects(:get).raises(OpenSSL::SSL::SSLError, 'Expired certificate')
|
||||
assert_raise(ActiveResource::SSLError) { @conn.get('/people/1.xml') }
|
||||
end
|
||||
|
||||
protected
|
||||
def assert_response_raises(klass, code)
|
||||
assert_raise(klass, "Expected response code #{code} to raise #{klass}") do
|
||||
handle_response ResponseCodeStub.new(code)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_response(response)
|
||||
@conn.__send__(:handle_response, response)
|
||||
end
|
||||
end
|
||||
14
activeresource/test/fixtures/beast.rb
vendored
14
activeresource/test/fixtures/beast.rb
vendored
@@ -1,14 +0,0 @@
|
||||
class BeastResource < ActiveResource::Base
|
||||
self.site = 'http://beast.caboo.se'
|
||||
site.user = 'foo'
|
||||
site.password = 'bar'
|
||||
end
|
||||
|
||||
class Forum < BeastResource
|
||||
# taken from BeastResource
|
||||
# self.site = 'http://beast.caboo.se'
|
||||
end
|
||||
|
||||
class Topic < BeastResource
|
||||
self.site += '/forums/:forum_id'
|
||||
end
|
||||
3
activeresource/test/fixtures/customer.rb
vendored
3
activeresource/test/fixtures/customer.rb
vendored
@@ -1,3 +0,0 @@
|
||||
class Customer < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
3
activeresource/test/fixtures/person.rb
vendored
3
activeresource/test/fixtures/person.rb
vendored
@@ -1,3 +0,0 @@
|
||||
class Person < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
4
activeresource/test/fixtures/proxy.rb
vendored
4
activeresource/test/fixtures/proxy.rb
vendored
@@ -1,4 +0,0 @@
|
||||
class ProxyResource < ActiveResource::Base
|
||||
self.site = "http://localhost"
|
||||
self.proxy = "http://user:password@proxy.local:3000"
|
||||
end
|
||||
@@ -1,4 +0,0 @@
|
||||
class StreetAddress < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000/people/:person_id/"
|
||||
self.element_name = 'address'
|
||||
end
|
||||
@@ -1,112 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
require "fixtures/person"
|
||||
require "fixtures/street_address"
|
||||
|
||||
class FormatTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@matz = { :id => 1, :name => 'Matz' }
|
||||
@david = { :id => 2, :name => 'David' }
|
||||
|
||||
@programmers = [ @matz, @david ]
|
||||
end
|
||||
|
||||
def test_http_format_header_name
|
||||
header_name = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[:get]
|
||||
assert_equal 'Accept', header_name
|
||||
|
||||
headers_names = [ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[:put], ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[:post]]
|
||||
headers_names.each{ |name| assert_equal 'Content-Type', name }
|
||||
end
|
||||
|
||||
def test_formats_on_single_element
|
||||
for format in [ :json, :xml ]
|
||||
using_format(Person, format) do
|
||||
ActiveResource::HttpMock.respond_to.get "/people/1.#{format}", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode(@david)
|
||||
assert_equal @david[:name], Person.find(1).name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_formats_on_collection
|
||||
for format in [ :json, :xml ]
|
||||
using_format(Person, format) do
|
||||
ActiveResource::HttpMock.respond_to.get "/people.#{format}", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode(@programmers)
|
||||
remote_programmers = Person.find(:all)
|
||||
assert_equal 2, remote_programmers.size
|
||||
assert remote_programmers.select { |p| p.name == 'David' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_formats_on_custom_collection_method
|
||||
for format in [ :json, :xml ]
|
||||
using_format(Person, format) do
|
||||
ActiveResource::HttpMock.respond_to.get "/people/retrieve.#{format}?name=David", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode([@david])
|
||||
remote_programmers = Person.get(:retrieve, :name => 'David')
|
||||
assert_equal 1, remote_programmers.size
|
||||
assert_equal @david[:id], remote_programmers[0]['id']
|
||||
assert_equal @david[:name], remote_programmers[0]['name']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_formats_on_custom_element_method
|
||||
for format in [ :json, :xml ]
|
||||
using_format(Person, format) do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/2.#{format}", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode(@david)
|
||||
mock.get "/people/2/shallow.#{format}", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode(@david)
|
||||
end
|
||||
remote_programmer = Person.find(2).get(:shallow)
|
||||
assert_equal @david[:id], remote_programmer['id']
|
||||
assert_equal @david[:name], remote_programmer['name']
|
||||
end
|
||||
end
|
||||
|
||||
for format in [ :json, :xml ]
|
||||
ryan = ActiveResource::Formats[format].encode({ :name => 'Ryan' })
|
||||
using_format(Person, format) do
|
||||
remote_ryan = Person.new(:name => 'Ryan')
|
||||
ActiveResource::HttpMock.respond_to.post "/people.#{format}", {'Content-Type' => ActiveResource::Formats[format].mime_type}, ryan, 201, {'Location' => "/people/5.#{format}"}
|
||||
remote_ryan.save
|
||||
|
||||
remote_ryan = Person.new(:name => 'Ryan')
|
||||
ActiveResource::HttpMock.respond_to.post "/people/new/register.#{format}", {'Content-Type' => ActiveResource::Formats[format].mime_type}, ryan, 201, {'Location' => "/people/5.#{format}"}
|
||||
assert_equal ActiveResource::Response.new(ryan, 201, {'Location' => "/people/5.#{format}"}), remote_ryan.post(:register)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_setting_format_before_site
|
||||
resource = Class.new(ActiveResource::Base)
|
||||
resource.format = :json
|
||||
resource.site = 'http://37s.sunrise.i:3000'
|
||||
assert_equal ActiveResource::Formats[:json], resource.connection.format
|
||||
end
|
||||
|
||||
def test_serialization_of_nested_resource
|
||||
address = { :street => '12345 Street' }
|
||||
person = { :name=> 'Rus', :address => address}
|
||||
|
||||
[:json, :xml].each do |format|
|
||||
encoded_person = ActiveResource::Formats[format].encode(person)
|
||||
assert_match(/12345 Street/, encoded_person)
|
||||
remote_person = Person.new(person.update({:address => StreetAddress.new(address)}))
|
||||
assert_kind_of StreetAddress, remote_person.address
|
||||
using_format(Person, format) do
|
||||
ActiveResource::HttpMock.respond_to.post "/people.#{format}", {'Content-Type' => ActiveResource::Formats[format].mime_type}, encoded_person, 201, {'Location' => "/people/5.#{format}"}
|
||||
remote_person.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def using_format(klass, mime_type_reference)
|
||||
previous_format = klass.format
|
||||
klass.format = mime_type_reference
|
||||
|
||||
yield
|
||||
ensure
|
||||
klass.format = previous_format
|
||||
end
|
||||
end
|
||||
@@ -1,155 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
class HttpMockTest < ActiveSupport::TestCase
|
||||
def setup
|
||||
@http = ActiveResource::HttpMock.new("http://example.com")
|
||||
end
|
||||
|
||||
FORMAT_HEADER = { :get => 'Accept',
|
||||
:put => 'Content-Type',
|
||||
:post => 'Content-Type',
|
||||
:delete => 'Accept',
|
||||
:head => 'Accept'
|
||||
}
|
||||
|
||||
[:post, :put, :get, :delete, :head].each do |method|
|
||||
test "responds to simple #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/xml"}, "Response")
|
||||
end
|
||||
|
||||
assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body
|
||||
end
|
||||
|
||||
test "adds format header by default to #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {}, "Response")
|
||||
end
|
||||
|
||||
assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body
|
||||
end
|
||||
|
||||
test "respond only when headers match header by default to #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {"X-Header" => "X"}, "Response")
|
||||
end
|
||||
|
||||
assert_equal "Response", request(method, "/people/1", "X-Header" => "X").body
|
||||
assert_raise(ActiveResource::InvalidRequestError) { request(method, "/people/1") }
|
||||
end
|
||||
|
||||
test "does not overwrite format header to #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/json"}, "Response")
|
||||
end
|
||||
|
||||
assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/json").body
|
||||
end
|
||||
|
||||
test "ignores format header when there is only one response to same url in a #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {}, "Response")
|
||||
end
|
||||
|
||||
assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/json").body
|
||||
assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body
|
||||
end
|
||||
|
||||
test "responds correctly when format header is given to #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/xml"}, "XML")
|
||||
mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/json"}, "Json")
|
||||
end
|
||||
|
||||
assert_equal "XML", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body
|
||||
assert_equal "Json", request(method, "/people/1", FORMAT_HEADER[method] => "application/json").body
|
||||
end
|
||||
|
||||
test "raises InvalidRequestError if no response found for the #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/xml"}, "XML")
|
||||
end
|
||||
|
||||
assert_raise(::ActiveResource::InvalidRequestError) do
|
||||
request(method, "/people/1", FORMAT_HEADER[method] => "application/json")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
test "allows you to send in pairs directly to the respond_to method" do
|
||||
matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
|
||||
create_matz = ActiveResource::Request.new(:post, '/people.xml', matz, {})
|
||||
created_response = ActiveResource::Response.new("", 201, {"Location" => "/people/1.xml"})
|
||||
get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil)
|
||||
ok_response = ActiveResource::Response.new(matz, 200, {})
|
||||
|
||||
pairs = {create_matz => created_response, get_matz => ok_response}
|
||||
|
||||
ActiveResource::HttpMock.respond_to(pairs)
|
||||
assert_equal 2, ActiveResource::HttpMock.responses.length
|
||||
assert_equal "", ActiveResource::HttpMock.responses.assoc(create_matz)[1].body
|
||||
assert_equal matz, ActiveResource::HttpMock.responses.assoc(get_matz)[1].body
|
||||
end
|
||||
|
||||
test "resets all mocked responses on each call to respond_to with a block by default" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(:get, "/people/1", {}, "XML1")
|
||||
end
|
||||
assert_equal 1, ActiveResource::HttpMock.responses.length
|
||||
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(:get, "/people/2", {}, "XML2")
|
||||
end
|
||||
assert_equal 1, ActiveResource::HttpMock.responses.length
|
||||
end
|
||||
|
||||
test "resets all mocked responses on each call to respond_to by passing pairs by default" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(:get, "/people/1", {}, "XML1")
|
||||
end
|
||||
assert_equal 1, ActiveResource::HttpMock.responses.length
|
||||
|
||||
matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil)
|
||||
ok_response = ActiveResource::Response.new(matz, 200, {})
|
||||
ActiveResource::HttpMock.respond_to({get_matz => ok_response})
|
||||
|
||||
assert_equal 1, ActiveResource::HttpMock.responses.length
|
||||
end
|
||||
|
||||
test "allows you to add new responses to the existing responses by calling a block" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(:get, "/people/1", {}, "XML1")
|
||||
end
|
||||
assert_equal 1, ActiveResource::HttpMock.responses.length
|
||||
|
||||
ActiveResource::HttpMock.respond_to(false) do |mock|
|
||||
mock.send(:get, "/people/2", {}, "XML2")
|
||||
end
|
||||
assert_equal 2, ActiveResource::HttpMock.responses.length
|
||||
end
|
||||
|
||||
test "allows you to add new responses to the existing responses by passing pairs" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(:get, "/people/1", {}, "XML1")
|
||||
end
|
||||
assert_equal 1, ActiveResource::HttpMock.responses.length
|
||||
|
||||
matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil)
|
||||
ok_response = ActiveResource::Response.new(matz, 200, {})
|
||||
ActiveResource::HttpMock.respond_to({get_matz => ok_response}, false)
|
||||
|
||||
assert_equal 2, ActiveResource::HttpMock.responses.length
|
||||
end
|
||||
|
||||
def request(method, path, headers = {}, body = nil)
|
||||
if [:put, :post].include? method
|
||||
@http.send(method, path, body, headers)
|
||||
else
|
||||
@http.send(method, path, headers)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
class SetterTrap < ActiveSupport::BasicObject
|
||||
class << self
|
||||
def rollback_sets(obj)
|
||||
trapped = new(obj)
|
||||
yield(trapped).tap { trapped.rollback_sets }
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(obj)
|
||||
@cache = {}
|
||||
@obj = obj
|
||||
end
|
||||
|
||||
def respond_to?(method)
|
||||
@obj.respond_to?(method)
|
||||
end
|
||||
|
||||
def method_missing(method, *args, &proc)
|
||||
@cache[method] ||= @obj.send($`) if method.to_s =~ /=$/
|
||||
@obj.send method, *args, &proc
|
||||
end
|
||||
|
||||
def rollback_sets
|
||||
@cache.each { |k, v| @obj.send k, v }
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,8 @@
|
||||
version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).chomp
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'activesupport'
|
||||
s.version = '2.3.18'
|
||||
s.version = version
|
||||
s.summary = 'Support and utility classes used by the Rails framework.'
|
||||
s.description = 'Utility library which carries commonly used classes and goodies from the Rails framework'
|
||||
|
||||
@@ -9,4 +11,7 @@ Gem::Specification.new do |s|
|
||||
s.homepage = 'http://www.rubyonrails.org'
|
||||
|
||||
s.require_path = 'lib'
|
||||
|
||||
s.add_dependency('i18n', '~> 0.6', '>= 0.6.4')
|
||||
s.add_dependency('multi_json', '~> 1.0')
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
begin
|
||||
$:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
rescue IOError
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#--
|
||||
# Copyright (c) 2005 David Heinemeier Hansson
|
||||
# Copyright (c) 2005-2011 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
@@ -21,40 +21,62 @@
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
module ActiveSupport
|
||||
def self.load_all!
|
||||
[Dependencies, Deprecation, Gzip, MessageVerifier, Multibyte, SecureRandom, TimeWithZone]
|
||||
end
|
||||
require 'securerandom'
|
||||
|
||||
autoload :BacktraceCleaner, 'active_support/backtrace_cleaner'
|
||||
autoload :Base64, 'active_support/base64'
|
||||
autoload :BasicObject, 'active_support/basic_object'
|
||||
autoload :BufferedLogger, 'active_support/buffered_logger'
|
||||
autoload :Cache, 'active_support/cache'
|
||||
autoload :Callbacks, 'active_support/callbacks'
|
||||
autoload :Deprecation, 'active_support/deprecation'
|
||||
autoload :Duration, 'active_support/duration'
|
||||
autoload :Gzip, 'active_support/gzip'
|
||||
autoload :Inflector, 'active_support/inflector'
|
||||
autoload :Memoizable, 'active_support/memoizable'
|
||||
autoload :MessageEncryptor, 'active_support/message_encryptor'
|
||||
autoload :MessageVerifier, 'active_support/message_verifier'
|
||||
autoload :Multibyte, 'active_support/multibyte'
|
||||
autoload :OptionMerger, 'active_support/option_merger'
|
||||
autoload :OrderedHash, 'active_support/ordered_hash'
|
||||
autoload :OrderedOptions, 'active_support/ordered_options'
|
||||
autoload :Rescuable, 'active_support/rescuable'
|
||||
autoload :SafeBuffer, 'active_support/core_ext/string/output_safety'
|
||||
autoload :SecureRandom, 'active_support/secure_random'
|
||||
autoload :StringInquirer, 'active_support/string_inquirer'
|
||||
autoload :TimeWithZone, 'active_support/time_with_zone'
|
||||
autoload :TimeZone, 'active_support/values/time_zone'
|
||||
autoload :XmlMini, 'active_support/xml_mini'
|
||||
module ActiveSupport
|
||||
class << self
|
||||
attr_accessor :load_all_hooks
|
||||
def on_load_all(&hook) load_all_hooks << hook end
|
||||
def load_all!; load_all_hooks.each { |hook| hook.call } end
|
||||
end
|
||||
self.load_all_hooks = []
|
||||
|
||||
on_load_all do
|
||||
[Dependencies, Deprecation, Gzip, MessageVerifier, Multibyte]
|
||||
end
|
||||
end
|
||||
|
||||
require 'active_support/vendor'
|
||||
require 'active_support/core_ext'
|
||||
require 'active_support/dependencies'
|
||||
require 'active_support/json'
|
||||
require "active_support/dependencies/autoload"
|
||||
require "active_support/version"
|
||||
|
||||
I18n.load_path << "#{File.dirname(__FILE__)}/active_support/locale/en.yml"
|
||||
module ActiveSupport
|
||||
extend ActiveSupport::Autoload
|
||||
|
||||
autoload :DescendantsTracker
|
||||
autoload :FileUpdateChecker
|
||||
autoload :LogSubscriber
|
||||
autoload :Notifications
|
||||
|
||||
# TODO: Narrow this list down
|
||||
eager_autoload do
|
||||
autoload :BacktraceCleaner
|
||||
autoload :Base64
|
||||
autoload :BasicObject
|
||||
autoload :Benchmarkable
|
||||
autoload :BufferedLogger
|
||||
autoload :Cache
|
||||
autoload :Callbacks
|
||||
autoload :Concern
|
||||
autoload :Configurable
|
||||
autoload :Deprecation
|
||||
autoload :Gzip
|
||||
autoload :Inflector
|
||||
autoload :JSON
|
||||
autoload :Memoizable
|
||||
autoload :MessageEncryptor
|
||||
autoload :MessageVerifier
|
||||
autoload :Multibyte
|
||||
autoload :OptionMerger
|
||||
autoload :OrderedHash
|
||||
autoload :OrderedOptions
|
||||
autoload :Rescuable
|
||||
autoload :StringInquirer
|
||||
autoload :TaggedLogging
|
||||
autoload :XmlMini
|
||||
end
|
||||
|
||||
autoload :SafeBuffer, "active_support/core_ext/string/output_safety"
|
||||
autoload :TestCase
|
||||
end
|
||||
|
||||
autoload :I18n, "active_support/i18n"
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
# For forward compatibility with Rails 3.
|
||||
#
|
||||
# require 'active_support' loads a very bare minumum in Rails 3.
|
||||
# require 'active_support/all' loads the whole suite like Rails 2 did.
|
||||
#
|
||||
# To prepare for Rails 3, switch to require 'active_support/all' now.
|
||||
|
||||
require 'active_support'
|
||||
require 'active_support/time'
|
||||
require 'active_support/core_ext'
|
||||
|
||||
@@ -1,27 +1,43 @@
|
||||
module ActiveSupport
|
||||
# Many backtraces include too much information that's not relevant for the context. This makes it hard to find the signal
|
||||
# in the backtrace and adds debugging time. With a BacktraceCleaner, you can setup filters and silencers for your particular
|
||||
# context, so only the relevant lines are included.
|
||||
# Backtraces often include many lines that are not relevant for the context under review. This makes it hard to find the
|
||||
# signal amongst the backtrace noise, and adds debugging time. With a BacktraceCleaner, filters and silencers are used to
|
||||
# remove the noisy lines, so that only the most relevant lines remain.
|
||||
#
|
||||
# If you need to reconfigure an existing BacktraceCleaner, like the one in Rails, to show as much as possible, you can always
|
||||
# call BacktraceCleaner#remove_silencers!
|
||||
# Filters are used to modify lines of data, while silencers are used to remove lines entirely. The typical filter use case
|
||||
# is to remove lengthy path information from the start of each line, and view file paths relevant to the app directory
|
||||
# instead of the file system root. The typical silencer use case is to exclude the output of a noisy library from the
|
||||
# backtrace, so that you can focus on the rest.
|
||||
#
|
||||
# Example:
|
||||
# ==== Example:
|
||||
#
|
||||
# bc = BacktraceCleaner.new
|
||||
# bc.add_filter { |line| line.gsub(Rails.root, '') }
|
||||
# bc.add_filter { |line| line.gsub(Rails.root, '') }
|
||||
# bc.add_silencer { |line| line =~ /mongrel|rubygems/ }
|
||||
# bc.clean(exception.backtrace) # will strip the Rails.root prefix and skip any lines from mongrel or rubygems
|
||||
#
|
||||
# To reconfigure an existing BacktraceCleaner (like the default one in Rails) and show as much data as possible, you can
|
||||
# always call <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the backtrace to a pristine state. If you
|
||||
# need to reconfigure an existing BacktraceCleaner so that it does not filter or modify the paths of any lines of the
|
||||
# backtrace, you can call BacktraceCleaner#remove_filters! These two methods will give you a completely untouched backtrace.
|
||||
#
|
||||
# Inspired by the Quiet Backtrace gem by Thoughtbot.
|
||||
class BacktraceCleaner
|
||||
def initialize
|
||||
@filters, @silencers = [], []
|
||||
end
|
||||
|
||||
# Returns the backtrace after all filters and silencers has been run against it. Filters run first, then silencers.
|
||||
def clean(backtrace)
|
||||
silence(filter(backtrace))
|
||||
|
||||
# Returns the backtrace after all filters and silencers have been run against it. Filters run first, then silencers.
|
||||
def clean(backtrace, kind = :silent)
|
||||
filtered = filter(backtrace)
|
||||
|
||||
case kind
|
||||
when :silent
|
||||
silence(filtered)
|
||||
when :noise
|
||||
noise(filtered)
|
||||
else
|
||||
filtered
|
||||
end
|
||||
end
|
||||
|
||||
# Adds a filter from the block provided. Each line in the backtrace will be mapped against this filter.
|
||||
@@ -34,8 +50,8 @@ module ActiveSupport
|
||||
@filters << block
|
||||
end
|
||||
|
||||
# Adds a silencer from the block provided. If the silencer returns true for a given line, it'll be excluded from the
|
||||
# clean backtrace.
|
||||
# Adds a silencer from the block provided. If the silencer returns true for a given line, it will be excluded from
|
||||
# the clean backtrace.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
@@ -46,26 +62,37 @@ module ActiveSupport
|
||||
end
|
||||
|
||||
# Will remove all silencers, but leave in the filters. This is useful if your context of debugging suddenly expands as
|
||||
# you suspect a bug in the libraries you use.
|
||||
# you suspect a bug in one of the libraries you use.
|
||||
def remove_silencers!
|
||||
@silencers = []
|
||||
end
|
||||
|
||||
|
||||
def remove_filters!
|
||||
@filters = []
|
||||
end
|
||||
|
||||
private
|
||||
def filter(backtrace)
|
||||
@filters.each do |f|
|
||||
backtrace = backtrace.map { |line| f.call(line) }
|
||||
end
|
||||
|
||||
|
||||
backtrace
|
||||
end
|
||||
|
||||
|
||||
def silence(backtrace)
|
||||
@silencers.each do |s|
|
||||
backtrace = backtrace.reject { |line| s.call(line) }
|
||||
end
|
||||
|
||||
|
||||
backtrace
|
||||
end
|
||||
|
||||
def noise(backtrace)
|
||||
@silencers.each do |s|
|
||||
backtrace = backtrace.select { |line| s.call(line) }
|
||||
end
|
||||
|
||||
backtrace
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,33 +1,54 @@
|
||||
require 'active_support/deprecation'
|
||||
|
||||
begin
|
||||
require 'base64'
|
||||
rescue LoadError
|
||||
end
|
||||
# The Base64 module isn't available in earlier versions of Ruby 1.9.
|
||||
module Base64
|
||||
# Encodes a string to its base 64 representation. Each 60 characters of
|
||||
# output is separated by a newline character.
|
||||
#
|
||||
# ActiveSupport::Base64.encode64("Original unencoded string")
|
||||
# # => "T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw==\n"
|
||||
def self.encode64(data)
|
||||
[data].pack("m")
|
||||
end
|
||||
|
||||
module ActiveSupport
|
||||
if defined? ::Base64
|
||||
Base64 = ::Base64
|
||||
else
|
||||
# Base64 provides utility methods for encoding and de-coding binary data
|
||||
# using a base 64 representation. A base 64 representation of binary data
|
||||
# consists entirely of printable US-ASCII characters. The Base64 module
|
||||
# is included in Ruby 1.8, but has been removed in Ruby 1.9.
|
||||
module Base64
|
||||
# Encodes a string to its base 64 representation. Each 60 characters of
|
||||
# output is separated by a newline character.
|
||||
#
|
||||
# ActiveSupport::Base64.encode64("Original unencoded string")
|
||||
# # => "T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw==\n"
|
||||
def self.encode64(data)
|
||||
[data].pack("m")
|
||||
end
|
||||
|
||||
# Decodes a base 64 encoded string to its original representation.
|
||||
#
|
||||
# ActiveSupport::Base64.decode64("T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw==")
|
||||
# # => "Original unencoded string"
|
||||
def self.decode64(data)
|
||||
data.unpack("m").first
|
||||
end
|
||||
# Decodes a base 64 encoded string to its original representation.
|
||||
#
|
||||
# ActiveSupport::Base64.decode64("T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw==")
|
||||
# # => "Original unencoded string"
|
||||
def self.decode64(data)
|
||||
data.unpack("m").first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unless Base64.respond_to?(:strict_encode64)
|
||||
# Included in Ruby 1.9
|
||||
def Base64.strict_encode64(value)
|
||||
encode64(value).gsub(/\n/, '')
|
||||
end
|
||||
end
|
||||
|
||||
module ActiveSupport
|
||||
module Base64
|
||||
def self.encode64(value)
|
||||
ActiveSupport::Deprecation.warn "ActiveSupport::Base64.encode64 " \
|
||||
"is deprecated. Use Base64.encode64 instead", caller
|
||||
::Base64.encode64(value)
|
||||
end
|
||||
|
||||
def self.decode64(value)
|
||||
ActiveSupport::Deprecation.warn "ActiveSupport::Base64.decode64 " \
|
||||
"is deprecated. Use Base64.decode64 instead", caller
|
||||
::Base64.decode64(value)
|
||||
end
|
||||
|
||||
def self.encode64s(value)
|
||||
ActiveSupport::Deprecation.warn "ActiveSupport::Base64.encode64s " \
|
||||
"is deprecated. Use Base64.strict_encode64 instead", caller
|
||||
::Base64.strict_encode64(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
55
activesupport/lib/active_support/benchmarkable.rb
Normal file
55
activesupport/lib/active_support/benchmarkable.rb
Normal file
@@ -0,0 +1,55 @@
|
||||
require 'active_support/core_ext/benchmark'
|
||||
require 'active_support/core_ext/hash/keys'
|
||||
|
||||
module ActiveSupport
|
||||
module Benchmarkable
|
||||
# Allows you to measure the execution time of a block in a template and records the result to
|
||||
# the log. Wrap this block around expensive operations or possible bottlenecks to get a time
|
||||
# reading for the operation. For example, let's say you thought your file processing method
|
||||
# was taking too long; you could wrap it in a benchmark block.
|
||||
#
|
||||
# <% benchmark "Process data files" do %>
|
||||
# <%= expensive_files_operation %>
|
||||
# <% end %>
|
||||
#
|
||||
# That would add something like "Process data files (345.2ms)" to the log, which you can then
|
||||
# use to compare timings when optimizing your code.
|
||||
#
|
||||
# You may give an optional logger level (:debug, :info, :warn, :error) as the :level option.
|
||||
# The default logger level value is :info.
|
||||
#
|
||||
# <% benchmark "Low-level files", :level => :debug do %>
|
||||
# <%= lowlevel_files_operation %>
|
||||
# <% end %>
|
||||
#
|
||||
# Finally, you can pass true as the third argument to silence all log activity (other than the
|
||||
# timing information) from inside the block. This is great for boiling down a noisy block to
|
||||
# just a single statement that produces one log line:
|
||||
#
|
||||
# <% benchmark "Process data files", :level => :info, :silence => true do %>
|
||||
# <%= expensive_and_chatty_files_operation %>
|
||||
# <% end %>
|
||||
def benchmark(message = "Benchmarking", options = {})
|
||||
if logger
|
||||
options.assert_valid_keys(:level, :silence)
|
||||
options[:level] ||= :info
|
||||
|
||||
result = nil
|
||||
ms = Benchmark.ms { result = options[:silence] ? silence { yield } : yield }
|
||||
logger.send(options[:level], '%s (%.1fms)' % [ message, ms ])
|
||||
result
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Silence the logger during the execution of the block.
|
||||
#
|
||||
def silence
|
||||
old_logger_level, logger.level = logger.level, ::Logger::ERROR if logger
|
||||
yield
|
||||
ensure
|
||||
logger.level = old_logger_level if logger
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,9 @@
|
||||
require 'thread'
|
||||
require 'logger'
|
||||
require 'active_support/core_ext/logger'
|
||||
require 'active_support/core_ext/class/attribute_accessors'
|
||||
require 'active_support/deprecation'
|
||||
require 'fileutils'
|
||||
|
||||
module ActiveSupport
|
||||
# Inspired by the buffered logger idea by Ezra
|
||||
@@ -25,62 +30,69 @@ module ActiveSupport
|
||||
def silence(temporary_level = ERROR)
|
||||
if silencer
|
||||
begin
|
||||
old_logger_level, self.level = level, temporary_level
|
||||
yield self
|
||||
logger = self.class.new @log_dest.dup, temporary_level
|
||||
yield logger
|
||||
ensure
|
||||
self.level = old_logger_level
|
||||
logger.close
|
||||
end
|
||||
else
|
||||
yield self
|
||||
end
|
||||
end
|
||||
deprecate :silence
|
||||
|
||||
attr_accessor :level
|
||||
attr_reader :auto_flushing
|
||||
deprecate :auto_flushing
|
||||
|
||||
def initialize(log, level = DEBUG)
|
||||
@level = level
|
||||
@buffer = {}
|
||||
@auto_flushing = 1
|
||||
@guard = Mutex.new
|
||||
@log_dest = log
|
||||
|
||||
if log.respond_to?(:write)
|
||||
@log = log
|
||||
elsif File.exist?(log)
|
||||
@log = open(log, (File::WRONLY | File::APPEND))
|
||||
@log.sync = true
|
||||
else
|
||||
FileUtils.mkdir_p(File.dirname(log))
|
||||
@log = open(log, (File::WRONLY | File::APPEND | File::CREAT))
|
||||
@log.sync = true
|
||||
@log.write("# Logfile created on %s" % [Time.now.to_s])
|
||||
unless log.respond_to?(:write)
|
||||
unless File.exist?(File.dirname(log))
|
||||
ActiveSupport::Deprecation.warn(<<-eowarn)
|
||||
Automatic directory creation for '#{log}' is deprecated. Please make sure the directory for your log file exists before creating the logger.
|
||||
eowarn
|
||||
FileUtils.mkdir_p(File.dirname(log))
|
||||
end
|
||||
end
|
||||
|
||||
@log = open_logfile log
|
||||
self.level = level
|
||||
end
|
||||
|
||||
def open_log(log, mode)
|
||||
open(log, mode).tap do |open_log|
|
||||
open_log.set_encoding(Encoding::BINARY) if open_log.respond_to?(:set_encoding)
|
||||
open_log.sync = true
|
||||
end
|
||||
end
|
||||
deprecate :open_log
|
||||
|
||||
def level
|
||||
@log.level
|
||||
end
|
||||
|
||||
def level=(l)
|
||||
@log.level = l
|
||||
end
|
||||
|
||||
def add(severity, message = nil, progname = nil, &block)
|
||||
return if @level > severity
|
||||
message = (message || (block && block.call) || progname).to_s
|
||||
# If a newline is necessary then create a new message ending with a newline.
|
||||
# Ensures that the original message is not mutated.
|
||||
message = "#{message}\n" unless message[-1] == ?\n
|
||||
if message.respond_to?(:force_encoding)
|
||||
buffer << message.force_encoding(Encoding.default_external)
|
||||
else
|
||||
buffer << message
|
||||
end
|
||||
auto_flush
|
||||
message
|
||||
@log.add(severity, message, progname, &block)
|
||||
end
|
||||
|
||||
for severity in Severity.constants
|
||||
# Dynamically add methods such as:
|
||||
# def info
|
||||
# def warn
|
||||
# def debug
|
||||
Severity.constants.each do |severity|
|
||||
class_eval <<-EOT, __FILE__, __LINE__ + 1
|
||||
def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block)
|
||||
add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block)
|
||||
end # end
|
||||
#
|
||||
def #{severity.downcase}? # def debug?
|
||||
#{severity} >= @level # DEBUG >= @level
|
||||
end # end
|
||||
def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block)
|
||||
add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block)
|
||||
end # end
|
||||
|
||||
def #{severity.downcase}? # def debug?
|
||||
#{severity} >= level # DEBUG >= level
|
||||
end # end
|
||||
EOT
|
||||
end
|
||||
|
||||
@@ -89,45 +101,25 @@ module ActiveSupport
|
||||
# never auto-flush. If you turn auto-flushing off, be sure to regularly
|
||||
# flush the log yourself -- it will eat up memory until you do.
|
||||
def auto_flushing=(period)
|
||||
@auto_flushing =
|
||||
case period
|
||||
when true; 1
|
||||
when false, nil, 0; MAX_BUFFER_SIZE
|
||||
when Integer; period
|
||||
else raise ArgumentError, "Unrecognized auto_flushing period: #{period.inspect}"
|
||||
end
|
||||
end
|
||||
deprecate :auto_flushing=
|
||||
|
||||
def flush
|
||||
@guard.synchronize do
|
||||
unless buffer.empty?
|
||||
old_buffer = buffer
|
||||
@log.write(old_buffer.join)
|
||||
end
|
||||
end
|
||||
deprecate :flush
|
||||
|
||||
# Important to do this even if buffer was empty or else @buffer will
|
||||
# accumulate empty arrays for each request where nothing was logged.
|
||||
clear_buffer
|
||||
end
|
||||
def respond_to?(method, include_private = false)
|
||||
return false if method.to_s == "flush"
|
||||
super
|
||||
end
|
||||
|
||||
def close
|
||||
flush
|
||||
@log.close if @log.respond_to?(:close)
|
||||
@log = nil
|
||||
@log.close
|
||||
end
|
||||
|
||||
protected
|
||||
def auto_flush
|
||||
flush if buffer.size >= @auto_flushing
|
||||
end
|
||||
|
||||
def buffer
|
||||
@buffer[Thread.current] ||= []
|
||||
end
|
||||
|
||||
def clear_buffer
|
||||
@buffer.delete(Thread.current)
|
||||
end
|
||||
private
|
||||
def open_logfile(log)
|
||||
Logger.new log
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
6
activesupport/lib/active_support/builder.rb
Normal file
6
activesupport/lib/active_support/builder.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
begin
|
||||
require 'builder'
|
||||
rescue LoadError => e
|
||||
$stderr.puts "You don't have builder installed in your application. Please add it to your Gemfile and run bundle install"
|
||||
raise e
|
||||
end
|
||||
@@ -1,76 +1,99 @@
|
||||
require 'benchmark'
|
||||
require 'zlib'
|
||||
require 'active_support/core_ext/array/extract_options'
|
||||
require 'active_support/core_ext/array/wrap'
|
||||
require 'active_support/core_ext/benchmark'
|
||||
require 'active_support/core_ext/exception'
|
||||
require 'active_support/core_ext/class/attribute_accessors'
|
||||
require 'active_support/core_ext/numeric/bytes'
|
||||
require 'active_support/core_ext/numeric/time'
|
||||
require 'active_support/core_ext/object/to_param'
|
||||
require 'active_support/core_ext/string/inflections'
|
||||
|
||||
module ActiveSupport
|
||||
# See ActiveSupport::Cache::Store for documentation.
|
||||
module Cache
|
||||
autoload :FileStore, 'active_support/cache/file_store'
|
||||
autoload :MemoryStore, 'active_support/cache/memory_store'
|
||||
autoload :SynchronizedMemoryStore, 'active_support/cache/synchronized_memory_store'
|
||||
autoload :DRbStore, 'active_support/cache/drb_store'
|
||||
autoload :MemCacheStore, 'active_support/cache/mem_cache_store'
|
||||
autoload :CompressedMemCacheStore, 'active_support/cache/compressed_mem_cache_store'
|
||||
autoload :NullStore, 'active_support/cache/null_store'
|
||||
|
||||
# These options mean something to all cache implementations. Individual cache
|
||||
# implementations may support additional options.
|
||||
UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl]
|
||||
|
||||
module Strategy
|
||||
autoload :LocalCache, 'active_support/cache/strategy/local_cache'
|
||||
end
|
||||
|
||||
# Creates a new CacheStore object according to the given options.
|
||||
#
|
||||
# If no arguments are passed to this method, then a new
|
||||
# ActiveSupport::Cache::MemoryStore object will be returned.
|
||||
#
|
||||
# If you pass a Symbol as the first argument, then a corresponding cache
|
||||
# store class under the ActiveSupport::Cache namespace will be created.
|
||||
# For example:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:memory_store)
|
||||
# # => returns a new ActiveSupport::Cache::MemoryStore object
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:drb_store)
|
||||
# # => returns a new ActiveSupport::Cache::DRbStore object
|
||||
#
|
||||
# Any additional arguments will be passed to the corresponding cache store
|
||||
# class's constructor:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:file_store, "/tmp/cache")
|
||||
# # => same as: ActiveSupport::Cache::FileStore.new("/tmp/cache")
|
||||
#
|
||||
# If the first argument is not a Symbol, then it will simply be returned:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(MyOwnCacheStore.new)
|
||||
# # => returns MyOwnCacheStore.new
|
||||
def self.lookup_store(*store_option)
|
||||
store, *parameters = *([ store_option ].flatten)
|
||||
class << self
|
||||
# Creates a new CacheStore object according to the given options.
|
||||
#
|
||||
# If no arguments are passed to this method, then a new
|
||||
# ActiveSupport::Cache::MemoryStore object will be returned.
|
||||
#
|
||||
# If you pass a Symbol as the first argument, then a corresponding cache
|
||||
# store class under the ActiveSupport::Cache namespace will be created.
|
||||
# For example:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:memory_store)
|
||||
# # => returns a new ActiveSupport::Cache::MemoryStore object
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:mem_cache_store)
|
||||
# # => returns a new ActiveSupport::Cache::MemCacheStore object
|
||||
#
|
||||
# Any additional arguments will be passed to the corresponding cache store
|
||||
# class's constructor:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:file_store, "/tmp/cache")
|
||||
# # => same as: ActiveSupport::Cache::FileStore.new("/tmp/cache")
|
||||
#
|
||||
# If the first argument is not a Symbol, then it will simply be returned:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(MyOwnCacheStore.new)
|
||||
# # => returns MyOwnCacheStore.new
|
||||
def lookup_store(*store_option)
|
||||
store, *parameters = *Array.wrap(store_option).flatten
|
||||
|
||||
case store
|
||||
when Symbol
|
||||
store_class_name = (store == :drb_store ? "DRbStore" : store.to_s.camelize)
|
||||
store_class = ActiveSupport::Cache.const_get(store_class_name)
|
||||
store_class.new(*parameters)
|
||||
when nil
|
||||
ActiveSupport::Cache::MemoryStore.new
|
||||
else
|
||||
store
|
||||
end
|
||||
end
|
||||
|
||||
def self.expand_cache_key(key, namespace = nil)
|
||||
expanded_cache_key = namespace ? "#{namespace}/" : ""
|
||||
|
||||
if ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
|
||||
expanded_cache_key << "#{ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]}/"
|
||||
case store
|
||||
when Symbol
|
||||
store_class_name = store.to_s.camelize
|
||||
store_class =
|
||||
begin
|
||||
require "active_support/cache/#{store}"
|
||||
rescue LoadError => e
|
||||
raise "Could not find cache store adapter for #{store} (#{e})"
|
||||
else
|
||||
ActiveSupport::Cache.const_get(store_class_name)
|
||||
end
|
||||
store_class.new(*parameters)
|
||||
when nil
|
||||
ActiveSupport::Cache::MemoryStore.new
|
||||
else
|
||||
store
|
||||
end
|
||||
end
|
||||
|
||||
expanded_cache_key << case
|
||||
when key.respond_to?(:cache_key)
|
||||
key.cache_key
|
||||
when key.is_a?(Array)
|
||||
key.collect { |element| expand_cache_key(element) }.to_param
|
||||
when key
|
||||
key.to_param
|
||||
def expand_cache_key(key, namespace = nil)
|
||||
expanded_cache_key = namespace ? "#{namespace}/" : ""
|
||||
|
||||
if prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
|
||||
expanded_cache_key << "#{prefix}/"
|
||||
end
|
||||
|
||||
expanded_cache_key << retrieve_cache_key(key)
|
||||
expanded_cache_key
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def retrieve_cache_key(key)
|
||||
case
|
||||
when key.respond_to?(:cache_key) then key.cache_key
|
||||
when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
|
||||
else key.to_param
|
||||
end.to_s
|
||||
|
||||
expanded_cache_key
|
||||
end
|
||||
end
|
||||
|
||||
# An abstract cache store class. There are multiple cache store
|
||||
@@ -79,28 +102,64 @@ module ActiveSupport
|
||||
# ActiveSupport::Cache::MemCacheStore. MemCacheStore is currently the most
|
||||
# popular cache store for large production websites.
|
||||
#
|
||||
# ActiveSupport::Cache::Store is meant for caching strings. Some cache
|
||||
# store implementations, like MemoryStore, are able to cache arbitrary
|
||||
# Ruby objects, but don't count on every cache store to be able to do that.
|
||||
# Some implementations may not support all methods beyond the basic cache
|
||||
# methods of +fetch+, +write+, +read+, +exist?+, and +delete+.
|
||||
#
|
||||
# ActiveSupport::Cache::Store can store any serializable Ruby object.
|
||||
#
|
||||
# cache = ActiveSupport::Cache::MemoryStore.new
|
||||
#
|
||||
#
|
||||
# cache.read("city") # => nil
|
||||
# cache.write("city", "Duckburgh")
|
||||
# cache.read("city") # => "Duckburgh"
|
||||
#
|
||||
# Keys are always translated into Strings and are case sensitive. When an
|
||||
# object is specified as a key and has a +cache_key+ method defined, this
|
||||
# method will be called to define the key. Otherwise, the +to_param+
|
||||
# method will be called. Hashes and Arrays can also be used as keys. The
|
||||
# elements will be delimited by slashes, and the elements within a Hash
|
||||
# will be sorted by key so they are consistent.
|
||||
#
|
||||
# cache.read("city") == cache.read(:city) # => true
|
||||
#
|
||||
# Nil values can be cached.
|
||||
#
|
||||
# If your cache is on a shared infrastructure, you can define a namespace
|
||||
# for your cache entries. If a namespace is defined, it will be prefixed on
|
||||
# to every key. The namespace can be either a static value or a Proc. If it
|
||||
# is a Proc, it will be invoked when each key is evaluated so that you can
|
||||
# use application logic to invalidate keys.
|
||||
#
|
||||
# cache.namespace = lambda { @last_mod_time } # Set the namespace to a variable
|
||||
# @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
|
||||
#
|
||||
#
|
||||
# Caches can also store values in a compressed format to save space and
|
||||
# reduce time spent sending data. Since there is overhead, values must be
|
||||
# large enough to warrant compression. To turn on compression either pass
|
||||
# <tt>:compress => true</tt> in the initializer or as an option to +fetch+
|
||||
# or +write+. To specify the threshold at which to compress values, set the
|
||||
# <tt>:compress_threshold</tt> option. The default threshold is 16K.
|
||||
class Store
|
||||
cattr_accessor :logger
|
||||
|
||||
attr_reader :silence, :logger_off
|
||||
cattr_accessor :logger, :instance_writer => true
|
||||
|
||||
attr_reader :silence, :options
|
||||
alias :silence? :silence
|
||||
|
||||
# Create a new cache. The options will be passed to any write method calls except
|
||||
# for :namespace which can be used to set the global namespace for the cache.
|
||||
def initialize(options = nil)
|
||||
@options = options ? options.dup : {}
|
||||
end
|
||||
|
||||
# Silence the logger.
|
||||
def silence!
|
||||
@silence = true
|
||||
self
|
||||
end
|
||||
|
||||
alias silence? silence
|
||||
alias logger_off? logger_off
|
||||
|
||||
# Silence the logger within a block.
|
||||
def mute
|
||||
previous_silence, @silence = defined?(@silence) && @silence, true
|
||||
yield
|
||||
@@ -108,18 +167,27 @@ module ActiveSupport
|
||||
@silence = previous_silence
|
||||
end
|
||||
|
||||
# Set to true if cache stores should be instrumented. Default is false.
|
||||
def self.instrument=(boolean)
|
||||
Thread.current[:instrument_cache_store] = boolean
|
||||
end
|
||||
|
||||
def self.instrument
|
||||
Thread.current[:instrument_cache_store] || false
|
||||
end
|
||||
|
||||
# Fetches data from the cache, using the given key. If there is data in
|
||||
# the cache with the given key, then that data is returned.
|
||||
#
|
||||
# If there is no such data in the cache (a cache miss occurred), then
|
||||
# then nil will be returned. However, if a block has been passed, then
|
||||
# that block will be run in the event of a cache miss. The return value
|
||||
# of the block will be written to the cache under the given cache key,
|
||||
# and that return value will be returned.
|
||||
# If there is no such data in the cache (a cache miss), then nil will be
|
||||
# returned. However, if a block has been passed, that block will be run
|
||||
# in the event of a cache miss. The return value of the block will be
|
||||
# written to the cache under the given cache key, and that return value
|
||||
# will be returned.
|
||||
#
|
||||
# cache.write("today", "Monday")
|
||||
# cache.fetch("today") # => "Monday"
|
||||
#
|
||||
#
|
||||
# cache.fetch("city") # => nil
|
||||
# cache.fetch("city") do
|
||||
# "Duckburgh"
|
||||
@@ -132,42 +200,107 @@ module ActiveSupport
|
||||
# cache.write("today", "Monday")
|
||||
# cache.fetch("today", :force => true) # => nil
|
||||
#
|
||||
# Setting <tt>:compress</tt> will store a large cache entry set by the call
|
||||
# in a compressed format.
|
||||
#
|
||||
#
|
||||
# Setting <tt>:expires_in</tt> will set an expiration time on the cache.
|
||||
# All caches support auto-expiring content after a specified number of
|
||||
# seconds. This value can be specified as an option to the constructor
|
||||
# (in which case all entries will be affected), or it can be supplied to
|
||||
# the +fetch+ or +write+ method to effect just one entry.
|
||||
#
|
||||
# cache = ActiveSupport::Cache::MemoryStore.new(:expires_in => 5.minutes)
|
||||
# cache.write(key, value, :expires_in => 1.minute) # Set a lower value for one entry
|
||||
#
|
||||
# Setting <tt>:race_condition_ttl</tt> is very useful in situations where a cache entry
|
||||
# is used very frequently and is under heavy load. If a cache expires and due to heavy load
|
||||
# seven different processes will try to read data natively and then they all will try to
|
||||
# write to cache. To avoid that case the first process to find an expired cache entry will
|
||||
# bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>. Yes
|
||||
# this process is extending the time for a stale value by another few seconds. Because
|
||||
# of extended life of the previous cache, other processes will continue to use slightly
|
||||
# stale data for a just a big longer. In the meantime that first process will go ahead
|
||||
# and will write into cache the new value. After that all the processes will start
|
||||
# getting new value. The key is to keep <tt>:race_condition_ttl</tt> small.
|
||||
#
|
||||
# If the process regenerating the entry errors out, the entry will be regenerated
|
||||
# after the specified number of seconds. Also note that the life of stale cache is
|
||||
# extended only if it expired recently. Otherwise a new value is generated and
|
||||
# <tt>:race_condition_ttl</tt> does not play any role.
|
||||
#
|
||||
# # Set all values to expire after one minute.
|
||||
# cache = ActiveSupport::Cache::MemoryStore.new(:expires_in => 1.minute)
|
||||
#
|
||||
# cache.write("foo", "original value")
|
||||
# val_1 = nil
|
||||
# val_2 = nil
|
||||
# sleep 60
|
||||
#
|
||||
# Thread.new do
|
||||
# val_1 = cache.fetch("foo", :race_condition_ttl => 10) do
|
||||
# sleep 1
|
||||
# "new value 1"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Thread.new do
|
||||
# val_2 = cache.fetch("foo", :race_condition_ttl => 10) do
|
||||
# "new value 2"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # val_1 => "new value 1"
|
||||
# # val_2 => "original value"
|
||||
# # sleep 10 # First thread extend the life of cache by another 10 seconds
|
||||
# # cache.fetch("foo") => "new value 1"
|
||||
#
|
||||
# Other options will be handled by the specific cache store implementation.
|
||||
# Internally, #fetch calls #read, and calls #write on a cache miss.
|
||||
# Internally, #fetch calls #read_entry, and calls #write_entry on a cache miss.
|
||||
# +options+ will be passed to the #read and #write calls.
|
||||
#
|
||||
# For example, MemCacheStore's #write method supports the +:expires_in+
|
||||
# option, which tells the memcached server to automatically expire the
|
||||
# cache item after a certain period. We can use this option with #fetch
|
||||
# too:
|
||||
# For example, MemCacheStore's #write method supports the +:raw+
|
||||
# option, which tells the memcached server to store all values as strings.
|
||||
# We can use this option with #fetch too:
|
||||
#
|
||||
# cache = ActiveSupport::Cache::MemCacheStore.new
|
||||
# cache.fetch("foo", :force => true, :expires_in => 5.seconds) do
|
||||
# "bar"
|
||||
# cache.fetch("foo", :force => true, :raw => true) do
|
||||
# :bar
|
||||
# end
|
||||
# cache.fetch("foo") # => "bar"
|
||||
# sleep(6)
|
||||
# cache.fetch("foo") # => nil
|
||||
def fetch(key, options = {})
|
||||
@logger_off = true
|
||||
if !options[:force] && value = read(key, options)
|
||||
@logger_off = false
|
||||
log("hit", key, options)
|
||||
value
|
||||
elsif block_given?
|
||||
@logger_off = false
|
||||
log("miss", key, options)
|
||||
def fetch(name, options = nil)
|
||||
if block_given?
|
||||
options = merged_options(options)
|
||||
key = namespaced_key(name, options)
|
||||
unless options[:force]
|
||||
entry = instrument(:read, name, options) do |payload|
|
||||
payload[:super_operation] = :fetch if payload
|
||||
read_entry(key, options)
|
||||
end
|
||||
end
|
||||
if entry && entry.expired?
|
||||
race_ttl = options[:race_condition_ttl].to_f
|
||||
if race_ttl and Time.now.to_f - entry.expires_at <= race_ttl
|
||||
entry.expires_at = Time.now + race_ttl
|
||||
write_entry(key, entry, :expires_in => race_ttl * 2)
|
||||
else
|
||||
delete_entry(key, options)
|
||||
end
|
||||
entry = nil
|
||||
end
|
||||
|
||||
value = nil
|
||||
ms = Benchmark.ms { value = yield }
|
||||
|
||||
@logger_off = true
|
||||
write(key, value, options)
|
||||
@logger_off = false
|
||||
|
||||
log('write (will save %.2fms)' % ms, key, nil)
|
||||
|
||||
value
|
||||
if entry
|
||||
instrument(:fetch_hit, name, options) { |payload| }
|
||||
entry.value
|
||||
else
|
||||
result = instrument(:generate, name, options) do |payload|
|
||||
yield
|
||||
end
|
||||
write(name, result, options)
|
||||
result
|
||||
end
|
||||
else
|
||||
read(name, options)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -175,73 +308,330 @@ module ActiveSupport
|
||||
# the cache with the given key, then that data is returned. Otherwise,
|
||||
# nil is returned.
|
||||
#
|
||||
# You may also specify additional options via the +options+ argument.
|
||||
# The specific cache store implementation will decide what to do with
|
||||
# +options+.
|
||||
def read(key, options = nil)
|
||||
log("read", key, options)
|
||||
end
|
||||
|
||||
# Writes the given value to the cache, with the given key.
|
||||
#
|
||||
# You may also specify additional options via the +options+ argument.
|
||||
# The specific cache store implementation will decide what to do with
|
||||
# +options+.
|
||||
#
|
||||
# For example, MemCacheStore supports the +:expires_in+ option, which
|
||||
# tells the memcached server to automatically expire the cache item after
|
||||
# a certain period:
|
||||
#
|
||||
# cache = ActiveSupport::Cache::MemCacheStore.new
|
||||
# cache.write("foo", "bar", :expires_in => 5.seconds)
|
||||
# cache.read("foo") # => "bar"
|
||||
# sleep(6)
|
||||
# cache.read("foo") # => nil
|
||||
def write(key, value, options = nil)
|
||||
log("write", key, options)
|
||||
end
|
||||
|
||||
def delete(key, options = nil)
|
||||
log("delete", key, options)
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
log("delete matched", matcher.inspect, options)
|
||||
end
|
||||
|
||||
def exist?(key, options = nil)
|
||||
log("exist?", key, options)
|
||||
end
|
||||
|
||||
def increment(key, amount = 1)
|
||||
log("incrementing", key, amount)
|
||||
if num = read(key)
|
||||
write(key, num + amount)
|
||||
else
|
||||
nil
|
||||
# Options are passed to the underlying cache implementation.
|
||||
def read(name, options = nil)
|
||||
options = merged_options(options)
|
||||
key = namespaced_key(name, options)
|
||||
instrument(:read, name, options) do |payload|
|
||||
entry = read_entry(key, options)
|
||||
if entry
|
||||
if entry.expired?
|
||||
delete_entry(key, options)
|
||||
payload[:hit] = false if payload
|
||||
nil
|
||||
else
|
||||
payload[:hit] = true if payload
|
||||
entry.value
|
||||
end
|
||||
else
|
||||
payload[:hit] = false if payload
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def decrement(key, amount = 1)
|
||||
log("decrementing", key, amount)
|
||||
if num = read(key)
|
||||
write(key, num - amount)
|
||||
# Read multiple values at once from the cache. Options can be passed
|
||||
# in the last argument.
|
||||
#
|
||||
# Some cache implementation may optimize this method.
|
||||
#
|
||||
# Returns a hash mapping the names provided to the values found.
|
||||
def read_multi(*names)
|
||||
options = names.extract_options!
|
||||
options = merged_options(options)
|
||||
results = {}
|
||||
names.each do |name|
|
||||
key = namespaced_key(name, options)
|
||||
entry = read_entry(key, options)
|
||||
if entry
|
||||
if entry.expired?
|
||||
delete_entry(key, options)
|
||||
else
|
||||
results[name] = entry.value
|
||||
end
|
||||
end
|
||||
end
|
||||
results
|
||||
end
|
||||
|
||||
# Writes the value to the cache, with the key.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
def write(name, value, options = nil)
|
||||
options = merged_options(options)
|
||||
instrument(:write, name, options) do |payload|
|
||||
entry = Entry.new(value, options)
|
||||
write_entry(namespaced_key(name, options), entry, options)
|
||||
end
|
||||
end
|
||||
|
||||
# Deletes an entry in the cache. Returns +true+ if an entry is deleted.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
def delete(name, options = nil)
|
||||
options = merged_options(options)
|
||||
instrument(:delete, name) do |payload|
|
||||
delete_entry(namespaced_key(name, options), options)
|
||||
end
|
||||
end
|
||||
|
||||
# Return true if the cache contains an entry for the given key.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
def exist?(name, options = nil)
|
||||
options = merged_options(options)
|
||||
instrument(:exist?, name) do |payload|
|
||||
entry = read_entry(namespaced_key(name, options), options)
|
||||
if entry && !entry.expired?
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Delete all entries with keys matching the pattern.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
#
|
||||
# All implementations may not support this method.
|
||||
def delete_matched(matcher, options = nil)
|
||||
raise NotImplementedError.new("#{self.class.name} does not support delete_matched")
|
||||
end
|
||||
|
||||
# Increment an integer value in the cache.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
#
|
||||
# All implementations may not support this method.
|
||||
def increment(name, amount = 1, options = nil)
|
||||
raise NotImplementedError.new("#{self.class.name} does not support increment")
|
||||
end
|
||||
|
||||
# Increment an integer value in the cache.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
#
|
||||
# All implementations may not support this method.
|
||||
def decrement(name, amount = 1, options = nil)
|
||||
raise NotImplementedError.new("#{self.class.name} does not support decrement")
|
||||
end
|
||||
|
||||
# Cleanup the cache by removing expired entries.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
#
|
||||
# All implementations may not support this method.
|
||||
def cleanup(options = nil)
|
||||
raise NotImplementedError.new("#{self.class.name} does not support cleanup")
|
||||
end
|
||||
|
||||
# Clear the entire cache. Be careful with this method since it could
|
||||
# affect other processes if shared cache is being used.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
#
|
||||
# All implementations may not support this method.
|
||||
def clear(options = nil)
|
||||
raise NotImplementedError.new("#{self.class.name} does not support clear")
|
||||
end
|
||||
|
||||
protected
|
||||
# Add the namespace defined in the options to a pattern designed to match keys.
|
||||
# Implementations that support delete_matched should call this method to translate
|
||||
# a pattern that matches names into one that matches namespaced keys.
|
||||
def key_matcher(pattern, options)
|
||||
prefix = options[:namespace].is_a?(Proc) ? options[:namespace].call : options[:namespace]
|
||||
if prefix
|
||||
source = pattern.source
|
||||
if source.start_with?('^')
|
||||
source = source[1, source.length]
|
||||
else
|
||||
source = ".*#{source[0, source.length]}"
|
||||
end
|
||||
Regexp.new("^#{Regexp.escape(prefix)}:#{source}", pattern.options)
|
||||
else
|
||||
pattern
|
||||
end
|
||||
end
|
||||
|
||||
# Read an entry from the cache implementation. Subclasses must implement this method.
|
||||
def read_entry(key, options) # :nodoc:
|
||||
raise NotImplementedError.new
|
||||
end
|
||||
|
||||
# Write an entry to the cache implementation. Subclasses must implement this method.
|
||||
def write_entry(key, entry, options) # :nodoc:
|
||||
raise NotImplementedError.new
|
||||
end
|
||||
|
||||
# Delete an entry from the cache implementation. Subclasses must implement this method.
|
||||
def delete_entry(key, options) # :nodoc:
|
||||
raise NotImplementedError.new
|
||||
end
|
||||
|
||||
private
|
||||
# Merge the default options with ones specific to a method call.
|
||||
def merged_options(call_options) # :nodoc:
|
||||
if call_options
|
||||
options.merge(call_options)
|
||||
else
|
||||
options.dup
|
||||
end
|
||||
end
|
||||
|
||||
# Expand key to be a consistent string value. Invoke +cache_key+ if
|
||||
# object responds to +cache_key+. Otherwise, to_param method will be
|
||||
# called. If the key is a Hash, then keys will be sorted alphabetically.
|
||||
def expanded_key(key) # :nodoc:
|
||||
return key.cache_key.to_s if key.respond_to?(:cache_key)
|
||||
|
||||
case key
|
||||
when Array
|
||||
if key.size > 1
|
||||
key = key.collect{|element| expanded_key(element)}
|
||||
else
|
||||
key = key.first
|
||||
end
|
||||
when Hash
|
||||
key = key.sort_by { |k,_| k.to_s }.collect{|k,v| "#{k}=#{v}"}
|
||||
end
|
||||
|
||||
key.to_param
|
||||
end
|
||||
|
||||
# Prefix a key with the namespace. Namespace and key will be delimited with a colon.
|
||||
def namespaced_key(key, options)
|
||||
key = expanded_key(key)
|
||||
namespace = options[:namespace] if options
|
||||
prefix = namespace.is_a?(Proc) ? namespace.call : namespace
|
||||
key = "#{prefix}:#{key}" if prefix
|
||||
key
|
||||
end
|
||||
|
||||
def instrument(operation, key, options = nil)
|
||||
log(operation, key, options)
|
||||
|
||||
if self.class.instrument
|
||||
payload = { :key => key }
|
||||
payload.merge!(options) if options.is_a?(Hash)
|
||||
ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }
|
||||
else
|
||||
yield(nil)
|
||||
end
|
||||
end
|
||||
|
||||
def log(operation, key, options = nil)
|
||||
return unless logger && logger.debug? && !silence?
|
||||
logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}")
|
||||
end
|
||||
end
|
||||
|
||||
# Entry that is put into caches. It supports expiration time on entries and can compress values
|
||||
# to save space in the cache.
|
||||
class Entry
|
||||
attr_reader :created_at, :expires_in
|
||||
|
||||
DEFAULT_COMPRESS_LIMIT = 16.kilobytes
|
||||
|
||||
class << self
|
||||
# Create an entry with internal attributes set. This method is intended to be
|
||||
# used by implementations that store cache entries in a native format instead
|
||||
# of as serialized Ruby objects.
|
||||
def create(raw_value, created_at, options = {})
|
||||
entry = new(nil)
|
||||
entry.instance_variable_set(:@value, raw_value)
|
||||
entry.instance_variable_set(:@created_at, created_at.to_f)
|
||||
entry.instance_variable_set(:@compressed, options[:compressed])
|
||||
entry.instance_variable_set(:@expires_in, options[:expires_in])
|
||||
entry
|
||||
end
|
||||
end
|
||||
|
||||
# Create a new cache entry for the specified value. Options supported are
|
||||
# +:compress+, +:compress_threshold+, and +:expires_in+.
|
||||
def initialize(value, options = {})
|
||||
@compressed = false
|
||||
@expires_in = options[:expires_in]
|
||||
@expires_in = @expires_in.to_f if @expires_in
|
||||
@created_at = Time.now.to_f
|
||||
if value.nil?
|
||||
@value = nil
|
||||
else
|
||||
nil
|
||||
@value = Marshal.dump(value)
|
||||
if should_compress?(@value, options)
|
||||
@value = Zlib::Deflate.deflate(@value)
|
||||
@compressed = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Get the raw value. This value may be serialized and compressed.
|
||||
def raw_value
|
||||
@value
|
||||
end
|
||||
|
||||
# Get the value stored in the cache.
|
||||
def value
|
||||
# If the original value was exactly false @value is still true because
|
||||
# it is marshalled and eventually compressed. Both operations yield
|
||||
# strings.
|
||||
if @value
|
||||
# In rails 3.1 and earlier values in entries did not marshaled without
|
||||
# options[:compress] and if it's Numeric.
|
||||
# But after commit a263f377978fc07515b42808ebc1f7894fafaa3a
|
||||
# all values in entries are marshalled. And after that code below expects
|
||||
# that all values in entries will be marshaled (and will be strings).
|
||||
# So here we need a check for old ones.
|
||||
begin
|
||||
Marshal.load(compressed? ? Zlib::Inflate.inflate(@value) : @value)
|
||||
rescue TypeError
|
||||
compressed? ? Zlib::Inflate.inflate(@value) : @value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def compressed?
|
||||
@compressed
|
||||
end
|
||||
|
||||
# Check if the entry is expired. The +expires_in+ parameter can override the
|
||||
# value set when the entry was created.
|
||||
def expired?
|
||||
@expires_in && @created_at + @expires_in <= Time.now.to_f
|
||||
end
|
||||
|
||||
# Set a new time when the entry will expire.
|
||||
def expires_at=(time)
|
||||
if time
|
||||
@expires_in = time.to_f - @created_at
|
||||
else
|
||||
@expires_in = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Seconds since the epoch when the entry will expire.
|
||||
def expires_at
|
||||
@expires_in ? @created_at + @expires_in : nil
|
||||
end
|
||||
|
||||
# Returns the size of the cached value. This could be less than value.size
|
||||
# if the data is compressed.
|
||||
def size
|
||||
if @value.nil?
|
||||
0
|
||||
else
|
||||
@value.bytesize
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def expires_in(options)
|
||||
expires_in = options && options[:expires_in]
|
||||
|
||||
raise ":expires_in must be a number" if expires_in && !expires_in.is_a?(Numeric)
|
||||
|
||||
expires_in || 0
|
||||
end
|
||||
|
||||
def log(operation, key, options)
|
||||
logger.debug("Cache #{operation}: #{key}#{options ? " (#{options.inspect})" : ""}") if logger && !silence? && !logger_off?
|
||||
def should_compress?(serialized_value, options)
|
||||
if options[:compress]
|
||||
compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT
|
||||
return true if serialized_value.size >= compress_threshold
|
||||
end
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
class CompressedMemCacheStore < MemCacheStore
|
||||
def read(name, options = nil)
|
||||
if value = super(name, (options || {}).merge(:raw => true))
|
||||
if raw?(options)
|
||||
value
|
||||
else
|
||||
Marshal.load(ActiveSupport::Gzip.decompress(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write(name, value, options = nil)
|
||||
value = ActiveSupport::Gzip.compress(Marshal.dump(value)) unless raw?(options)
|
||||
super(name, value, (options || {}).merge(:raw => true))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
class DRbStore < MemoryStore #:nodoc:
|
||||
attr_reader :address
|
||||
|
||||
def initialize(address = 'druby://localhost:9192')
|
||||
require 'drb' unless defined?(DRbObject)
|
||||
super()
|
||||
@address = address
|
||||
@data = DRbObject.new(nil, address)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
176
activesupport/lib/active_support/cache/file_store.rb
vendored
176
activesupport/lib/active_support/cache/file_store.rb
vendored
@@ -1,64 +1,170 @@
|
||||
require 'active_support/core_ext/file/atomic'
|
||||
require 'active_support/core_ext/string/conversions'
|
||||
require 'active_support/core_ext/object/inclusion'
|
||||
require 'rack/utils'
|
||||
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# A cache store implementation which stores everything on the filesystem.
|
||||
#
|
||||
# FileStore implements the Strategy::LocalCache strategy which implements
|
||||
# an in-memory cache inside of a block.
|
||||
class FileStore < Store
|
||||
attr_reader :cache_path
|
||||
|
||||
def initialize(cache_path)
|
||||
@cache_path = cache_path
|
||||
DIR_FORMATTER = "%03X"
|
||||
FILENAME_MAX_SIZE = 230 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write)
|
||||
EXCLUDED_DIRS = ['.', '..'].freeze
|
||||
|
||||
def initialize(cache_path, options = nil)
|
||||
super(options)
|
||||
@cache_path = cache_path.to_s
|
||||
extend Strategy::LocalCache
|
||||
end
|
||||
|
||||
def read(name, options = nil)
|
||||
super
|
||||
File.open(real_file_path(name), 'rb') { |f| Marshal.load(f) } rescue nil
|
||||
def clear(options = nil)
|
||||
root_dirs = Dir.entries(cache_path).reject{|f| f.in?(EXCLUDED_DIRS)}
|
||||
FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)})
|
||||
end
|
||||
|
||||
def write(name, value, options = nil)
|
||||
super
|
||||
ensure_cache_path(File.dirname(real_file_path(name)))
|
||||
File.atomic_write(real_file_path(name), cache_path) { |f| Marshal.dump(value, f) }
|
||||
value
|
||||
rescue => e
|
||||
logger.error "Couldn't create cache directory: #{name} (#{e.message})" if logger
|
||||
def cleanup(options = nil)
|
||||
options = merged_options(options)
|
||||
each_key(options) do |key|
|
||||
entry = read_entry(key, options)
|
||||
delete_entry(key, options) if entry && entry.expired?
|
||||
end
|
||||
end
|
||||
|
||||
def delete(name, options = nil)
|
||||
super
|
||||
File.delete(real_file_path(name))
|
||||
rescue SystemCallError => e
|
||||
# If there's no cache, then there's nothing to complain about
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
super
|
||||
search_dir(@cache_path) do |f|
|
||||
if f =~ matcher
|
||||
begin
|
||||
File.delete(f)
|
||||
rescue SystemCallError => e
|
||||
# If there's no cache, then there's nothing to complain about
|
||||
end
|
||||
def increment(name, amount = 1, options = nil)
|
||||
file_name = key_file_path(namespaced_key(name, options))
|
||||
lock_file(file_name) do
|
||||
options = merged_options(options)
|
||||
if num = read(name, options)
|
||||
num = num.to_i + amount
|
||||
write(name, num, options)
|
||||
num
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def exist?(name, options = nil)
|
||||
super
|
||||
File.exist?(real_file_path(name))
|
||||
def decrement(name, amount = 1, options = nil)
|
||||
file_name = key_file_path(namespaced_key(name, options))
|
||||
lock_file(file_name) do
|
||||
options = merged_options(options)
|
||||
if num = read(name, options)
|
||||
num = num.to_i - amount
|
||||
write(name, num, options)
|
||||
num
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def real_file_path(name)
|
||||
'%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')]
|
||||
def delete_matched(matcher, options = nil)
|
||||
options = merged_options(options)
|
||||
instrument(:delete_matched, matcher.inspect) do
|
||||
matcher = key_matcher(matcher, options)
|
||||
search_dir(cache_path) do |path|
|
||||
key = file_path_key(path)
|
||||
delete_entry(key, options) if key.match(matcher)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def read_entry(key, options)
|
||||
file_name = key_file_path(key)
|
||||
if File.exist?(file_name)
|
||||
File.open(file_name) { |f| Marshal.load(f) }
|
||||
end
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
def write_entry(key, entry, options)
|
||||
file_name = key_file_path(key)
|
||||
ensure_cache_path(File.dirname(file_name))
|
||||
File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)}
|
||||
true
|
||||
end
|
||||
|
||||
def delete_entry(key, options)
|
||||
file_name = key_file_path(key)
|
||||
if File.exist?(file_name)
|
||||
begin
|
||||
File.delete(file_name)
|
||||
delete_empty_directories(File.dirname(file_name))
|
||||
true
|
||||
rescue => e
|
||||
# Just in case the error was caused by another process deleting the file first.
|
||||
raise e if File.exist?(file_name)
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# Lock a file for a block so only one process can modify it at a time.
|
||||
def lock_file(file_name, &block) # :nodoc:
|
||||
if File.exist?(file_name)
|
||||
File.open(file_name, 'r+') do |f|
|
||||
begin
|
||||
f.flock File::LOCK_EX
|
||||
yield
|
||||
ensure
|
||||
f.flock File::LOCK_UN
|
||||
end
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Translate a key into a file path.
|
||||
def key_file_path(key)
|
||||
fname = Rack::Utils.escape(key)
|
||||
hash = Zlib.adler32(fname)
|
||||
hash, dir_1 = hash.divmod(0x1000)
|
||||
dir_2 = hash.modulo(0x1000)
|
||||
fname_paths = []
|
||||
|
||||
# Make sure file name doesn't exceed file system limits.
|
||||
begin
|
||||
fname_paths << fname[0, FILENAME_MAX_SIZE]
|
||||
fname = fname[FILENAME_MAX_SIZE..-1]
|
||||
end until fname.blank?
|
||||
|
||||
File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths)
|
||||
end
|
||||
|
||||
# Translate a file path into a key.
|
||||
def file_path_key(path)
|
||||
fname = path[cache_path.size, path.size].split(File::SEPARATOR, 4).last
|
||||
Rack::Utils.unescape(fname)
|
||||
end
|
||||
|
||||
# Delete empty directories in the cache.
|
||||
def delete_empty_directories(dir)
|
||||
return if dir == cache_path
|
||||
if Dir.entries(dir).reject{|f| f.in?(EXCLUDED_DIRS)}.empty?
|
||||
File.delete(dir) rescue nil
|
||||
delete_empty_directories(File.dirname(dir))
|
||||
end
|
||||
end
|
||||
|
||||
# Make sure a file path's directories exist.
|
||||
def ensure_cache_path(path)
|
||||
FileUtils.makedirs(path) unless File.exist?(path)
|
||||
end
|
||||
|
||||
def search_dir(dir, &callback)
|
||||
return if !File.exist?(dir)
|
||||
Dir.foreach(dir) do |d|
|
||||
next if d == "." || d == ".."
|
||||
next if d.in?(EXCLUDED_DIRS)
|
||||
name = File.join(dir, d)
|
||||
if File.directory?(name)
|
||||
search_dir(name, &callback)
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
require 'memcache'
|
||||
begin
|
||||
require 'memcache'
|
||||
rescue LoadError => e
|
||||
$stderr.puts "You don't have memcache-client installed in your application. Please add it to your Gemfile and run bundle install"
|
||||
raise e
|
||||
end
|
||||
|
||||
require 'digest/md5'
|
||||
require 'active_support/core_ext/string/encoding'
|
||||
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# A cache store implementation which stores data in Memcached:
|
||||
# http://www.danga.com/memcached/
|
||||
# http://memcached.org/
|
||||
#
|
||||
# This is currently the most popular cache store for production websites.
|
||||
#
|
||||
# Special features:
|
||||
# - Clustering and load balancing. One can specify multiple memcached servers,
|
||||
# and MemCacheStore will load balance between all available servers. If a
|
||||
# server goes down, then MemCacheStore will ignore it until it goes back
|
||||
# online.
|
||||
# - Time-based expiry support. See #write and the +:expires_in+ option.
|
||||
# - Per-request in memory cache for all communication with the MemCache server(s).
|
||||
# server goes down, then MemCacheStore will ignore it until it comes back up.
|
||||
#
|
||||
# MemCacheStore implements the Strategy::LocalCache strategy which implements
|
||||
# an in-memory cache inside of a block.
|
||||
class MemCacheStore < Store
|
||||
module Response # :nodoc:
|
||||
STORED = "STORED\r\n"
|
||||
@@ -23,10 +31,12 @@ module ActiveSupport
|
||||
DELETED = "DELETED\r\n"
|
||||
end
|
||||
|
||||
ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
|
||||
|
||||
def self.build_mem_cache(*addresses)
|
||||
addresses = addresses.flatten
|
||||
options = addresses.extract_options!
|
||||
addresses = ["localhost"] if addresses.empty?
|
||||
addresses = ["localhost:11211"] if addresses.empty?
|
||||
MemCache.new(addresses, options)
|
||||
end
|
||||
|
||||
@@ -44,100 +54,153 @@ module ActiveSupport
|
||||
# require 'memcached' # gem install memcached; uses C bindings to libmemcached
|
||||
# ActiveSupport::Cache::MemCacheStore.new(Memcached::Rails.new("localhost:11211"))
|
||||
def initialize(*addresses)
|
||||
addresses = addresses.flatten
|
||||
options = addresses.extract_options!
|
||||
super(options)
|
||||
|
||||
if addresses.first.respond_to?(:get)
|
||||
@data = addresses.first
|
||||
else
|
||||
@data = self.class.build_mem_cache(*addresses)
|
||||
mem_cache_options = options.dup
|
||||
UNIVERSAL_OPTIONS.each{|name| mem_cache_options.delete(name)}
|
||||
@data = self.class.build_mem_cache(*(addresses + [mem_cache_options]))
|
||||
end
|
||||
|
||||
extend Strategy::LocalCache
|
||||
extend LocalCacheWithRaw
|
||||
end
|
||||
|
||||
# Reads multiple keys from the cache.
|
||||
def read_multi(*keys)
|
||||
@data.get_multi keys
|
||||
# Reads multiple values from the cache using a single call to the
|
||||
# servers for all keys. Options can be passed in the last argument.
|
||||
def read_multi(*names)
|
||||
options = names.extract_options!
|
||||
options = merged_options(options)
|
||||
keys_to_names = Hash[names.map{|name| [escape_key(namespaced_key(name, options)), name]}]
|
||||
raw_values = @data.get_multi(keys_to_names.keys, :raw => true)
|
||||
values = {}
|
||||
raw_values.each do |key, value|
|
||||
entry = deserialize_entry(value)
|
||||
values[keys_to_names[key]] = entry.value unless entry.expired?
|
||||
end
|
||||
values
|
||||
end
|
||||
|
||||
def read(key, options = nil) # :nodoc:
|
||||
super
|
||||
@data.get(key, raw?(options))
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}")
|
||||
nil
|
||||
end
|
||||
|
||||
# Writes a value to the cache.
|
||||
#
|
||||
# Possible options:
|
||||
# - +:unless_exist+ - set to true if you don't want to update the cache
|
||||
# if the key is already set.
|
||||
# - +:expires_in+ - the number of seconds that this value may stay in
|
||||
# the cache. See ActiveSupport::Cache::Store#write for an example.
|
||||
def write(key, value, options = nil)
|
||||
super
|
||||
method = options && options[:unless_exist] ? :add : :set
|
||||
# memcache-client will break the connection if you send it an integer
|
||||
# in raw mode, so we convert it to a string to be sure it continues working.
|
||||
value = value.to_s if raw?(options)
|
||||
response = @data.send(method, key, value, expires_in(options), raw?(options))
|
||||
response == Response::STORED
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}")
|
||||
false
|
||||
end
|
||||
|
||||
def delete(key, options = nil) # :nodoc:
|
||||
super
|
||||
response = @data.delete(key, expires_in(options))
|
||||
response == Response::DELETED
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}")
|
||||
false
|
||||
end
|
||||
|
||||
def exist?(key, options = nil) # :nodoc:
|
||||
# Doesn't call super, cause exist? in memcache is in fact a read
|
||||
# But who cares? Reading is very fast anyway
|
||||
# Local cache is checked first, if it doesn't know then memcache itself is read from
|
||||
!read(key, options).nil?
|
||||
end
|
||||
|
||||
def increment(key, amount = 1) # :nodoc:
|
||||
log("incrementing", key, amount)
|
||||
|
||||
response = @data.incr(key, amount)
|
||||
response == Response::NOT_FOUND ? nil : response
|
||||
# Increment a cached value. This method uses the memcached incr atomic
|
||||
# operator and can only be used on values written with the :raw option.
|
||||
# Calling it on a value not stored with :raw will initialize that value
|
||||
# to zero.
|
||||
def increment(name, amount = 1, options = nil) # :nodoc:
|
||||
options = merged_options(options)
|
||||
response = instrument(:increment, name, :amount => amount) do
|
||||
@data.incr(escape_key(namespaced_key(name, options)), amount)
|
||||
end
|
||||
response == Response::NOT_FOUND ? nil : response.to_i
|
||||
rescue MemCache::MemCacheError
|
||||
nil
|
||||
end
|
||||
|
||||
def decrement(key, amount = 1) # :nodoc:
|
||||
log("decrement", key, amount)
|
||||
response = @data.decr(key, amount)
|
||||
response == Response::NOT_FOUND ? nil : response
|
||||
# Decrement a cached value. This method uses the memcached decr atomic
|
||||
# operator and can only be used on values written with the :raw option.
|
||||
# Calling it on a value not stored with :raw will initialize that value
|
||||
# to zero.
|
||||
def decrement(name, amount = 1, options = nil) # :nodoc:
|
||||
options = merged_options(options)
|
||||
response = instrument(:decrement, name, :amount => amount) do
|
||||
@data.decr(escape_key(namespaced_key(name, options)), amount)
|
||||
end
|
||||
response == Response::NOT_FOUND ? nil : response.to_i
|
||||
rescue MemCache::MemCacheError
|
||||
nil
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil) # :nodoc:
|
||||
# don't do any local caching at present, just pass
|
||||
# through and let the error happen
|
||||
super
|
||||
raise "Not supported by Memcache"
|
||||
end
|
||||
|
||||
def clear
|
||||
# Clear the entire cache on all memcached servers. This method should
|
||||
# be used with care when shared cache is being used.
|
||||
def clear(options = nil)
|
||||
@data.flush_all
|
||||
end
|
||||
|
||||
# Get the statistics from the memcached servers.
|
||||
def stats
|
||||
@data.stats
|
||||
end
|
||||
|
||||
private
|
||||
def raw?(options)
|
||||
options && options[:raw]
|
||||
protected
|
||||
# Read an entry from the cache.
|
||||
def read_entry(key, options) # :nodoc:
|
||||
deserialize_entry(@data.get(escape_key(key), true))
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}") if logger
|
||||
nil
|
||||
end
|
||||
|
||||
# Write an entry to the cache.
|
||||
def write_entry(key, entry, options) # :nodoc:
|
||||
method = options && options[:unless_exist] ? :add : :set
|
||||
value = options[:raw] ? entry.value.to_s : entry
|
||||
expires_in = options[:expires_in].to_i
|
||||
if expires_in > 0 && !options[:raw]
|
||||
# Set the memcache expire a few minutes in the future to support race condition ttls on read
|
||||
expires_in += 5.minutes
|
||||
end
|
||||
response = @data.send(method, escape_key(key), value, expires_in, options[:raw])
|
||||
response == Response::STORED
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}") if logger
|
||||
false
|
||||
end
|
||||
|
||||
# Delete an entry from the cache.
|
||||
def delete_entry(key, options) # :nodoc:
|
||||
response = @data.delete(escape_key(key))
|
||||
response == Response::DELETED
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}") if logger
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Memcache keys are binaries. So we need to force their encoding to binary
|
||||
# before applying the regular expression to ensure we are escaping all
|
||||
# characters properly.
|
||||
def escape_key(key)
|
||||
key = key.to_s.dup
|
||||
key = key.force_encoding("BINARY") if key.encoding_aware?
|
||||
key = key.gsub(ESCAPE_KEY_CHARS){ |match| "%#{match.getbyte(0).to_s(16).upcase}" }
|
||||
key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250
|
||||
key
|
||||
end
|
||||
|
||||
def deserialize_entry(raw_value)
|
||||
if raw_value
|
||||
entry = Marshal.load(raw_value) rescue raw_value
|
||||
entry.is_a?(Entry) ? entry : Entry.new(entry)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Provide support for raw values in the local cache strategy.
|
||||
module LocalCacheWithRaw # :nodoc:
|
||||
protected
|
||||
def read_entry(key, options)
|
||||
entry = super
|
||||
if options[:raw] && local_cache && entry
|
||||
entry = deserialize_entry(entry.value)
|
||||
end
|
||||
entry
|
||||
end
|
||||
|
||||
def write_entry(key, entry, options) # :nodoc:
|
||||
retval = super
|
||||
if options[:raw] && local_cache && retval
|
||||
raw_entry = Entry.new(entry.value.to_s)
|
||||
raw_entry.expires_at = entry.expires_at
|
||||
local_cache.write_entry(key, raw_entry, options)
|
||||
end
|
||||
retval
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,58 +1,159 @@
|
||||
require 'monitor'
|
||||
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# A cache store implementation which stores everything into memory in the
|
||||
# same process. If you're running multiple Ruby on Rails server processes
|
||||
# (which is the case if you're using mongrel_cluster or Phusion Passenger),
|
||||
# then this means that your Rails server process instances won't be able
|
||||
# to share cache data with each other. If your application never performs
|
||||
# manual cache item expiry (e.g. when you're using generational cache keys),
|
||||
# then using MemoryStore is ok. Otherwise, consider carefully whether you
|
||||
# should be using this cache store.
|
||||
# then this means that Rails server process instances won't be able
|
||||
# to share cache data with each other and this may not be the most
|
||||
# appropriate cache in that scenario.
|
||||
#
|
||||
# MemoryStore is not only able to store strings, but also arbitrary Ruby
|
||||
# objects.
|
||||
# This cache has a bounded size specified by the :size options to the
|
||||
# initializer (default is 32Mb). When the cache exceeds the allotted size,
|
||||
# a cleanup will occur which tries to prune the cache down to three quarters
|
||||
# of the maximum size by removing the least recently used entries.
|
||||
#
|
||||
# MemoryStore is not thread-safe. Use SynchronizedMemoryStore instead
|
||||
# if you need thread-safety.
|
||||
# MemoryStore is thread-safe.
|
||||
class MemoryStore < Store
|
||||
def initialize
|
||||
def initialize(options = nil)
|
||||
options ||= {}
|
||||
super(options)
|
||||
@data = {}
|
||||
@key_access = {}
|
||||
@max_size = options[:size] || 32.megabytes
|
||||
@max_prune_time = options[:max_prune_time] || 2
|
||||
@cache_size = 0
|
||||
@monitor = Monitor.new
|
||||
@pruning = false
|
||||
end
|
||||
|
||||
def read_multi(*names)
|
||||
results = {}
|
||||
names.each { |n| results[n] = read(n) }
|
||||
results
|
||||
def clear(options = nil)
|
||||
synchronize do
|
||||
@data.clear
|
||||
@key_access.clear
|
||||
@cache_size = 0
|
||||
end
|
||||
end
|
||||
|
||||
def read(name, options = nil)
|
||||
super
|
||||
@data[name]
|
||||
def cleanup(options = nil)
|
||||
options = merged_options(options)
|
||||
instrument(:cleanup, :size => @data.size) do
|
||||
keys = synchronize{ @data.keys }
|
||||
keys.each do |key|
|
||||
entry = @data[key]
|
||||
delete_entry(key, options) if entry && entry.expired?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write(name, value, options = nil)
|
||||
super
|
||||
@data[name] = value.freeze
|
||||
# To ensure entries fit within the specified memory prune the cache by removing the least
|
||||
# recently accessed entries.
|
||||
def prune(target_size, max_time = nil)
|
||||
return if pruning?
|
||||
@pruning = true
|
||||
begin
|
||||
start_time = Time.now
|
||||
cleanup
|
||||
instrument(:prune, target_size, :from => @cache_size) do
|
||||
keys = synchronize{ @key_access.keys.sort{|a,b| @key_access[a].to_f <=> @key_access[b].to_f} }
|
||||
keys.each do |key|
|
||||
delete_entry(key, options)
|
||||
return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time)
|
||||
end
|
||||
end
|
||||
ensure
|
||||
@pruning = false
|
||||
end
|
||||
end
|
||||
|
||||
def delete(name, options = nil)
|
||||
super
|
||||
@data.delete(name)
|
||||
# Returns true if the cache is currently being pruned.
|
||||
def pruning?
|
||||
@pruning
|
||||
end
|
||||
|
||||
# Increment an integer value in the cache.
|
||||
def increment(name, amount = 1, options = nil)
|
||||
synchronize do
|
||||
options = merged_options(options)
|
||||
if num = read(name, options)
|
||||
num = num.to_i + amount
|
||||
write(name, num, options)
|
||||
num
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Decrement an integer value in the cache.
|
||||
def decrement(name, amount = 1, options = nil)
|
||||
synchronize do
|
||||
options = merged_options(options)
|
||||
if num = read(name, options)
|
||||
num = num.to_i - amount
|
||||
write(name, num, options)
|
||||
num
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
super
|
||||
@data.delete_if { |k,v| k =~ matcher }
|
||||
options = merged_options(options)
|
||||
instrument(:delete_matched, matcher.inspect) do
|
||||
matcher = key_matcher(matcher, options)
|
||||
keys = synchronize { @data.keys }
|
||||
keys.each do |key|
|
||||
delete_entry(key, options) if key.match(matcher)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def exist?(name, options = nil)
|
||||
super
|
||||
@data.has_key?(name)
|
||||
def inspect # :nodoc:
|
||||
"<##{self.class.name} entries=#{@data.size}, size=#{@cache_size}, options=#{@options.inspect}>"
|
||||
end
|
||||
|
||||
def clear
|
||||
@data.clear
|
||||
# Synchronize calls to the cache. This should be called wherever the underlying cache implementation
|
||||
# is not thread safe.
|
||||
def synchronize(&block) # :nodoc:
|
||||
@monitor.synchronize(&block)
|
||||
end
|
||||
|
||||
protected
|
||||
def read_entry(key, options) # :nodoc:
|
||||
entry = @data[key]
|
||||
synchronize do
|
||||
if entry
|
||||
@key_access[key] = Time.now.to_f
|
||||
else
|
||||
@key_access.delete(key)
|
||||
end
|
||||
end
|
||||
entry
|
||||
end
|
||||
|
||||
def write_entry(key, entry, options) # :nodoc:
|
||||
synchronize do
|
||||
old_entry = @data[key]
|
||||
@cache_size -= old_entry.size if old_entry
|
||||
@cache_size += entry.size
|
||||
@key_access[key] = Time.now.to_f
|
||||
@data[key] = entry
|
||||
prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def delete_entry(key, options) # :nodoc:
|
||||
synchronize do
|
||||
@key_access.delete(key)
|
||||
entry = @data.delete(key)
|
||||
@cache_size -= entry.size if entry
|
||||
!!entry
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
44
activesupport/lib/active_support/cache/null_store.rb
vendored
Normal file
44
activesupport/lib/active_support/cache/null_store.rb
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# A cache store implementation which doesn't actually store anything. Useful in
|
||||
# development and test environments where you don't want caching turned on but
|
||||
# need to go through the caching interface.
|
||||
#
|
||||
# This cache does implement the local cache strategy, so values will actually
|
||||
# be cached inside blocks that utilize this strategy. See
|
||||
# ActiveSupport::Cache::Strategy::LocalCache for more details.
|
||||
class NullStore < Store
|
||||
def initialize(options = nil)
|
||||
super(options)
|
||||
extend Strategy::LocalCache
|
||||
end
|
||||
|
||||
def clear(options = nil)
|
||||
end
|
||||
|
||||
def cleanup(options = nil)
|
||||
end
|
||||
|
||||
def increment(name, amount = 1, options = nil)
|
||||
end
|
||||
|
||||
def decrement(name, amount = 1, options = nil)
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
end
|
||||
|
||||
protected
|
||||
def read_entry(key, options) # :nodoc:
|
||||
end
|
||||
|
||||
def write_entry(key, entry, options) # :nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def delete_entry(key, options) # :nodoc:
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,103 +1,168 @@
|
||||
require 'active_support/core_ext/object/duplicable'
|
||||
require 'active_support/core_ext/string/inflections'
|
||||
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
module Strategy
|
||||
# Caches that implement LocalCache will be backed by an in-memory cache for the
|
||||
# duration of a block. Repeated calls to the cache for the same key will hit the
|
||||
# in-memory cache for faster access.
|
||||
module LocalCache
|
||||
# this allows caching of the fact that there is nothing in the remote cache
|
||||
NULL = 'remote_cache_store:null'
|
||||
|
||||
def with_local_cache
|
||||
Thread.current[thread_local_key] = MemoryStore.new
|
||||
yield
|
||||
ensure
|
||||
Thread.current[thread_local_key] = nil
|
||||
end
|
||||
|
||||
def middleware
|
||||
@middleware ||= begin
|
||||
klass = Class.new
|
||||
klass.class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
Thread.current[:#{thread_local_key}] = MemoryStore.new
|
||||
@app.call(env)
|
||||
ensure
|
||||
Thread.current[:#{thread_local_key}] = nil
|
||||
end
|
||||
EOS
|
||||
klass
|
||||
# Simple memory backed cache. This cache is not thread safe and is intended only
|
||||
# for serving as a temporary memory cache for a single thread.
|
||||
class LocalStore < Store
|
||||
def initialize
|
||||
super
|
||||
@data = {}
|
||||
end
|
||||
end
|
||||
|
||||
def read(key, options = nil)
|
||||
value = local_cache && local_cache.read(key)
|
||||
if value == NULL
|
||||
nil
|
||||
elsif value.nil?
|
||||
value = super
|
||||
local_cache.mute { local_cache.write(key, value || NULL) } if local_cache
|
||||
value.duplicable? ? value.dup : value
|
||||
else
|
||||
# forcing the value to be immutable
|
||||
value.duplicable? ? value.dup : value
|
||||
# Don't allow synchronizing since it isn't thread safe,
|
||||
def synchronize # :nodoc:
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def write(key, value, options = nil)
|
||||
value = value.to_s if respond_to?(:raw?) && raw?(options)
|
||||
local_cache.mute { local_cache.write(key, value || NULL) } if local_cache
|
||||
super
|
||||
end
|
||||
def clear(options = nil)
|
||||
@data.clear
|
||||
end
|
||||
|
||||
def delete(key, options = nil)
|
||||
local_cache.mute { local_cache.write(key, NULL) } if local_cache
|
||||
super
|
||||
end
|
||||
def read_entry(key, options)
|
||||
@data[key]
|
||||
end
|
||||
|
||||
def exist(key, options = nil)
|
||||
value = local_cache.read(key) if local_cache
|
||||
if value == NULL
|
||||
false
|
||||
elsif value
|
||||
def write_entry(key, value, options)
|
||||
@data[key] = value
|
||||
true
|
||||
else
|
||||
end
|
||||
|
||||
def delete_entry(key, options)
|
||||
!!@data.delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
# Use a local cache for the duration of block.
|
||||
def with_local_cache
|
||||
save_val = Thread.current[thread_local_key]
|
||||
begin
|
||||
Thread.current[thread_local_key] = LocalStore.new
|
||||
yield
|
||||
ensure
|
||||
Thread.current[thread_local_key] = save_val
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# This class wraps up local storage for middlewares. Only the middleware method should
|
||||
# construct them.
|
||||
class Middleware # :nodoc:
|
||||
attr_reader :name, :thread_local_key
|
||||
|
||||
def initialize(name, thread_local_key)
|
||||
@name = name
|
||||
@thread_local_key = thread_local_key
|
||||
@app = nil
|
||||
end
|
||||
|
||||
def new(app)
|
||||
@app = app
|
||||
self
|
||||
end
|
||||
|
||||
def call(env)
|
||||
Thread.current[thread_local_key] = LocalStore.new
|
||||
@app.call(env)
|
||||
ensure
|
||||
Thread.current[thread_local_key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Middleware class can be inserted as a Rack handler to be local cache for the
|
||||
# duration of request.
|
||||
def middleware
|
||||
@middleware ||= Middleware.new(
|
||||
"ActiveSupport::Cache::Strategy::LocalCache",
|
||||
thread_local_key)
|
||||
end
|
||||
|
||||
def clear(options = nil) # :nodoc:
|
||||
local_cache.clear(options) if local_cache
|
||||
super
|
||||
end
|
||||
|
||||
def cleanup(options = nil) # :nodoc:
|
||||
local_cache.clear(options) if local_cache
|
||||
super
|
||||
end
|
||||
|
||||
def increment(name, amount = 1, options = nil) # :nodoc:
|
||||
value = bypass_local_cache{super}
|
||||
if local_cache
|
||||
local_cache.mute do
|
||||
if value
|
||||
local_cache.write(name, value, options)
|
||||
else
|
||||
local_cache.delete(name, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
def decrement(name, amount = 1, options = nil) # :nodoc:
|
||||
value = bypass_local_cache{super}
|
||||
if local_cache
|
||||
local_cache.mute do
|
||||
if value
|
||||
local_cache.write(name, value, options)
|
||||
else
|
||||
local_cache.delete(name, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
protected
|
||||
def read_entry(key, options) # :nodoc:
|
||||
if local_cache
|
||||
entry = local_cache.read_entry(key, options)
|
||||
unless entry
|
||||
entry = super
|
||||
local_cache.write_entry(key, entry, options)
|
||||
end
|
||||
entry
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def write_entry(key, entry, options) # :nodoc:
|
||||
local_cache.write_entry(key, entry, options) if local_cache
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def increment(key, amount = 1)
|
||||
if value = super
|
||||
local_cache.mute { local_cache.write(key, value.to_s) } if local_cache
|
||||
value
|
||||
else
|
||||
nil
|
||||
def delete_entry(key, options) # :nodoc:
|
||||
local_cache.delete_entry(key, options) if local_cache
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def decrement(key, amount = 1)
|
||||
if value = super
|
||||
local_cache.mute { local_cache.write(key, value.to_s) } if local_cache
|
||||
value
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def clear
|
||||
local_cache.clear if local_cache
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
def thread_local_key
|
||||
@thread_local_key ||= "#{self.class.name.underscore}_local_cache".gsub("/", "_").to_sym
|
||||
@thread_local_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym
|
||||
end
|
||||
|
||||
def local_cache
|
||||
Thread.current[thread_local_key]
|
||||
end
|
||||
|
||||
def bypass_local_cache
|
||||
save_cache = Thread.current[thread_local_key]
|
||||
begin
|
||||
Thread.current[thread_local_key] = nil
|
||||
yield
|
||||
ensure
|
||||
Thread.current[thread_local_key] = save_cache
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# Like MemoryStore, but thread-safe.
|
||||
class SynchronizedMemoryStore < MemoryStore
|
||||
def initialize
|
||||
super
|
||||
@guard = Monitor.new
|
||||
end
|
||||
|
||||
def fetch(key, options = {})
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def read(name, options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def write(name, value, options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def delete(name, options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def exist?(name,options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def increment(key, amount = 1)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def decrement(key, amount = 1)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def clear
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,279 +1,626 @@
|
||||
require 'active_support/concern'
|
||||
require 'active_support/descendants_tracker'
|
||||
require 'active_support/core_ext/array/wrap'
|
||||
require 'active_support/core_ext/class/attribute'
|
||||
require 'active_support/core_ext/kernel/reporting'
|
||||
require 'active_support/core_ext/kernel/singleton_class'
|
||||
require 'active_support/core_ext/object/inclusion'
|
||||
|
||||
module ActiveSupport
|
||||
# Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
|
||||
# before or after an alteration of the object state.
|
||||
# \Callbacks are code hooks that are run at key points in an object's lifecycle.
|
||||
# The typical use case is to have a base class define a set of callbacks relevant
|
||||
# to the other functionality it supplies, so that subclasses can install callbacks
|
||||
# that enhance or modify the base functionality without needing to override
|
||||
# or redefine methods of the base class.
|
||||
#
|
||||
# Mixing in this module allows you to define callbacks in your class.
|
||||
# Mixing in this module allows you to define the events in the object's lifecycle
|
||||
# that will support callbacks (via +ClassMethods.define_callbacks+), set the instance
|
||||
# methods, procs, or callback objects to be called (via +ClassMethods.set_callback+),
|
||||
# and run the installed callbacks at the appropriate times (via +run_callbacks+).
|
||||
#
|
||||
# Example:
|
||||
# class Storage
|
||||
# Three kinds of callbacks are supported: before callbacks, run before a certain event;
|
||||
# after callbacks, run after the event; and around callbacks, blocks that surround the
|
||||
# event, triggering it when they yield. Callback code can be contained in instance
|
||||
# methods, procs or lambdas, or callback objects that respond to certain predetermined
|
||||
# methods. See +ClassMethods.set_callback+ for details.
|
||||
#
|
||||
# ==== Example
|
||||
#
|
||||
# class Record
|
||||
# include ActiveSupport::Callbacks
|
||||
# define_callbacks :save
|
||||
#
|
||||
# define_callbacks :before_save, :after_save
|
||||
# def save
|
||||
# run_callbacks :save do
|
||||
# puts "- save"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class ConfigStorage < Storage
|
||||
# before_save :saving_message
|
||||
# class PersonRecord < Record
|
||||
# set_callback :save, :before, :saving_message
|
||||
# def saving_message
|
||||
# puts "saving..."
|
||||
# end
|
||||
#
|
||||
# after_save do |object|
|
||||
# set_callback :save, :after do |object|
|
||||
# puts "saved"
|
||||
# end
|
||||
#
|
||||
# def save
|
||||
# run_callbacks(:before_save)
|
||||
# puts "- save"
|
||||
# run_callbacks(:after_save)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# config = ConfigStorage.new
|
||||
# config.save
|
||||
# person = PersonRecord.new
|
||||
# person.save
|
||||
#
|
||||
# Output:
|
||||
# saving...
|
||||
# - save
|
||||
# saved
|
||||
#
|
||||
# Callbacks from parent classes are inherited.
|
||||
#
|
||||
# Example:
|
||||
# class Storage
|
||||
# include ActiveSupport::Callbacks
|
||||
#
|
||||
# define_callbacks :before_save, :after_save
|
||||
#
|
||||
# before_save :prepare
|
||||
# def prepare
|
||||
# puts "preparing save"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class ConfigStorage < Storage
|
||||
# before_save :saving_message
|
||||
# def saving_message
|
||||
# puts "saving..."
|
||||
# end
|
||||
#
|
||||
# after_save do |object|
|
||||
# puts "saved"
|
||||
# end
|
||||
#
|
||||
# def save
|
||||
# run_callbacks(:before_save)
|
||||
# puts "- save"
|
||||
# run_callbacks(:after_save)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# config = ConfigStorage.new
|
||||
# config.save
|
||||
#
|
||||
# Output:
|
||||
# preparing save
|
||||
# saving...
|
||||
# - save
|
||||
# saved
|
||||
module Callbacks
|
||||
class CallbackChain < Array
|
||||
def self.build(kind, *methods, &block)
|
||||
methods, options = extract_options(*methods, &block)
|
||||
methods.map! { |method| Callback.new(kind, method, options) }
|
||||
new(methods)
|
||||
end
|
||||
extend Concern
|
||||
|
||||
def run(object, options = {}, &terminator)
|
||||
enumerator = options[:enumerator] || :each
|
||||
|
||||
unless block_given?
|
||||
send(enumerator) { |callback| callback.call(object) }
|
||||
else
|
||||
send(enumerator) do |callback|
|
||||
result = callback.call(object)
|
||||
break result if terminator.call(result, object)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Decompose into more Array like behavior
|
||||
def replace_or_append!(chain)
|
||||
if index = index(chain)
|
||||
self[index] = chain
|
||||
else
|
||||
self << chain
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def find(callback, &block)
|
||||
select { |c| c == callback && (!block_given? || yield(c)) }.first
|
||||
end
|
||||
|
||||
def delete(callback)
|
||||
super(callback.is_a?(Callback) ? callback : find(callback))
|
||||
end
|
||||
|
||||
private
|
||||
def self.extract_options(*methods, &block)
|
||||
methods.flatten!
|
||||
options = methods.extract_options!
|
||||
methods << block if block_given?
|
||||
return methods, options
|
||||
end
|
||||
|
||||
def extract_options(*methods, &block)
|
||||
self.class.extract_options(*methods, &block)
|
||||
end
|
||||
included do
|
||||
extend ActiveSupport::DescendantsTracker
|
||||
end
|
||||
|
||||
class Callback
|
||||
attr_reader :kind, :method, :identifier, :options
|
||||
# Runs the callbacks for the given event.
|
||||
#
|
||||
# Calls the before and around callbacks in the order they were set, yields
|
||||
# the block (if given one), and then runs the after callbacks in reverse order.
|
||||
# Optionally accepts a key, which will be used to compile an optimized callback
|
||||
# method for each key. See +ClassMethods.define_callbacks+ for more information.
|
||||
#
|
||||
# If the callback chain was halted, returns +false+. Otherwise returns the result
|
||||
# of the block, or +true+ if no block is given.
|
||||
#
|
||||
# run_callbacks :save do
|
||||
# save
|
||||
# end
|
||||
#
|
||||
def run_callbacks(kind, *args, &block)
|
||||
send("_run_#{kind}_callbacks", *args, &block)
|
||||
end
|
||||
|
||||
def initialize(kind, method, options = {})
|
||||
@kind = kind
|
||||
@method = method
|
||||
@identifier = options[:identifier]
|
||||
@options = options
|
||||
private
|
||||
|
||||
# A hook invoked everytime a before callback is halted.
|
||||
# This can be overriden in AS::Callback implementors in order
|
||||
# to provide better debugging/logging.
|
||||
def halted_callback_hook(filter)
|
||||
end
|
||||
|
||||
class Callback #:nodoc:#
|
||||
@@_callback_sequence = 0
|
||||
|
||||
attr_accessor :chain, :filter, :kind, :options, :per_key, :klass, :raw_filter
|
||||
|
||||
def initialize(chain, filter, kind, options, klass)
|
||||
@chain, @kind, @klass = chain, kind, klass
|
||||
normalize_options!(options)
|
||||
|
||||
@per_key = options.delete(:per_key)
|
||||
@raw_filter, @options = filter, options
|
||||
@filter = _compile_filter(filter)
|
||||
@compiled_options = _compile_options(options)
|
||||
@callback_id = next_id
|
||||
|
||||
_compile_per_key_options
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
case other
|
||||
when Callback
|
||||
(self.identifier && self.identifier == other.identifier) || self.method == other.method
|
||||
else
|
||||
(self.identifier && self.identifier == other) || self.method == other
|
||||
end
|
||||
def clone(chain, klass)
|
||||
obj = super()
|
||||
obj.chain = chain
|
||||
obj.klass = klass
|
||||
obj.per_key = @per_key.dup
|
||||
obj.options = @options.dup
|
||||
obj.per_key[:if] = @per_key[:if].dup
|
||||
obj.per_key[:unless] = @per_key[:unless].dup
|
||||
obj.options[:if] = @options[:if].dup
|
||||
obj.options[:unless] = @options[:unless].dup
|
||||
obj
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
self == other
|
||||
def normalize_options!(options)
|
||||
options[:if] = Array.wrap(options[:if])
|
||||
options[:unless] = Array.wrap(options[:unless])
|
||||
|
||||
options[:per_key] ||= {}
|
||||
options[:per_key][:if] = Array.wrap(options[:per_key][:if])
|
||||
options[:per_key][:unless] = Array.wrap(options[:per_key][:unless])
|
||||
end
|
||||
|
||||
def dup
|
||||
self.class.new(@kind, @method, @options.dup)
|
||||
def name
|
||||
chain.name
|
||||
end
|
||||
|
||||
def hash
|
||||
if @identifier
|
||||
@identifier.hash
|
||||
else
|
||||
@method.hash
|
||||
end
|
||||
def next_id
|
||||
@@_callback_sequence += 1
|
||||
end
|
||||
|
||||
def call(*args, &block)
|
||||
evaluate_method(method, *args, &block) if should_run_callback?(*args)
|
||||
rescue LocalJumpError
|
||||
raise ArgumentError,
|
||||
"Cannot yield from a Proc type filter. The Proc must take two " +
|
||||
"arguments and execute #call on the second argument."
|
||||
def matches?(_kind, _filter)
|
||||
@kind == _kind && @filter == _filter
|
||||
end
|
||||
|
||||
private
|
||||
def evaluate_method(method, *args, &block)
|
||||
case method
|
||||
when Symbol
|
||||
object = args.shift
|
||||
object.send(method, *args, &block)
|
||||
when String
|
||||
eval(method, args.first.instance_eval { binding })
|
||||
when Proc, Method
|
||||
method.call(*args, &block)
|
||||
else
|
||||
if method.respond_to?(kind)
|
||||
method.send(kind, *args, &block)
|
||||
else
|
||||
raise ArgumentError,
|
||||
"Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
|
||||
"a block to be invoked, or an object responding to the callback method."
|
||||
def _update_filter(filter_options, new_options)
|
||||
filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
|
||||
filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
|
||||
end
|
||||
|
||||
def recompile!(_options, _per_key)
|
||||
_update_filter(self.options, _options)
|
||||
_update_filter(self.per_key, _per_key)
|
||||
|
||||
@callback_id = next_id
|
||||
@filter = _compile_filter(@raw_filter)
|
||||
@compiled_options = _compile_options(@options)
|
||||
_compile_per_key_options
|
||||
end
|
||||
|
||||
def _compile_per_key_options
|
||||
key_options = _compile_options(@per_key)
|
||||
|
||||
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def _one_time_conditions_valid_#{@callback_id}?
|
||||
true if #{key_options}
|
||||
end
|
||||
RUBY_EVAL
|
||||
end
|
||||
|
||||
# This will supply contents for before and around filters, and no
|
||||
# contents for after filters (for the forward pass).
|
||||
def start(key=nil, object=nil)
|
||||
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
|
||||
|
||||
# options[0] is the compiled form of supplied conditions
|
||||
# options[1] is the "end" for the conditional
|
||||
#
|
||||
case @kind
|
||||
when :before
|
||||
# if condition # before_save :filter_name, :if => :condition
|
||||
# filter_name
|
||||
# end
|
||||
<<-RUBY_EVAL
|
||||
if !halted && #{@compiled_options}
|
||||
# This double assignment is to prevent warnings in 1.9.3 as
|
||||
# the `result` variable is not always used except if the
|
||||
# terminator code refers to it.
|
||||
result = result = #{@filter}
|
||||
halted = (#{chain.config[:terminator]})
|
||||
if halted
|
||||
halted_callback_hook(#{@raw_filter.inspect.inspect})
|
||||
end
|
||||
end
|
||||
RUBY_EVAL
|
||||
when :around
|
||||
# Compile around filters with conditions into proxy methods
|
||||
# that contain the conditions.
|
||||
#
|
||||
# For `around_save :filter_name, :if => :condition':
|
||||
#
|
||||
# def _conditional_callback_save_17
|
||||
# if condition
|
||||
# filter_name do
|
||||
# yield self
|
||||
# end
|
||||
# else
|
||||
# yield self
|
||||
# end
|
||||
# end
|
||||
#
|
||||
name = "_conditional_callback_#{@kind}_#{next_id}"
|
||||
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{name}(halted)
|
||||
if #{@compiled_options} && !halted
|
||||
#{@filter} do
|
||||
yield self
|
||||
end
|
||||
else
|
||||
yield self
|
||||
end
|
||||
end
|
||||
RUBY_EVAL
|
||||
"#{name}(halted) do"
|
||||
end
|
||||
end
|
||||
|
||||
# This will supply contents for around and after filters, but not
|
||||
# before filters (for the backward pass).
|
||||
def end(key=nil, object=nil)
|
||||
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
|
||||
|
||||
case @kind
|
||||
when :after
|
||||
# after_save :filter_name, :if => :condition
|
||||
<<-RUBY_EVAL
|
||||
if #{@compiled_options}
|
||||
#{@filter}
|
||||
end
|
||||
RUBY_EVAL
|
||||
when :around
|
||||
<<-RUBY_EVAL
|
||||
value
|
||||
end
|
||||
RUBY_EVAL
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Options support the same options as filters themselves (and support
|
||||
# symbols, string, procs, and objects), so compile a conditional
|
||||
# expression based on the options
|
||||
def _compile_options(options)
|
||||
conditions = ["true"]
|
||||
|
||||
unless options[:if].empty?
|
||||
conditions << Array.wrap(_compile_filter(options[:if]))
|
||||
end
|
||||
|
||||
unless options[:unless].empty?
|
||||
conditions << Array.wrap(_compile_filter(options[:unless])).map {|f| "!#{f}"}
|
||||
end
|
||||
|
||||
conditions.flatten.join(" && ")
|
||||
end
|
||||
|
||||
# Filters support:
|
||||
#
|
||||
# Arrays:: Used in conditions. This is used to specify
|
||||
# multiple conditions. Used internally to
|
||||
# merge conditions from skip_* filters
|
||||
# Symbols:: A method to call
|
||||
# Strings:: Some content to evaluate
|
||||
# Procs:: A proc to call with the object
|
||||
# Objects:: An object with a before_foo method on it to call
|
||||
#
|
||||
# All of these objects are compiled into methods and handled
|
||||
# the same after this point:
|
||||
#
|
||||
# Arrays:: Merged together into a single filter
|
||||
# Symbols:: Already methods
|
||||
# Strings:: class_eval'ed into methods
|
||||
# Procs:: define_method'ed into methods
|
||||
# Objects::
|
||||
# a method is created that calls the before_foo method
|
||||
# on the object.
|
||||
#
|
||||
def _compile_filter(filter)
|
||||
method_name = "_callback_#{@kind}_#{next_id}"
|
||||
case filter
|
||||
when Array
|
||||
filter.map {|f| _compile_filter(f)}
|
||||
when Symbol
|
||||
filter
|
||||
when String
|
||||
"(#{filter})"
|
||||
when Proc
|
||||
@klass.send(:define_method, method_name, &filter)
|
||||
return method_name if filter.arity <= 0
|
||||
|
||||
method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ")
|
||||
else
|
||||
@klass.send(:define_method, "#{method_name}_object") { filter }
|
||||
|
||||
_normalize_legacy_filter(kind, filter)
|
||||
scopes = Array.wrap(chain.config[:scope])
|
||||
method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_")
|
||||
|
||||
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{method_name}(&blk)
|
||||
#{method_name}_object.send(:#{method_to_call}, self, &blk)
|
||||
end
|
||||
RUBY_EVAL
|
||||
|
||||
method_name
|
||||
end
|
||||
end
|
||||
|
||||
def _normalize_legacy_filter(kind, filter)
|
||||
if !filter.respond_to?(kind) && filter.respond_to?(:filter)
|
||||
filter.singleton_class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{kind}(context, &block) filter(context, &block) end
|
||||
RUBY_EVAL
|
||||
elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around
|
||||
def filter.around(context)
|
||||
should_continue = before(context)
|
||||
yield if should_continue
|
||||
after(context)
|
||||
end
|
||||
end
|
||||
|
||||
def should_run_callback?(*args)
|
||||
[options[:if]].flatten.compact.all? { |a| evaluate_method(a, *args) } &&
|
||||
![options[:unless]].flatten.compact.any? { |a| evaluate_method(a, *args) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
# An Array with a compile method
|
||||
class CallbackChain < Array #:nodoc:#
|
||||
attr_reader :name, :config
|
||||
|
||||
def initialize(name, config)
|
||||
@name = name
|
||||
@config = {
|
||||
:terminator => "false",
|
||||
:rescuable => false,
|
||||
:scope => [ :kind ]
|
||||
}.merge(config)
|
||||
end
|
||||
|
||||
def compile(key=nil, object=nil)
|
||||
method = []
|
||||
method << "value = nil"
|
||||
method << "halted = false"
|
||||
|
||||
each do |callback|
|
||||
method << callback.start(key, object)
|
||||
end
|
||||
|
||||
if config[:rescuable]
|
||||
method << "rescued_error = nil"
|
||||
method << "begin"
|
||||
end
|
||||
|
||||
method << "value = yield if block_given? && !halted"
|
||||
|
||||
if config[:rescuable]
|
||||
method << "rescue Exception => e"
|
||||
method << "rescued_error = e"
|
||||
method << "end"
|
||||
end
|
||||
|
||||
reverse_each do |callback|
|
||||
method << callback.end(key, object)
|
||||
end
|
||||
|
||||
method << "raise rescued_error if rescued_error" if config[:rescuable]
|
||||
method << "halted ? false : (block_given? ? value : true)"
|
||||
method.compact.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Generate the internal runner method called by +run_callbacks+.
|
||||
def __define_runner(symbol) #:nodoc:
|
||||
runner_method = "_run_#{symbol}_callbacks"
|
||||
unless private_method_defined?(runner_method)
|
||||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{runner_method}(key = nil, &blk)
|
||||
self.class.__run_callback(key, :#{symbol}, self, &blk)
|
||||
end
|
||||
private :#{runner_method}
|
||||
RUBY_EVAL
|
||||
end
|
||||
end
|
||||
|
||||
# This method calls the callback method for the given key.
|
||||
# If this called first time it creates a new callback method for the key,
|
||||
# calculating which callbacks can be omitted because of per_key conditions.
|
||||
#
|
||||
def __run_callback(key, kind, object, &blk) #:nodoc:
|
||||
name = __callback_runner_name(key, kind)
|
||||
unless object.respond_to?(name, true)
|
||||
str = object.send("_#{kind}_callbacks").compile(key, object)
|
||||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{name}() #{str} end
|
||||
protected :#{name}
|
||||
RUBY_EVAL
|
||||
end
|
||||
object.send(name, &blk)
|
||||
end
|
||||
|
||||
def __reset_runner(symbol)
|
||||
name = __callback_runner_name(nil, symbol)
|
||||
undef_method(name) if method_defined?(name)
|
||||
end
|
||||
|
||||
def __callback_runner_name(key, kind)
|
||||
"_run__#{self.name.hash.abs}__#{kind}__#{key.hash.abs}__callbacks"
|
||||
end
|
||||
|
||||
# This is used internally to append, prepend and skip callbacks to the
|
||||
# CallbackChain.
|
||||
#
|
||||
def __update_callbacks(name, filters = [], block = nil) #:nodoc:
|
||||
type = filters.first.in?([:before, :after, :around]) ? filters.shift : :before
|
||||
options = filters.last.is_a?(Hash) ? filters.pop : {}
|
||||
filters.unshift(block) if block
|
||||
|
||||
([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse.each do |target|
|
||||
chain = target.send("_#{name}_callbacks")
|
||||
yield target, chain.dup, type, filters, options
|
||||
target.__reset_runner(name)
|
||||
end
|
||||
end
|
||||
|
||||
# Install a callback for the given event.
|
||||
#
|
||||
# set_callback :save, :before, :before_meth
|
||||
# set_callback :save, :after, :after_meth, :if => :condition
|
||||
# set_callback :save, :around, lambda { |r| stuff; result = yield; stuff }
|
||||
#
|
||||
# The second arguments indicates whether the callback is to be run +:before+,
|
||||
# +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
|
||||
# means the first example above can also be written as:
|
||||
#
|
||||
# set_callback :save, :before_meth
|
||||
#
|
||||
# The callback can specified as a symbol naming an instance method; as a proc,
|
||||
# lambda, or block; as a string to be instance evaluated; or as an object that
|
||||
# responds to a certain method determined by the <tt>:scope</tt> argument to
|
||||
# +define_callback+.
|
||||
#
|
||||
# If a proc, lambda, or block is given, its body is evaluated in the context
|
||||
# of the current object. It can also optionally accept the current object as
|
||||
# an argument.
|
||||
#
|
||||
# Before and around callbacks are called in the order that they are set; after
|
||||
# callbacks are called in the reverse order.
|
||||
#
|
||||
# Around callbacks can access the return value from the event, if it
|
||||
# wasn't halted, from the +yield+ call.
|
||||
#
|
||||
# ===== Options
|
||||
#
|
||||
# * <tt>:if</tt> - A symbol naming an instance method or a proc; the callback
|
||||
# will be called only when it returns a true value.
|
||||
# * <tt>:unless</tt> - A symbol naming an instance method or a proc; the callback
|
||||
# will be called only when it returns a false value.
|
||||
# * <tt>:prepend</tt> - If true, the callback will be prepended to the existing
|
||||
# chain rather than appended.
|
||||
# * <tt>:per_key</tt> - A hash with <tt>:if</tt> and <tt>:unless</tt> options;
|
||||
# see "Per-key conditions" below.
|
||||
#
|
||||
# ===== Per-key conditions
|
||||
#
|
||||
# When creating or skipping callbacks, you can specify conditions that
|
||||
# are always the same for a given key. For instance, in Action Pack,
|
||||
# we convert :only and :except conditions into per-key conditions.
|
||||
#
|
||||
# before_filter :authenticate, :except => "index"
|
||||
#
|
||||
# becomes
|
||||
#
|
||||
# set_callback :process_action, :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}}
|
||||
#
|
||||
# Per-key conditions are evaluated only once per use of a given key.
|
||||
# In the case of the above example, you would do:
|
||||
#
|
||||
# run_callbacks(:process_action, action_name) { ... dispatch stuff ... }
|
||||
#
|
||||
# In that case, each action_name would get its own compiled callback
|
||||
# method that took into consideration the per_key conditions. This
|
||||
# is a speed improvement for ActionPack.
|
||||
#
|
||||
def set_callback(name, *filter_list, &block)
|
||||
mapped = nil
|
||||
|
||||
__update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
|
||||
mapped ||= filters.map do |filter|
|
||||
Callback.new(chain, filter, type, options.dup, self)
|
||||
end
|
||||
|
||||
filters.each do |filter|
|
||||
chain.delete_if {|c| c.matches?(type, filter) }
|
||||
end
|
||||
|
||||
options[:prepend] ? chain.unshift(*(mapped.reverse)) : chain.push(*mapped)
|
||||
|
||||
target.send("_#{name}_callbacks=", chain)
|
||||
end
|
||||
end
|
||||
|
||||
# Skip a previously set callback. Like +set_callback+, <tt>:if</tt> or <tt>:unless</tt>
|
||||
# options may be passed in order to control when the callback is skipped.
|
||||
#
|
||||
# class Writer < Person
|
||||
# skip_callback :validate, :before, :check_membership, :if => lambda { self.age > 18 }
|
||||
# end
|
||||
#
|
||||
def skip_callback(name, *filter_list, &block)
|
||||
__update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
|
||||
filters.each do |filter|
|
||||
filter = chain.find {|c| c.matches?(type, filter) }
|
||||
|
||||
if filter && options.any?
|
||||
new_filter = filter.clone(chain, self)
|
||||
chain.insert(chain.index(filter), new_filter)
|
||||
new_filter.recompile!(options, options[:per_key] || {})
|
||||
end
|
||||
|
||||
chain.delete(filter)
|
||||
end
|
||||
target.send("_#{name}_callbacks=", chain)
|
||||
end
|
||||
end
|
||||
|
||||
# Remove all set callbacks for the given event.
|
||||
#
|
||||
def reset_callbacks(symbol)
|
||||
callbacks = send("_#{symbol}_callbacks")
|
||||
|
||||
ActiveSupport::DescendantsTracker.descendants(self).each do |target|
|
||||
chain = target.send("_#{symbol}_callbacks").dup
|
||||
callbacks.each { |c| chain.delete(c) }
|
||||
target.send("_#{symbol}_callbacks=", chain)
|
||||
target.__reset_runner(symbol)
|
||||
end
|
||||
|
||||
self.send("_#{symbol}_callbacks=", callbacks.dup.clear)
|
||||
|
||||
__reset_runner(symbol)
|
||||
end
|
||||
|
||||
# Define sets of events in the object lifecycle that support callbacks.
|
||||
#
|
||||
# define_callbacks :validate
|
||||
# define_callbacks :initialize, :save, :destroy
|
||||
#
|
||||
# ===== Options
|
||||
#
|
||||
# * <tt>:terminator</tt> - Determines when a before filter will halt the callback
|
||||
# chain, preventing following callbacks from being called and the event from being
|
||||
# triggered. This is a string to be eval'ed. The result of the callback is available
|
||||
# in the <tt>result</tt> variable.
|
||||
#
|
||||
# define_callbacks :validate, :terminator => "result == false"
|
||||
#
|
||||
# In this example, if any before validate callbacks returns +false+,
|
||||
# other callbacks are not executed. Defaults to "false", meaning no value
|
||||
# halts the chain.
|
||||
#
|
||||
# * <tt>:rescuable</tt> - By default, after filters are not executed if
|
||||
# the given block or a before filter raises an error. By setting this option
|
||||
# to <tt>true</tt> exception raised by given block is stored and after
|
||||
# executing all the after callbacks the stored exception is raised.
|
||||
#
|
||||
# * <tt>:scope</tt> - Indicates which methods should be executed when an object
|
||||
# is used as a callback.
|
||||
#
|
||||
# class Audit
|
||||
# def before(caller)
|
||||
# puts 'Audit: before is called'
|
||||
# end
|
||||
#
|
||||
# def before_save(caller)
|
||||
# puts 'Audit: before_save is called'
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class Account
|
||||
# include ActiveSupport::Callbacks
|
||||
#
|
||||
# define_callbacks :save
|
||||
# set_callback :save, :before, Audit.new
|
||||
#
|
||||
# def save
|
||||
# run_callbacks :save do
|
||||
# puts 'save in main'
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# In the above case whenever you save an account the method <tt>Audit#before</tt> will
|
||||
# be called. On the other hand
|
||||
#
|
||||
# define_callbacks :save, :scope => [:kind, :name]
|
||||
#
|
||||
# would trigger <tt>Audit#before_save</tt> instead. That's constructed by calling
|
||||
# <tt>#{kind}_#{name}</tt> on the given instance. In this case "kind" is "before" and
|
||||
# "name" is "save". In this context +:kind+ and +:name+ have special meanings: +:kind+
|
||||
# refers to the kind of callback (before/after/around) and +:name+ refers to the
|
||||
# method on which callbacks are being defined.
|
||||
#
|
||||
# A declaration like
|
||||
#
|
||||
# define_callbacks :save, :scope => [:name]
|
||||
#
|
||||
# would call <tt>Audit#save</tt>.
|
||||
#
|
||||
def define_callbacks(*callbacks)
|
||||
config = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
|
||||
callbacks.each do |callback|
|
||||
class_eval <<-"end_eval"
|
||||
def self.#{callback}(*methods, &block) # def self.before_save(*methods, &block)
|
||||
callbacks = CallbackChain.build(:#{callback}, *methods, &block) # callbacks = CallbackChain.build(:before_save, *methods, &block)
|
||||
@#{callback}_callbacks ||= CallbackChain.new # @before_save_callbacks ||= CallbackChain.new
|
||||
@#{callback}_callbacks.concat callbacks # @before_save_callbacks.concat callbacks
|
||||
end # end
|
||||
#
|
||||
def self.#{callback}_callback_chain # def self.before_save_callback_chain
|
||||
@#{callback}_callbacks ||= CallbackChain.new # @before_save_callbacks ||= CallbackChain.new
|
||||
#
|
||||
if superclass.respond_to?(:#{callback}_callback_chain) # if superclass.respond_to?(:before_save_callback_chain)
|
||||
CallbackChain.new( # CallbackChain.new(
|
||||
superclass.#{callback}_callback_chain + # superclass.before_save_callback_chain +
|
||||
@#{callback}_callbacks # @before_save_callbacks
|
||||
) # )
|
||||
else # else
|
||||
@#{callback}_callbacks # @before_save_callbacks
|
||||
end # end
|
||||
end # end
|
||||
end_eval
|
||||
class_attribute "_#{callback}_callbacks"
|
||||
send("_#{callback}_callbacks=", CallbackChain.new(callback, config))
|
||||
__define_runner(callback)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Runs all the callbacks defined for the given options.
|
||||
#
|
||||
# If a block is given it will be called after each callback receiving as arguments:
|
||||
#
|
||||
# * the result from the callback
|
||||
# * the object which has the callback
|
||||
#
|
||||
# If the result from the block evaluates to false, the callback chain is stopped.
|
||||
#
|
||||
# Example:
|
||||
# class Storage
|
||||
# include ActiveSupport::Callbacks
|
||||
#
|
||||
# define_callbacks :before_save, :after_save
|
||||
# end
|
||||
#
|
||||
# class ConfigStorage < Storage
|
||||
# before_save :pass
|
||||
# before_save :pass
|
||||
# before_save :stop
|
||||
# before_save :pass
|
||||
#
|
||||
# def pass
|
||||
# puts "pass"
|
||||
# end
|
||||
#
|
||||
# def stop
|
||||
# puts "stop"
|
||||
# return false
|
||||
# end
|
||||
#
|
||||
# def save
|
||||
# result = run_callbacks(:before_save) { |result, object| result == false }
|
||||
# puts "- save" if result
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# config = ConfigStorage.new
|
||||
# config.save
|
||||
#
|
||||
# Output:
|
||||
# pass
|
||||
# pass
|
||||
# stop
|
||||
def run_callbacks(kind, options = {}, &block)
|
||||
self.class.send("#{kind}_callback_chain").run(self, options, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require 'active_support/deprecation'
|
||||
|
||||
module ActiveSupport
|
||||
# A typical module looks like this:
|
||||
#
|
||||
@@ -5,7 +7,7 @@ module ActiveSupport
|
||||
# def self.included(base)
|
||||
# base.extend ClassMethods
|
||||
# base.class_eval do
|
||||
# scope :disabled, -> { where(disabled: true) }
|
||||
# scope :disabled, where(:disabled => true)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
@@ -14,8 +16,7 @@ module ActiveSupport
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be
|
||||
# written as:
|
||||
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be written as:
|
||||
#
|
||||
# require 'active_support/concern'
|
||||
#
|
||||
@@ -23,7 +24,7 @@ module ActiveSupport
|
||||
# extend ActiveSupport::Concern
|
||||
#
|
||||
# included do
|
||||
# scope :disabled, -> { where(disabled: true) }
|
||||
# scope :disabled, where(:disabled => true)
|
||||
# end
|
||||
#
|
||||
# module ClassMethods
|
||||
@@ -31,9 +32,8 @@ module ActiveSupport
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module
|
||||
# and a +Bar+ module which depends on the former, we would typically write the
|
||||
# following:
|
||||
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module and a +Bar+
|
||||
# module which depends on the former, we would typically write the following:
|
||||
#
|
||||
# module Foo
|
||||
# def self.included(base)
|
||||
@@ -56,11 +56,11 @@ module ActiveSupport
|
||||
# include Bar # Bar is the module that Host really needs
|
||||
# end
|
||||
#
|
||||
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
|
||||
# could try to hide these from +Host+ directly including +Foo+ in +Bar+:
|
||||
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We could try to hide
|
||||
# these from +Host+ directly including +Foo+ in +Bar+:
|
||||
#
|
||||
# module Bar
|
||||
# include Foo
|
||||
# include Foo
|
||||
# def self.included(base)
|
||||
# base.method_injected_by_foo
|
||||
# end
|
||||
@@ -70,17 +70,18 @@ module ActiveSupport
|
||||
# include Bar
|
||||
# end
|
||||
#
|
||||
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
|
||||
# is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
|
||||
# module dependencies are properly resolved:
|
||||
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt> is the +Bar+ module,
|
||||
# not the +Host+ class. With <tt>ActiveSupport::Concern</tt>, module dependencies are properly resolved:
|
||||
#
|
||||
# require 'active_support/concern'
|
||||
#
|
||||
# module Foo
|
||||
# extend ActiveSupport::Concern
|
||||
# included do
|
||||
# def self.method_injected_by_foo
|
||||
# ...
|
||||
# class_eval do
|
||||
# def self.method_injected_by_foo
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
@@ -97,38 +98,36 @@ module ActiveSupport
|
||||
# class Host
|
||||
# include Bar # works, Bar takes care now of its dependencies
|
||||
# end
|
||||
#
|
||||
module Concern
|
||||
class MultipleIncludedBlocks < StandardError #:nodoc:
|
||||
def initialize
|
||||
super "Cannot define multiple 'included' blocks for a Concern"
|
||||
end
|
||||
end
|
||||
|
||||
def self.extended(base) #:nodoc:
|
||||
base.instance_variable_set(:@_dependencies, [])
|
||||
def self.extended(base)
|
||||
base.instance_variable_set("@_dependencies", [])
|
||||
end
|
||||
|
||||
def append_features(base)
|
||||
if base.instance_variable_defined?(:@_dependencies)
|
||||
base.instance_variable_get(:@_dependencies) << self
|
||||
if base.instance_variable_defined?("@_dependencies")
|
||||
base.instance_variable_get("@_dependencies") << self
|
||||
return false
|
||||
else
|
||||
return false if base < self
|
||||
@_dependencies.each { |dep| base.send(:include, dep) }
|
||||
super
|
||||
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
|
||||
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
|
||||
base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
|
||||
if const_defined?("InstanceMethods")
|
||||
base.send :include, const_get("InstanceMethods")
|
||||
ActiveSupport::Deprecation.warn "The InstanceMethods module inside ActiveSupport::Concern will be " \
|
||||
"no longer included automatically. Please define instance methods directly in #{self} instead.", caller
|
||||
end
|
||||
base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
|
||||
end
|
||||
end
|
||||
|
||||
def included(base = nil, &block)
|
||||
if base.nil?
|
||||
raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
|
||||
|
||||
@_included_block = block
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
90
activesupport/lib/active_support/configurable.rb
Normal file
90
activesupport/lib/active_support/configurable.rb
Normal file
@@ -0,0 +1,90 @@
|
||||
require 'active_support/concern'
|
||||
require 'active_support/ordered_options'
|
||||
require 'active_support/core_ext/kernel/singleton_class'
|
||||
require 'active_support/core_ext/module/delegation'
|
||||
require 'active_support/core_ext/array/extract_options'
|
||||
|
||||
module ActiveSupport
|
||||
# Configurable provides a <tt>config</tt> method to store and retrieve
|
||||
# configuration options as an <tt>OrderedHash</tt>.
|
||||
module Configurable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class Configuration < ActiveSupport::InheritableOptions
|
||||
def compile_methods!
|
||||
self.class.compile_methods!(keys)
|
||||
end
|
||||
|
||||
# compiles reader methods so we don't have to go through method_missing
|
||||
def self.compile_methods!(keys)
|
||||
keys.reject { |m| method_defined?(m) }.each do |key|
|
||||
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
||||
def #{key}; _get(#{key.inspect}); end
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def config
|
||||
@_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
|
||||
superclass.config.inheritable_copy
|
||||
else
|
||||
# create a new "anonymous" class that will host the compiled reader methods
|
||||
Class.new(Configuration).new
|
||||
end
|
||||
end
|
||||
|
||||
def configure
|
||||
yield config
|
||||
end
|
||||
|
||||
# Allows you to add shortcut so that you don't have to refer to attribute through config.
|
||||
# Also look at the example for config to contrast.
|
||||
#
|
||||
# class User
|
||||
# include ActiveSupport::Configurable
|
||||
# config_accessor :allowed_access
|
||||
# end
|
||||
#
|
||||
# user = User.new
|
||||
# user.allowed_access = true
|
||||
# user.allowed_access # => true
|
||||
#
|
||||
def config_accessor(*names)
|
||||
options = names.extract_options!
|
||||
|
||||
names.each do |name|
|
||||
reader, line = "def #{name}; config.#{name}; end", __LINE__
|
||||
writer, line = "def #{name}=(value); config.#{name} = value; end", __LINE__
|
||||
|
||||
singleton_class.class_eval reader, __FILE__, line
|
||||
singleton_class.class_eval writer, __FILE__, line
|
||||
class_eval reader, __FILE__, line unless options[:instance_reader] == false
|
||||
class_eval writer, __FILE__, line unless options[:instance_writer] == false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Reads and writes attributes from a configuration <tt>OrderedHash</tt>.
|
||||
#
|
||||
# require 'active_support/configurable'
|
||||
#
|
||||
# class User
|
||||
# include ActiveSupport::Configurable
|
||||
# end
|
||||
#
|
||||
# user = User.new
|
||||
#
|
||||
# user.config.allowed_access = true
|
||||
# user.config.level = 1
|
||||
#
|
||||
# user.config.allowed_access # => true
|
||||
# user.config.level # => 1
|
||||
#
|
||||
def config
|
||||
@_config ||= self.class.config.inheritable_copy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
filenames = Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].sort.map do |path|
|
||||
File.basename(path, '.rb')
|
||||
Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].sort.each do |path|
|
||||
require "active_support/core_ext/#{File.basename(path, '.rb')}"
|
||||
end
|
||||
|
||||
# deprecated
|
||||
filenames -= %w(blank)
|
||||
|
||||
filenames.each { |filename| require "active_support/core_ext/#{filename}" }
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
require 'active_support/core_ext/array/wrap'
|
||||
require 'active_support/core_ext/array/access'
|
||||
require 'active_support/core_ext/array/uniq_by'
|
||||
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
|
||||
include ActiveSupport::CoreExtensions::Array::Conversions
|
||||
include ActiveSupport::CoreExtensions::Array::ExtractOptions
|
||||
include ActiveSupport::CoreExtensions::Array::Grouping
|
||||
include ActiveSupport::CoreExtensions::Array::RandomAccess
|
||||
extend ActiveSupport::CoreExtensions::Array::Wrapper
|
||||
end
|
||||
require 'active_support/core_ext/array/prepend_and_append'
|
||||
|
||||
@@ -1,53 +1,46 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
# Makes it easier to access parts of an array.
|
||||
module Access
|
||||
# Returns the tail of the array from +position+.
|
||||
#
|
||||
# %w( a b c d ).from(0) # => %w( a b c d )
|
||||
# %w( a b c d ).from(2) # => %w( c d )
|
||||
# %w( a b c d ).from(10) # => nil
|
||||
# %w().from(0) # => nil
|
||||
def from(position)
|
||||
self[position..-1]
|
||||
end
|
||||
|
||||
# Returns the beginning of the array up to +position+.
|
||||
#
|
||||
# %w( a b c d ).to(0) # => %w( a )
|
||||
# %w( a b c d ).to(2) # => %w( a b c )
|
||||
# %w( a b c d ).to(10) # => %w( a b c d )
|
||||
# %w().to(0) # => %w()
|
||||
def to(position)
|
||||
self[0..position]
|
||||
end
|
||||
class Array
|
||||
# Returns the tail of the array from +position+.
|
||||
#
|
||||
# %w( a b c d ).from(0) # => %w( a b c d )
|
||||
# %w( a b c d ).from(2) # => %w( c d )
|
||||
# %w( a b c d ).from(10) # => %w()
|
||||
# %w().from(0) # => %w()
|
||||
def from(position)
|
||||
self[position, length] || []
|
||||
end
|
||||
|
||||
# Equal to <tt>self[1]</tt>.
|
||||
def second
|
||||
self[1]
|
||||
end
|
||||
# Returns the beginning of the array up to +position+.
|
||||
#
|
||||
# %w( a b c d ).to(0) # => %w( a )
|
||||
# %w( a b c d ).to(2) # => %w( a b c )
|
||||
# %w( a b c d ).to(10) # => %w( a b c d )
|
||||
# %w().to(0) # => %w()
|
||||
def to(position)
|
||||
self.first position + 1
|
||||
end
|
||||
|
||||
# Equal to <tt>self[2]</tt>.
|
||||
def third
|
||||
self[2]
|
||||
end
|
||||
# Equal to <tt>self[1]</tt>.
|
||||
def second
|
||||
self[1]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[3]</tt>.
|
||||
def fourth
|
||||
self[3]
|
||||
end
|
||||
# Equal to <tt>self[2]</tt>.
|
||||
def third
|
||||
self[2]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[4]</tt>.
|
||||
def fifth
|
||||
self[4]
|
||||
end
|
||||
# Equal to <tt>self[3]</tt>.
|
||||
def fourth
|
||||
self[3]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
|
||||
def forty_two
|
||||
self[41]
|
||||
end
|
||||
end
|
||||
end
|
||||
# Equal to <tt>self[4]</tt>.
|
||||
def fifth
|
||||
self[4]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
|
||||
def forty_two
|
||||
self[41]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,197 +1,164 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module Conversions
|
||||
# Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options:
|
||||
# * <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 = {})
|
||||
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])
|
||||
require 'active_support/xml_mini'
|
||||
require 'active_support/core_ext/hash/keys'
|
||||
require 'active_support/core_ext/hash/reverse_merge'
|
||||
require 'active_support/core_ext/string/inflections'
|
||||
|
||||
# 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
|
||||
class Array
|
||||
# Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options:
|
||||
# * <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 = {})
|
||||
if defined?(I18n)
|
||||
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])
|
||||
else
|
||||
default_words_connector = ", "
|
||||
default_two_words_connector = " and "
|
||||
default_last_word_connector = ", and "
|
||||
end
|
||||
|
||||
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
|
||||
""
|
||||
when 1
|
||||
self[0].to_s
|
||||
when 2
|
||||
"#{self[0]}#{options[:two_words_connector]}#{self[1]}"
|
||||
else
|
||||
"#{self[0...-1].join(options[:words_connector])}#{options[:last_word_connector]}#{self[-1]}"
|
||||
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
|
||||
|
||||
# Calls <tt>to_param</tt> on all its elements and joins the result with
|
||||
# slashes. This is used by <tt>url_for</tt> in Action Pack.
|
||||
def to_param
|
||||
collect { |e| e.to_param }.join '/'
|
||||
end
|
||||
|
||||
# Converts an array into a string suitable for use as a URL query string,
|
||||
# using the given +key+ as the param name.
|
||||
#
|
||||
# ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
|
||||
def to_query(key)
|
||||
prefix = "#{key}[]"
|
||||
collect { |value| value.to_query(prefix) }.join '&'
|
||||
end
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
base.class_eval do
|
||||
alias_method :to_default_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
end
|
||||
end
|
||||
|
||||
# Converts a collection of elements into a formatted string by calling
|
||||
# <tt>to_s</tt> on all elements and joining them:
|
||||
#
|
||||
# Blog.find(:all).to_formatted_s # => "First PostSecond PostThird Post"
|
||||
#
|
||||
# Adding in the <tt>:db</tt> argument as the format yields a prettier
|
||||
# output:
|
||||
#
|
||||
# Blog.find(:all).to_formatted_s(:db) # => "First Post,Second Post,Third Post"
|
||||
def to_formatted_s(format = :default)
|
||||
case format
|
||||
when :db
|
||||
if respond_to?(:empty?) && self.empty?
|
||||
"null"
|
||||
else
|
||||
collect { |element| element.id }.join(",")
|
||||
end
|
||||
else
|
||||
to_default_s
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a string that represents this array in XML by sending +to_xml+
|
||||
# to each element. Active Record collections delegate their representation
|
||||
# in XML to this method.
|
||||
#
|
||||
# All elements are expected to respond to +to_xml+, if any of them does
|
||||
# not an exception is raised.
|
||||
#
|
||||
# The root node reflects the class name of the first element in plural
|
||||
# if all elements belong to the same type and that's not Hash:
|
||||
#
|
||||
# customer.projects.to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <projects type="array">
|
||||
# <project>
|
||||
# <amount type="decimal">20000.0</amount>
|
||||
# <customer-id type="integer">1567</customer-id>
|
||||
# <deal-date type="date">2008-04-09</deal-date>
|
||||
# ...
|
||||
# </project>
|
||||
# <project>
|
||||
# <amount type="decimal">57230.0</amount>
|
||||
# <customer-id type="integer">1567</customer-id>
|
||||
# <deal-date type="date">2008-04-15</deal-date>
|
||||
# ...
|
||||
# </project>
|
||||
# </projects>
|
||||
#
|
||||
# Otherwise the root element is "records":
|
||||
#
|
||||
# [{:foo => 1, :bar => 2}, {:baz => 3}].to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <records type="array">
|
||||
# <record>
|
||||
# <bar type="integer">2</bar>
|
||||
# <foo type="integer">1</foo>
|
||||
# </record>
|
||||
# <record>
|
||||
# <baz type="integer">3</baz>
|
||||
# </record>
|
||||
# </records>
|
||||
#
|
||||
# If the collection is empty the root element is "nil-classes" by default:
|
||||
#
|
||||
# [].to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <nil-classes type="array"/>
|
||||
#
|
||||
# To ensure a meaningful root element use the <tt>:root</tt> option:
|
||||
#
|
||||
# customer_with_no_projects.projects.to_xml(:root => "projects")
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <projects type="array"/>
|
||||
#
|
||||
# By default root children have as node name the one of the root
|
||||
# singularized. You can change it with the <tt>:children</tt> option.
|
||||
#
|
||||
# The +options+ hash is passed downwards:
|
||||
#
|
||||
# Message.all.to_xml(:skip_types => true)
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <messages>
|
||||
# <message>
|
||||
# <created-at>2008-03-07T09:58:18+01:00</created-at>
|
||||
# <id>1</id>
|
||||
# <name>1</name>
|
||||
# <updated-at>2008-03-07T09:58:18+01:00</updated-at>
|
||||
# <user-id>1</user-id>
|
||||
# </message>
|
||||
# </messages>
|
||||
#
|
||||
def to_xml(options = {})
|
||||
raise "Not all elements respond to to_xml" unless all? { |e| e.respond_to? :to_xml }
|
||||
require 'builder' unless defined?(Builder)
|
||||
|
||||
options = options.dup
|
||||
options[:root] ||= all? { |e| e.is_a?(first.class) && first.class.to_s != "Hash" } ? first.class.to_s.underscore.pluralize.tr('/', '-') : "records"
|
||||
options[:children] ||= options[:root].singularize
|
||||
options[:indent] ||= 2
|
||||
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
||||
|
||||
root = options.delete(:root).to_s
|
||||
children = options.delete(:children)
|
||||
|
||||
if !options.has_key?(:dasherize) || options[:dasherize]
|
||||
root = root.dasherize
|
||||
end
|
||||
|
||||
options[:builder].instruct! unless options.delete(:skip_instruct)
|
||||
|
||||
opts = options.merge({ :root => children })
|
||||
|
||||
xml = options[:builder]
|
||||
if empty?
|
||||
xml.tag!(root, options[:skip_types] ? {} : {:type => "array"})
|
||||
else
|
||||
xml.tag!(root, options[:skip_types] ? {} : {:type => "array"}) {
|
||||
yield xml if block_given?
|
||||
each { |e| e.to_xml(opts.merge({ :skip_instruct => true })) }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
case length
|
||||
when 0
|
||||
""
|
||||
when 1
|
||||
self[0].to_s.dup
|
||||
when 2
|
||||
"#{self[0]}#{options[:two_words_connector]}#{self[1]}"
|
||||
else
|
||||
"#{self[0...-1].join(options[:words_connector])}#{options[:last_word_connector]}#{self[-1]}"
|
||||
end
|
||||
end
|
||||
|
||||
# Converts a collection of elements into a formatted string by calling
|
||||
# <tt>to_s</tt> on all elements and joining them:
|
||||
#
|
||||
# Blog.all.to_formatted_s # => "First PostSecond PostThird Post"
|
||||
#
|
||||
# Adding in the <tt>:db</tt> argument as the format yields a comma separated
|
||||
# id list:
|
||||
#
|
||||
# Blog.all.to_formatted_s(:db) # => "1,2,3"
|
||||
def to_formatted_s(format = :default)
|
||||
case format
|
||||
when :db
|
||||
if respond_to?(:empty?) && self.empty?
|
||||
"null"
|
||||
else
|
||||
collect { |element| element.id }.join(",")
|
||||
end
|
||||
else
|
||||
to_default_s
|
||||
end
|
||||
end
|
||||
alias_method :to_default_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
|
||||
# Returns a string that represents the array in XML by invoking +to_xml+
|
||||
# on each element. Active Record collections delegate their representation
|
||||
# in XML to this method.
|
||||
#
|
||||
# All elements are expected to respond to +to_xml+, if any of them does
|
||||
# not then an exception is raised.
|
||||
#
|
||||
# The root node reflects the class name of the first element in plural
|
||||
# if all elements belong to the same type and that's not Hash:
|
||||
#
|
||||
# customer.projects.to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <projects type="array">
|
||||
# <project>
|
||||
# <amount type="decimal">20000.0</amount>
|
||||
# <customer-id type="integer">1567</customer-id>
|
||||
# <deal-date type="date">2008-04-09</deal-date>
|
||||
# ...
|
||||
# </project>
|
||||
# <project>
|
||||
# <amount type="decimal">57230.0</amount>
|
||||
# <customer-id type="integer">1567</customer-id>
|
||||
# <deal-date type="date">2008-04-15</deal-date>
|
||||
# ...
|
||||
# </project>
|
||||
# </projects>
|
||||
#
|
||||
# Otherwise the root element is "records":
|
||||
#
|
||||
# [{:foo => 1, :bar => 2}, {:baz => 3}].to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <records type="array">
|
||||
# <record>
|
||||
# <bar type="integer">2</bar>
|
||||
# <foo type="integer">1</foo>
|
||||
# </record>
|
||||
# <record>
|
||||
# <baz type="integer">3</baz>
|
||||
# </record>
|
||||
# </records>
|
||||
#
|
||||
# If the collection is empty the root element is "nil-classes" by default:
|
||||
#
|
||||
# [].to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <nil-classes type="array"/>
|
||||
#
|
||||
# To ensure a meaningful root element use the <tt>:root</tt> option:
|
||||
#
|
||||
# customer_with_no_projects.projects.to_xml(:root => "projects")
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <projects type="array"/>
|
||||
#
|
||||
# By default name of the node for the children of root is <tt>root.singularize</tt>.
|
||||
# You can change it with the <tt>:children</tt> option.
|
||||
#
|
||||
# The +options+ hash is passed downwards:
|
||||
#
|
||||
# Message.all.to_xml(:skip_types => true)
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <messages>
|
||||
# <message>
|
||||
# <created-at>2008-03-07T09:58:18+01:00</created-at>
|
||||
# <id>1</id>
|
||||
# <name>1</name>
|
||||
# <updated-at>2008-03-07T09:58:18+01:00</updated-at>
|
||||
# <user-id>1</user-id>
|
||||
# </message>
|
||||
# </messages>
|
||||
#
|
||||
def to_xml(options = {})
|
||||
require 'active_support/builder' unless defined?(Builder)
|
||||
|
||||
options = options.dup
|
||||
options[:indent] ||= 2
|
||||
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
||||
options[:root] ||= if first.class.to_s != "Hash" && all? { |e| e.is_a?(first.class) }
|
||||
underscored = ActiveSupport::Inflector.underscore(first.class.name)
|
||||
ActiveSupport::Inflector.pluralize(underscored).tr('/', '_')
|
||||
else
|
||||
"objects"
|
||||
end
|
||||
|
||||
builder = options[:builder]
|
||||
builder.instruct! unless options.delete(:skip_instruct)
|
||||
|
||||
root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
|
||||
children = options.delete(:children) || root.singularize
|
||||
|
||||
attributes = options[:skip_types] ? {} : {:type => "array"}
|
||||
return builder.tag!(root, attributes) if empty?
|
||||
|
||||
builder.__send__(:method_missing, root, attributes) do
|
||||
each { |value| ActiveSupport::XmlMini.to_tag(children, value, options) }
|
||||
yield builder if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module ExtractOptions
|
||||
# Extracts options from a set of arguments. Removes and returns the last
|
||||
# element in the array if it's a hash, otherwise returns a blank hash.
|
||||
#
|
||||
# def options(*args)
|
||||
# args.extract_options!
|
||||
# end
|
||||
#
|
||||
# options(1, 2) # => {}
|
||||
# options(1, 2, :a => :b) # => {:a=>:b}
|
||||
def extract_options!
|
||||
last.is_a?(::Hash) ? pop : {}
|
||||
end
|
||||
end
|
||||
class Hash
|
||||
# By default, only instances of Hash itself are extractable.
|
||||
# Subclasses of Hash may implement this method and return
|
||||
# true to declare themselves as extractable. If a Hash
|
||||
# is extractable, Array#extract_options! pops it from
|
||||
# the Array when it is the last element of the Array.
|
||||
def extractable_options?
|
||||
instance_of?(Hash)
|
||||
end
|
||||
end
|
||||
|
||||
class Array
|
||||
# Extracts options from a set of arguments. Removes and returns the last
|
||||
# element in the array if it's a hash, otherwise returns a blank hash.
|
||||
#
|
||||
# def options(*args)
|
||||
# args.extract_options!
|
||||
# end
|
||||
#
|
||||
# options(1, 2) # => {}
|
||||
# options(1, 2, :a => :b) # => {:a=>:b}
|
||||
def extract_options!
|
||||
if last.is_a?(Hash) && last.extractable_options?
|
||||
pop
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,106 +1,100 @@
|
||||
require 'enumerator'
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module Grouping
|
||||
# Splits or iterates over the array in groups of size +number+,
|
||||
# padding any remaining slots with +fill_with+ unless it is +false+.
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups_of(3) {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5", "6"]
|
||||
# ["7", nil, nil]
|
||||
#
|
||||
# %w(1 2 3).in_groups_of(2, ' ') {|group| p group}
|
||||
# ["1", "2"]
|
||||
# ["3", " "]
|
||||
#
|
||||
# %w(1 2 3).in_groups_of(2, false) {|group| p group}
|
||||
# ["1", "2"]
|
||||
# ["3"]
|
||||
def in_groups_of(number, fill_with = nil)
|
||||
if fill_with == false
|
||||
collection = self
|
||||
else
|
||||
# size % number gives how many extra we have;
|
||||
# subtracting from number gives how many to add;
|
||||
# modulo number ensures we don't add group of just fill.
|
||||
padding = (number - size % number) % number
|
||||
collection = dup.concat([fill_with] * padding)
|
||||
end
|
||||
class Array
|
||||
# Splits or iterates over the array in groups of size +number+,
|
||||
# padding any remaining slots with +fill_with+ unless it is +false+.
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups_of(3) {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5", "6"]
|
||||
# ["7", nil, nil]
|
||||
#
|
||||
# %w(1 2 3).in_groups_of(2, ' ') {|group| p group}
|
||||
# ["1", "2"]
|
||||
# ["3", " "]
|
||||
#
|
||||
# %w(1 2 3).in_groups_of(2, false) {|group| p group}
|
||||
# ["1", "2"]
|
||||
# ["3"]
|
||||
def in_groups_of(number, fill_with = nil)
|
||||
if fill_with == false
|
||||
collection = self
|
||||
else
|
||||
# size % number gives how many extra we have;
|
||||
# subtracting from number gives how many to add;
|
||||
# modulo number ensures we don't add group of just fill.
|
||||
padding = (number - size % number) % number
|
||||
collection = dup.concat([fill_with] * padding)
|
||||
end
|
||||
|
||||
if block_given?
|
||||
collection.each_slice(number) { |slice| yield(slice) }
|
||||
else
|
||||
[].tap do |groups|
|
||||
collection.each_slice(number) { |group| groups << group }
|
||||
end
|
||||
end
|
||||
end
|
||||
if block_given?
|
||||
collection.each_slice(number) { |slice| yield(slice) }
|
||||
else
|
||||
groups = []
|
||||
collection.each_slice(number) { |group| groups << group }
|
||||
groups
|
||||
end
|
||||
end
|
||||
|
||||
# Splits or iterates over the array in +number+ of groups, padding any
|
||||
# remaining slots with +fill_with+ unless it is +false+.
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group}
|
||||
# ["1", "2", "3", "4"]
|
||||
# ["5", "6", "7", nil]
|
||||
# ["8", "9", "10", nil]
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups(3, ' ') {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5", " "]
|
||||
# ["6", "7", " "]
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5"]
|
||||
# ["6", "7"]
|
||||
def in_groups(number, fill_with = nil)
|
||||
# size / number gives minor group size;
|
||||
# size % number gives how many objects need extra accomodation;
|
||||
# each group hold either division or division + 1 items.
|
||||
division = size / number
|
||||
modulo = size % number
|
||||
# Splits or iterates over the array in +number+ of groups, padding any
|
||||
# remaining slots with +fill_with+ unless it is +false+.
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group}
|
||||
# ["1", "2", "3", "4"]
|
||||
# ["5", "6", "7", nil]
|
||||
# ["8", "9", "10", nil]
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups(3, ' ') {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5", " "]
|
||||
# ["6", "7", " "]
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5"]
|
||||
# ["6", "7"]
|
||||
def in_groups(number, fill_with = nil)
|
||||
# size / number gives minor group size;
|
||||
# size % number gives how many objects need extra accommodation;
|
||||
# each group hold either division or division + 1 items.
|
||||
division = size / number
|
||||
modulo = size % number
|
||||
|
||||
# create a new array avoiding dup
|
||||
groups = []
|
||||
start = 0
|
||||
# create a new array avoiding dup
|
||||
groups = []
|
||||
start = 0
|
||||
|
||||
number.times do |index|
|
||||
length = division + (modulo > 0 && modulo > index ? 1 : 0)
|
||||
padding = fill_with != false &&
|
||||
modulo > 0 && length == division ? 1 : 0
|
||||
groups << slice(start, length).concat([fill_with] * padding)
|
||||
start += length
|
||||
end
|
||||
number.times do |index|
|
||||
length = division + (modulo > 0 && modulo > index ? 1 : 0)
|
||||
padding = fill_with != false &&
|
||||
modulo > 0 && length == division ? 1 : 0
|
||||
groups << slice(start, length).concat([fill_with] * padding)
|
||||
start += length
|
||||
end
|
||||
|
||||
if block_given?
|
||||
groups.each{|g| yield(g) }
|
||||
else
|
||||
groups
|
||||
end
|
||||
end
|
||||
if block_given?
|
||||
groups.each { |g| yield(g) }
|
||||
else
|
||||
groups
|
||||
end
|
||||
end
|
||||
|
||||
# Divides the array into one or more subarrays based on a delimiting +value+
|
||||
# or the result of an optional block.
|
||||
#
|
||||
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
|
||||
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
|
||||
def split(value = nil)
|
||||
using_block = block_given?
|
||||
# Divides the array into one or more subarrays based on a delimiting +value+
|
||||
# or the result of an optional block.
|
||||
#
|
||||
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
|
||||
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
|
||||
def split(value = nil)
|
||||
using_block = block_given?
|
||||
|
||||
inject([[]]) do |results, element|
|
||||
if (using_block && yield(element)) || (value == element)
|
||||
results << []
|
||||
else
|
||||
results.last << element
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
end
|
||||
inject([[]]) do |results, element|
|
||||
if (using_block && yield(element)) || (value == element)
|
||||
results << []
|
||||
else
|
||||
results.last << element
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
class Array
|
||||
# The human way of thinking about adding stuff to the end of a list is with append
|
||||
alias_method :append, :<<
|
||||
|
||||
# The human way of thinking about adding stuff to the beginning of a list is with prepend
|
||||
alias_method :prepend, :unshift
|
||||
end
|
||||
@@ -1,42 +1,30 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module RandomAccess
|
||||
# This method is deprecated because it masks Kernel#rand within the Array class itself,
|
||||
# which may be used by a 3rd party library extending Array in turn. See
|
||||
#
|
||||
# https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4555
|
||||
#
|
||||
def rand # :nodoc:
|
||||
ActiveSupport::Deprecation.warn 'Array#rand is deprecated and will be removed in Rails 3. Use Array#sample instead', caller
|
||||
sample
|
||||
end
|
||||
|
||||
# Returns a random element from the array.
|
||||
def random_element # :nodoc:
|
||||
ActiveSupport::Deprecation.warn 'Array#random_element is deprecated and will be removed in Rails 3. Use Array#sample instead', caller
|
||||
sample
|
||||
end
|
||||
|
||||
# Backport of Array#sample based on Marc-Andre Lafortune's http://github.com/marcandre/backports/
|
||||
def sample(n=nil)
|
||||
return self[Kernel.rand(size)] if n.nil?
|
||||
n = n.to_int
|
||||
rescue Exception => e
|
||||
raise TypeError, "Coercion error: #{n.inspect}.to_int => Integer failed:\n(#{e.message})"
|
||||
else
|
||||
raise TypeError, "Coercion error: #{n}.to_int did NOT return an Integer (was #{n.class})" unless n.kind_of? ::Integer
|
||||
raise ArgumentError, "negative array size" if n < 0
|
||||
n = size if n > size
|
||||
result = ::Array.new(self)
|
||||
n.times do |i|
|
||||
r = i + Kernel.rand(size - i)
|
||||
result[i], result[r] = result[r], result[i]
|
||||
end
|
||||
result[n..size] = []
|
||||
result
|
||||
end unless method_defined? :sample
|
||||
end
|
||||
class Array
|
||||
# Backport of Array#sample based on Marc-Andre Lafortune's https://github.com/marcandre/backports/
|
||||
# Returns a random element or +n+ random elements from the array.
|
||||
# If the array is empty and +n+ is nil, returns <tt>nil</tt>.
|
||||
# If +n+ is passed and its value is less than 0, it raises an +ArgumentError+ exception.
|
||||
# If the value of +n+ is equal or greater than 0 it returns <tt>[]</tt>.
|
||||
#
|
||||
# [1,2,3,4,5,6].sample # => 4
|
||||
# [1,2,3,4,5,6].sample(3) # => [2, 4, 5]
|
||||
# [1,2,3,4,5,6].sample(-3) # => ArgumentError: negative array size
|
||||
# [].sample # => nil
|
||||
# [].sample(3) # => []
|
||||
def sample(n=nil)
|
||||
return self[Kernel.rand(size)] if n.nil?
|
||||
n = n.to_int
|
||||
rescue Exception => e
|
||||
raise TypeError, "Coercion error: #{n.inspect}.to_int => Integer failed:\n(#{e.message})"
|
||||
else
|
||||
raise TypeError, "Coercion error: obj.to_int did NOT return an Integer (was #{n.class})" unless n.kind_of? Integer
|
||||
raise ArgumentError, "negative array size" if n < 0
|
||||
n = size if n > size
|
||||
result = Array.new(self)
|
||||
n.times do |i|
|
||||
r = i + Kernel.rand(size - i)
|
||||
result[i], result[r] = result[r], result[i]
|
||||
end
|
||||
end
|
||||
result[n..size] = []
|
||||
result
|
||||
end unless method_defined? :sample
|
||||
end
|
||||
|
||||
16
activesupport/lib/active_support/core_ext/array/uniq_by.rb
Normal file
16
activesupport/lib/active_support/core_ext/array/uniq_by.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
class Array
|
||||
# Returns an unique array based on the criteria given as a +Proc+.
|
||||
#
|
||||
# [1, 2, 3, 4].uniq_by { |i| i.odd? } # => [1, 2]
|
||||
#
|
||||
def uniq_by
|
||||
hash, array = {}, []
|
||||
each { |i| hash[yield(i)] ||= (array << i) }
|
||||
array
|
||||
end
|
||||
|
||||
# Same as uniq_by, but modifies self.
|
||||
def uniq_by!
|
||||
replace(uniq_by{ |i| yield(i) })
|
||||
end
|
||||
end
|
||||
48
activesupport/lib/active_support/core_ext/array/wrap.rb
Normal file
48
activesupport/lib/active_support/core_ext/array/wrap.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
class Array
|
||||
# Wraps its argument in an array unless it is already an array (or array-like).
|
||||
#
|
||||
# Specifically:
|
||||
#
|
||||
# * If the argument is +nil+ an empty list is returned.
|
||||
# * Otherwise, if the argument responds to +to_ary+ it is invoked, and its result returned.
|
||||
# * Otherwise, returns an array with the argument as its single element.
|
||||
#
|
||||
# Array.wrap(nil) # => []
|
||||
# Array.wrap([1, 2, 3]) # => [1, 2, 3]
|
||||
# Array.wrap(0) # => [0]
|
||||
#
|
||||
# This method is similar in purpose to <tt>Kernel#Array</tt>, but there are some differences:
|
||||
#
|
||||
# * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt>
|
||||
# moves on to try +to_a+ if the returned value is +nil+, but <tt>Array.wrap</tt> returns
|
||||
# such a +nil+ right away.
|
||||
# * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt>
|
||||
# raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value.
|
||||
# * It does not call +to_a+ on the argument, though special-cases +nil+ to return an empty array.
|
||||
#
|
||||
# The last point is particularly worth comparing for some enumerables:
|
||||
#
|
||||
# Array(:foo => :bar) # => [[:foo, :bar]]
|
||||
# Array.wrap(:foo => :bar) # => [{:foo => :bar}]
|
||||
#
|
||||
# Array("foo\nbar") # => ["foo\n", "bar"], in Ruby 1.8
|
||||
# Array.wrap("foo\nbar") # => ["foo\nbar"]
|
||||
#
|
||||
# There's also a related idiom that uses the splat operator:
|
||||
#
|
||||
# [*object]
|
||||
#
|
||||
# which returns <tt>[nil]</tt> for +nil+, and calls to <tt>Array(object)</tt> otherwise.
|
||||
#
|
||||
# Thus, in this case the behavior is different for +nil+, and the differences with
|
||||
# <tt>Kernel#Array</tt> explained above apply to the rest of +object+s.
|
||||
def self.wrap(object)
|
||||
if object.nil?
|
||||
[]
|
||||
elsif object.respond_to?(:to_ary)
|
||||
object.to_ary || [object]
|
||||
else
|
||||
[object]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
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
|
||||
@@ -1,4 +0,0 @@
|
||||
require 'active_support/base64'
|
||||
require 'active_support/core_ext/base64/encoding'
|
||||
|
||||
ActiveSupport::Base64.extend ActiveSupport::CoreExtensions::Base64::Encoding
|
||||
@@ -1,16 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Base64 #:nodoc:
|
||||
module Encoding
|
||||
# Encodes the value as base64 without the newline breaks. This makes the base64 encoding readily usable as URL parameters
|
||||
# or memcache keys without further processing.
|
||||
#
|
||||
# ActiveSupport::Base64.encode64s("Original unencoded string")
|
||||
# # => "T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw=="
|
||||
def encode64s(value)
|
||||
encode64(value).gsub(/\n/, '')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,18 +1,6 @@
|
||||
require 'benchmark'
|
||||
|
||||
class << Benchmark
|
||||
# Earlier Ruby had a slower implementation.
|
||||
if RUBY_VERSION < '1.8.7'
|
||||
remove_method :realtime
|
||||
|
||||
def realtime
|
||||
r0 = Time.now
|
||||
yield
|
||||
r1 = Time.now
|
||||
r1.to_f - r0.to_f
|
||||
end
|
||||
end
|
||||
|
||||
def ms
|
||||
1000 * realtime { yield }
|
||||
end
|
||||
|
||||
1
activesupport/lib/active_support/core_ext/big_decimal.rb
Normal file
1
activesupport/lib/active_support/core_ext/big_decimal.rb
Normal file
@@ -0,0 +1 @@
|
||||
require 'active_support/core_ext/big_decimal/conversions'
|
||||
@@ -0,0 +1,45 @@
|
||||
require 'bigdecimal'
|
||||
|
||||
begin
|
||||
require 'psych'
|
||||
rescue LoadError
|
||||
end
|
||||
|
||||
require 'yaml'
|
||||
|
||||
class BigDecimal
|
||||
YAML_TAG = 'tag:yaml.org,2002:float'
|
||||
YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' }
|
||||
|
||||
# This emits the number without any scientific notation.
|
||||
# This is better than self.to_f.to_s since it doesn't lose precision.
|
||||
#
|
||||
# Note that reconstituting YAML floats to native floats may lose precision.
|
||||
def to_yaml(opts = {})
|
||||
return super if defined?(YAML::ENGINE) && !YAML::ENGINE.syck?
|
||||
|
||||
YAML.quick_emit(nil, opts) do |out|
|
||||
string = to_s
|
||||
out.scalar(YAML_TAG, YAML_MAPPING[string] || string, :plain)
|
||||
end
|
||||
end
|
||||
|
||||
def encode_with(coder)
|
||||
string = to_s
|
||||
coder.represent_scalar(nil, YAML_MAPPING[string] || string)
|
||||
end
|
||||
|
||||
# Backport this method if it doesn't exist
|
||||
unless method_defined?(:to_d)
|
||||
def to_d
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
DEFAULT_STRING_FORMAT = 'F'
|
||||
def to_formatted_s(format = DEFAULT_STRING_FORMAT)
|
||||
_original_to_s(format)
|
||||
end
|
||||
alias_method :_original_to_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
require 'bigdecimal'
|
||||
require 'active_support/core_ext/bigdecimal/conversions'
|
||||
|
||||
class BigDecimal#:nodoc:
|
||||
include ActiveSupport::CoreExtensions::BigDecimal::Conversions
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
require 'yaml'
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module BigDecimal #:nodoc:
|
||||
module Conversions
|
||||
DEFAULT_STRING_FORMAT = 'F'.freeze
|
||||
YAML_TAG = 'tag:yaml.org,2002:float'.freeze
|
||||
YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' }
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
base.class_eval do
|
||||
alias_method :_original_to_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
|
||||
yaml_as YAML_TAG
|
||||
end
|
||||
end
|
||||
|
||||
def to_formatted_s(format = DEFAULT_STRING_FORMAT)
|
||||
_original_to_s(format)
|
||||
end
|
||||
|
||||
# This emits the number without any scientific notation.
|
||||
# This is better than self.to_f.to_s since it doesn't lose precision.
|
||||
#
|
||||
# Note that reconstituting YAML floats to native floats may lose precision.
|
||||
def to_yaml(opts = {})
|
||||
YAML.quick_emit(nil, opts) do |out|
|
||||
string = to_s
|
||||
out.scalar(YAML_TAG, YAML_MAPPING[string] || string, :plain)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
require 'active_support/core_ext/object/blank'
|
||||
ActiveSupport::Deprecation.warn 'require "active_support/core_ext/blank" is deprecated and will be removed in Rails 3. Use require "active_support/core_ext/object/blank" instead.'
|
||||
@@ -1,5 +0,0 @@
|
||||
require 'active_support/core_ext/cgi/escape_skipping_slashes'
|
||||
|
||||
class CGI #:nodoc:
|
||||
extend ActiveSupport::CoreExtensions::CGI::EscapeSkippingSlashes
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module CGI #:nodoc:
|
||||
module EscapeSkippingSlashes #:nodoc:
|
||||
if RUBY_VERSION >= '1.9'
|
||||
def escape_skipping_slashes(str)
|
||||
str = str.join('/') if str.respond_to? :join
|
||||
str.gsub(/([^ \/a-zA-Z0-9_.-])/n) do
|
||||
"%#{$1.unpack('H2' * $1.bytesize).join('%').upcase}"
|
||||
end.tr(' ', '+')
|
||||
end
|
||||
else
|
||||
def escape_skipping_slashes(str)
|
||||
str = str.join('/') if str.respond_to? :join
|
||||
str.gsub(/([^ \/a-zA-Z0-9_.-])/n) do
|
||||
"%#{$1.unpack('H2').first.upcase}"
|
||||
end.tr(' ', '+')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
require 'active_support/core_ext/class/attribute_accessors'
|
||||
require 'active_support/core_ext/class/inheritable_attributes'
|
||||
require 'active_support/core_ext/class/removal'
|
||||
require 'active_support/core_ext/class/delegating_attributes'
|
||||
require 'active_support/core_ext/class/attribute'
|
||||
require 'active_support/core_ext/class/attribute_accessors'
|
||||
require 'active_support/core_ext/class/delegating_attributes'
|
||||
require 'active_support/core_ext/class/inheritable_attributes'
|
||||
require 'active_support/core_ext/class/subclasses'
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
require 'active_support/core_ext/kernel/singleton_class'
|
||||
require 'active_support/core_ext/module/remove_method'
|
||||
require 'active_support/core_ext/array/extract_options'
|
||||
|
||||
class Class
|
||||
# Declare a class-level attribute whose value is inheritable and
|
||||
# overwritable by subclasses:
|
||||
# Declare a class-level attribute whose value is inheritable by subclasses.
|
||||
# Subclasses can change their own value and it will not impact parent class.
|
||||
#
|
||||
# class Base
|
||||
# class_attribute :setting
|
||||
@@ -18,12 +19,34 @@ class Class
|
||||
# Subclass.setting # => false
|
||||
# Base.setting # => true
|
||||
#
|
||||
# In the above case as long as Subclass does not assign a value to setting
|
||||
# by performing <tt>Subclass.setting = _something_ </tt>, <tt>Subclass.setting</tt>
|
||||
# would read value assigned to parent class. Once Subclass assigns a value then
|
||||
# the value assigned by Subclass would be returned.
|
||||
#
|
||||
# This matches normal Ruby method inheritance: think of writing an attribute
|
||||
# on a subclass as overriding the reader method.
|
||||
# on a subclass as overriding the reader method. However, you need to be aware
|
||||
# when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
|
||||
# In such cases, you don't want to do changes in places but use setters:
|
||||
#
|
||||
# Base.setting = []
|
||||
# Base.setting # => []
|
||||
# Subclass.setting # => []
|
||||
#
|
||||
# # Appending in child changes both parent and child because it is the same object:
|
||||
# Subclass.setting << :foo
|
||||
# Base.setting # => [:foo]
|
||||
# Subclass.setting # => [:foo]
|
||||
#
|
||||
# # Use setters to not propagate changes:
|
||||
# Base.setting = []
|
||||
# Subclass.setting += [:foo]
|
||||
# Base.setting # => []
|
||||
# Subclass.setting # => [:foo]
|
||||
#
|
||||
# For convenience, a query method is defined as well:
|
||||
#
|
||||
# Subclass.setting? # => false
|
||||
# Subclass.setting? # => false
|
||||
#
|
||||
# Instances may overwrite the class value in the same way:
|
||||
#
|
||||
@@ -34,11 +57,18 @@ class Class
|
||||
# object.setting # => false
|
||||
# Base.setting # => true
|
||||
#
|
||||
# To opt out of the instance reader method, pass :instance_reader => false.
|
||||
#
|
||||
# object.setting # => NoMethodError
|
||||
# object.setting? # => NoMethodError
|
||||
#
|
||||
# To opt out of the instance writer method, pass :instance_writer => false.
|
||||
#
|
||||
# object.setting = false # => NoMethodError
|
||||
def class_attribute(*attrs)
|
||||
instance_writer = !attrs.last.is_a?(Hash) || attrs.pop[:instance_writer]
|
||||
options = attrs.extract_options!
|
||||
instance_reader = options.fetch(:instance_reader, true)
|
||||
instance_writer = options.fetch(:instance_writer, true)
|
||||
|
||||
attrs.each do |name|
|
||||
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
||||
@@ -50,18 +80,36 @@ class Class
|
||||
remove_possible_method(:#{name})
|
||||
define_method(:#{name}) { val }
|
||||
end
|
||||
|
||||
if singleton_class?
|
||||
class_eval do
|
||||
remove_possible_method(:#{name})
|
||||
def #{name}
|
||||
defined?(@#{name}) ? @#{name} : singleton_class.#{name}
|
||||
end
|
||||
end
|
||||
end
|
||||
val
|
||||
end
|
||||
|
||||
def #{name}
|
||||
defined?(@#{name}) ? @#{name} : singleton_class.#{name}
|
||||
end
|
||||
if instance_reader
|
||||
remove_possible_method :#{name}
|
||||
def #{name}
|
||||
defined?(@#{name}) ? @#{name} : self.class.#{name}
|
||||
end
|
||||
|
||||
def #{name}?
|
||||
!!#{name}
|
||||
def #{name}?
|
||||
!!#{name}
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
attr_writer name if instance_writer
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def singleton_class?
|
||||
ancestors.first != self
|
||||
end
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user