Compare commits

...

28 Commits

Author SHA1 Message Date
Charlie Somerville
775febba74 use load_paths.rb in railties abstract unit 2013-11-12 14:30:04 -08:00
Charlie Somerville
d33a754a92 pull deleted ActiveSupport::ModelName into ActiveRecord::ModelName 2013-11-12 14:30:04 -08:00
Charlie Somerville
c12cd651c2 remove ActiveSupport::SecureRandom 2013-11-12 14:30:04 -08:00
Charlie Somerville
f709b5d1c8 remove bytesize require 2013-11-12 14:30:03 -08:00
Charlie Somerville
c6f9ec2d8d bring back inheritable_attributes, too much of Rails 2 internals uses it 2013-11-12 14:30:03 -08:00
Charlie Somerville
d7440a463c make actionpack depend on erubis 2013-11-12 14:30:03 -08:00
Charlie Somerville
7655b80261 require active_support/all everywhere 2013-11-12 14:30:03 -08:00
Charlie Somerville
c5be730a2e use load_paths in actionpack, activeresource and activerecord 2013-11-12 14:30:03 -08:00
Charlie Somerville
052556e5cf add tzinfo and builder to the gemfile 2013-11-12 14:30:03 -08:00
Charlie Somerville
173bc3c9e5 we don't need the isolated environment when using bundler 2013-11-12 14:30:03 -08:00
Charlie Somerville
01280149f2 bring over deps from activesupport 3 2013-11-12 14:30:03 -08:00
Charlie Somerville
79c1106fb2 try to use bundler install of Gemfile.sh 2013-11-12 14:30:03 -08:00
Charlie Somerville
8f6982c04b import ActiveSupport 3 tests 2013-11-12 14:30:03 -08:00
Charlie Somerville
a471098ab8 add ActiveSupport 3 dependencies 2013-11-12 14:30:02 -08:00
Charlie Somerville
87ef1f0e73 upgrade to ActiveSupport 3 2013-11-12 14:30:02 -08:00
Charlie Somerville
e42c679e43 Merge pull request #27 from github/remove-activeresource
Remove ActiveResource
2013-11-12 14:17:01 -08:00
Charlie Somerville
5c4dfa63f7 remove references to active_resource 2013-11-11 19:21:01 -08:00
Charlie Somerville
c394fd82fa delete references to activeresource 2013-11-11 19:17:45 -08:00
Charlie Somerville
49933594c1 delete activeresource/ 2013-11-11 19:17:20 -08:00
Charlie Somerville
94fae25703 forgot railties 2013-11-10 15:22:21 -05:00
Charlie Somerville
05cb9e6854 depend on the right versions 2013-11-10 15:20:15 -05:00
Charlie Somerville
1a5734e0b5 use RAILS_VERSION file 2013-11-10 11:43:01 -05:00
Charlie Somerville
24e5712294 Merge pull request #26 from github/kill-whiny-nils
Kill whiny nils
2013-10-29 20:32:13 -07:00
Charlie Somerville
8f6bafc333 💀 whiny nils 2013-10-29 20:25:48 -07:00
Charlie Somerville
c717a84b5d Merge pull request #24 from github/avoid-extension-when-instantiating-extended-association
Avoid extension when instantiating extended association
2013-10-29 20:23:28 -07:00
Charlie Somerville
d537304b20 replace :: with _ to avoid wrong constant name exceptions 2013-10-29 20:16:52 -07:00
Charlie Somerville
ca90ecf2cb use terrible hacks to make this work when rails tries to marshal 2013-10-29 20:06:11 -07:00
Charlie Somerville
4bb1d3ef20 cache a class with the extend module pre-included 2013-10-29 20:06:11 -07:00
640 changed files with 18866 additions and 37382 deletions

101
Gemfile Normal file
View 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"

View File

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

@@ -0,0 +1 @@
2.3.14.github30

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -212,7 +212,7 @@ module ActionController
end
def generate_sid
ActiveSupport::SecureRandom.hex(16)
SecureRandom.hex(16)
end
def load_session(env)

View File

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

View File

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

View File

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

View File

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

View File

@@ -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&lt;=?"/>), @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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

@@ -1,5 +1,4 @@
require 'active_support/json'
require 'active_support/core_ext/module/model_naming'
module ActiveRecord #:nodoc:
module Serialization

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
module ActiveResource
module VERSION #:nodoc:
MAJOR = 2
MINOR = 3
TINY = 14
STRING = [MAJOR, MINOR, TINY].join('.')
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
class Customer < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000"
end

View File

@@ -1,3 +0,0 @@
class Person < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000"
end

View File

@@ -1,4 +0,0 @@
class ProxyResource < ActiveResource::Base
self.site = "http://localhost"
self.proxy = "http://user:password@proxy.local:3000"
end

View File

@@ -1,4 +0,0 @@
class StreetAddress < ActiveResource::Base
self.site = "http://37s.sunrise.i:3000/people/:person_id/"
self.element_name = 'address'
end

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
begin
$:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
require 'active_support'
require 'active_support/all'
rescue IOError
end

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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, '&nbsp;') {|group| p group}
# ["1", "2"]
# ["3", "&nbsp;"]
#
# %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, '&nbsp;') {|group| p group}
# ["1", "2"]
# ["3", "&nbsp;"]
#
# %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, '&nbsp;') {|group| p group}
# ["1", "2", "3"]
# ["4", "5", "&nbsp;"]
# ["6", "7", "&nbsp;"]
#
# %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, '&nbsp;') {|group| p group}
# ["1", "2", "3"]
# ["4", "5", "&nbsp;"]
# ["6", "7", "&nbsp;"]
#
# %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

View File

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

View File

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

View 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

View 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

View File

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

View File

@@ -1,4 +0,0 @@
require 'active_support/base64'
require 'active_support/core_ext/base64/encoding'
ActiveSupport::Base64.extend ActiveSupport::CoreExtensions::Base64::Encoding

View File

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

View File

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

View File

@@ -0,0 +1 @@
require 'active_support/core_ext/big_decimal/conversions'

View File

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

View File

@@ -1,6 +0,0 @@
require 'bigdecimal'
require 'active_support/core_ext/bigdecimal/conversions'
class BigDecimal#:nodoc:
include ActiveSupport::CoreExtensions::BigDecimal::Conversions
end

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
require 'active_support/core_ext/cgi/escape_skipping_slashes'
class CGI #:nodoc:
extend ActiveSupport::CoreExtensions::CGI::EscapeSkippingSlashes
end

View File

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

View File

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

View File

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