Compare commits

..

99 Commits

Author SHA1 Message Date
Dirkjan Bussink
a141d9de0d bump 2.3.14.github41 2014-03-27 13:55:26 +01:00
Dirkjan Bussink
74492f43a8 Merge pull request #51 from github/dbussink/fix-logging-frozen-string-query
Dup string before changing encoding because it might be frozen
2014-03-27 12:53:25 +00:00
Dirkjan Bussink
c2894170bf Dup string before changing encoding because it might be frozen
Calling String#force_encoding! on a frozen string throws an exception.
By dupping the string we prevent this from happening.
2014-03-27 13:47:08 +01:00
Charlie Somerville
057aed6e18 Merge pull request #48 from github/2-3-kill-backtick-monkey-patch
[2.3] Kill Object#` monkey patch
2014-02-23 23:20:47 +11:00
Charlie Somerville
02fc012b42 kill Object#` monkey patch 2014-02-23 23:17:44 +11:00
Mastahyeti
4fdaf21b28 bump 2.3.14.github40 2014-02-18 15:28:32 -06:00
Ben Toews
35b871fbcd Merge pull request #47 from github/CVE-2014-0081
CVE-2014-0081
2014-02-18 15:28:00 -06:00
Mastahyeti
a5697840d6 escape format for CVE-2014-0081 2014-02-18 15:25:05 -06:00
Andy Lindeman
d0e554d231 Bumps to github39 2014-02-14 00:12:09 -05:00
Andy Lindeman
d38b7664cc github38 2014-02-13 22:36:14 -05:00
Andy Lindeman
e4cd9caf02 Merge pull request #46 from github/runtime_header
Removes the X-Runtime header from ActionController::Benchmarking
2014-02-13 22:34:18 -05:00
Andy Lindeman
89e4514704 Removes the X-Runtime header from ActionController::Benchmarking
The `Rack::Runtime` middleware now provides this header
2014-02-13 22:25:27 -05:00
Joshua Peek
0a0d975f51 github37 2014-02-11 23:36:30 -06:00
Joshua Peek
62daf4cb6f Merge pull request #45 from github/rack-session-skip
Backport Rack session skip
2014-02-11 23:34:52 -06:00
Joshua Peek
24711e1e29 Backport env['rack.session.options'][:skip] 2014-02-11 23:22:39 -06:00
Charlie Somerville
cf8f36930c Merge pull request #44 from github/builder-3.2.2
Builder 3.2.2
2014-01-20 19:55:16 -08:00
Charlie Somerville
d622643e47 fix tests 2014-01-21 14:30:00 +11:00
Charlie Somerville
3f0241a613 use assert_includes so we get a useful failure message 2014-01-21 14:28:01 +11:00
Charlie Somerville
38a7432590 add builder 3.2.2 to Gemfile.sh and use that instead of vendored copy 2014-01-21 14:22:20 +11:00
Charlie Somerville
1220d3c3ed delete vendored builder 2.1.2 2014-01-21 14:16:36 +11:00
Charlie Somerville
3d72818356 Merge pull request #43 from github/i18n-0.6.9
i18n 0.4.1
2014-01-20 18:05:20 -08:00
Charlie Somerville
221477dc21 fix this bit 2014-01-21 12:51:40 +11:00
Charlie Somerville
975155c110 use i18n 0.6.9 as a gem 2014-01-21 12:33:39 +11:00
Charlie Somerville
2931987892 delete vendored i18n-0.4.1 2014-01-21 12:25:44 +11:00
Charlie Somerville
e3290b98dd Merge pull request #42 from github/dont-test-on-1.9
Don't run tests on 1.9.3
2014-01-20 17:25:13 -08:00
Charlie Somerville
20088080a5 Don't run tests on 1.9.3 2014-01-21 12:23:05 +11:00
Dirkjan Bussink
24e348489d github36 2014-01-17 18:10:31 +01:00
Dirkjan Bussink
ba4f4f8a01 Treat mysql in the name as a mysql driver 2014-01-17 18:05:20 +01:00
Aman Gupta
ccf254b6cb build using 2.1 first 2014-01-15 01:10:18 -08:00
Mislav Marohnić
3766b1b377 github35 2014-01-13 13:58:57 -08:00
Mislav Marohnić
d3f87776a3 Merge pull request #41 from github/disable-generated-id
Disable auto-generated form field IDs by passing nil for "id" attribute
2014-01-13 13:57:38 -08:00
Mislav Marohnić
18c7c1f753 Disable auto-generated form field IDs by passing nil for "id" attribute
Previously it was not possible to opt out of auto-generated ID values
for various form fields.
2014-01-13 13:22:06 -08:00
Aman Gupta
f63b0340ff github34 2014-01-08 21:04:30 -08:00
Aman Gupta
7224ee1419 Merge pull request #37 from github/erb-freeze
Freeze ERB string literals
2014-01-08 20:33:01 -08:00
Aman Gupta
0c52ae6df3 Merge pull request #39 from github/write-fragment-fix
Fix fragment caching in mixed encoding scenario
2014-01-08 20:32:40 -08:00
Aman Gupta
f8b7cd2df7 Merge pull request #40 from github/ruby-2.1
Ruby 2.1
2014-01-08 20:32:12 -08:00
Aman Gupta
c73ba86136 use new 2.1 api 2014-01-08 18:03:55 -08:00
Aman Gupta
98fa5dd465 build on ruby 2.1 2014-01-08 17:46:13 -08:00
Mislav Marohnić
fa41bedf6b Don't rely on default encoding always being ASCII-8BIT 2014-01-08 17:41:17 -08:00
Aman Gupta
0a8282c557 freeze literals 2014-01-08 17:28:31 -08:00
Mislav Marohnić
d4a4facfcc Add test for extracting the cache fragment with mixed encodings 2014-01-08 17:12:18 -08:00
Aman Gupta
dd4146854a Fix fragment caching in mixed encodings scenario
To reduce ambiguity between char- and byte-based operations, explicitly
do byte operations when extracting the fragment that needs to be cached.
2014-01-08 16:35:55 -08:00
Charlie Somerville
cedf026a14 bump version to github33 2013-12-30 15:45:48 +11:00
Charlie Somerville
7ac3b0fa4f Merge pull request #34 from github/remove-cgi
Remove CGI support
2013-12-29 19:56:48 -08:00
Charlie Somerville
31cd7ea26d remove this NCGI stuff 2013-12-30 14:29:27 +11:00
Charlie Somerville
df387ab385 remove FastCGI crap 2013-12-30 14:28:24 +11:00
Charlie Somerville
0118959601 remove the webrick server 2013-12-30 14:26:08 +11:00
Charlie Somerville
83448c7de5 remove dispatch.rb and gateway.cgi 2013-12-30 14:23:00 +11:00
Charlie Somerville
8f99d00868 require properly 2013-12-30 14:23:00 +11:00
Charlie Somerville
987b61bd1d kill QueryExtension, it's more dead junk 2013-12-30 14:15:55 +11:00
Charlie Somerville
f05e54a9f3 remove stdinput monkey patch 2013-12-30 14:15:51 +11:00
Charlie Somerville
b9918117bb delete ActionController::CGIHandler and CgiRequest 2013-12-30 14:11:07 +11:00
Charlie Somerville
42f85d118d don't autoload CGIHandler and CgiRequest 2013-12-30 14:10:28 +11:00
Charlie Somerville
acb182d094 @output is never used anywhere, kill it 2013-12-30 14:09:20 +11:00
Charlie Somerville
6e0fcb788d remove CGI from the dispatcher 2013-12-30 14:09:00 +11:00
Charlie Somerville
fed4fafa8a Merge pull request #33 from github/dont-reload-middleware-stack-every-request
Don't reload middleware stack every request
2013-12-29 19:07:59 -08:00
Charlie Somerville
f699184047 test that we never call build_middleware_stack after initialization 2013-12-30 13:59:18 +11:00
Charlie Somerville
55d6a9f2df don't reload the middleware stack every request in development 2013-12-30 13:53:48 +11:00
Ted Nyman
e5bebc01a8 Merge pull request #32 from github/bump-to-github32
Bump to 2.3.14.github32
2013-12-03 14:53:14 -08:00
Ted Nyman
a019f07a39 Bump to 2.3.14.github32 2013-12-03 14:50:02 -08:00
Ted Nyman
d13866d75d Merge pull request #30 from github/CVE-2013-6417
CVE-2013-6417
2013-12-03 14:46:53 -08:00
Nathan Witmer
dfa2f469a4 Merge pull request #31 from github/currency-security-fix
CVE-2013-6415: Escape the unit value provided to number_to_currency
2013-12-03 14:41:51 -08:00
Nathan Witmer
bf0d43bb77 Only escape value if present 2013-12-03 14:47:38 -07:00
Nathan Witmer
72cebbcb59 Escape the unit value provided to number_to_currency
Fixes CVE-2013-6415.

Previously the values were trusted blindly allowing for potential XSS attacks.

This is different from the original upstream patch for 3.x in that return values
from other number helper methods are not marked as html_safe, so the html
escaping always applies. This requires applications to explicitly set .html_safe
on unit strings and number separators when calling number_to_currency.
2013-12-03 14:32:26 -07:00
Ted Nyman
379dd9071c Documentation for #deep_munge 2013-12-03 13:24:11 -08:00
Ted Nyman
a743f17dbd #deep_munge for CVE-2013-6417 2013-12-03 13:23:02 -08:00
Charlie Somerville
25b896611d Merge pull request #29 from github/tzinfo-json
Load timezone data from one big marshalled file
2013-12-03 00:38:50 -08:00
Charlie Somerville
b988837359 load definitions from a marshalled file 2013-12-03 19:32:36 +11:00
Charlie Somerville
890aff3b9d use vendored tzinfo 2013-12-03 18:10:11 +11:00
Charlie Somerville
c0124ba8f3 bump RAILS_VERSION 2013-12-02 20:43:27 +11:00
Charlie Somerville
455cd8c060 Merge pull request #28 from github/dont-turn-constant-names-into-strings
Don't turn constant names into strings prematurely
2013-12-02 01:27:18 -08:00
Charlie Somerville
5d322ad957 delete Module#local_constant_names 2013-12-02 20:09:05 +11:00
Charlie Somerville
3b6b4578c4 don't return anything interesting from require or load_with_new_constant_marking 2013-12-02 19:51:45 +11:00
Charlie Somerville
981016be60 call local_constants instead of local_constant_names 2013-12-02 19:40:28 +11:00
Aman Gupta
3c1e01068b faster String#blank? regex 2013-11-21 13:53:47 -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
John Barnette
3b7754c950 Merge pull request #25 from github/activesupport-concern
Pull in ActiveSupport::Concern
2013-10-29 12:10:45 -07:00
John Barnette
75638c576b Pull in ActiveSupport::Concern
We have quite a few module dependency situations that this can help
clarify.
2013-10-29 12:03:54 -05:00
Charlie Somerville
76884dd7f7 Merge pull request #22 from github/actionview-proxy-module-method-cache-nuke
Don't globally invalidate the method and constant cache every view render
2013-10-25 11:43:48 -07:00
Charlie Somerville
29a72262aa here too 2013-10-25 12:46:48 -04:00
Charlie Somerville
76c5bf4f4b instantiate the cached helper class instead of extending AV::B 2013-10-25 12:46:48 -04:00
Charlie Somerville
416b7171b8 delete ActionView::Base#helpers because it's completely useless 2013-10-25 12:46:48 -04:00
Charlie Somerville
e82a3ba2a0 cache a class that is pre-included with the master helper module 2013-10-25 12:46:48 -04:00
Charlie Somerville
8837faac73 Merge pull request #21 from github/kill-blankslate
Kill blankslate
2013-10-25 09:42:01 -07:00
Charlie Somerville
20b12c3b42 call Kernel.block_given? instead of block_given? coz of BasicObject 2013-10-24 14:30:20 -04:00
Charlie Somerville
0cf06787af use fully qualified constant access here 2013-10-24 14:30:20 -04:00
Charlie Somerville
5efad05b11 💀 in a 🔥 blankslate 2013-10-24 14:30:20 -04:00
153 changed files with 671 additions and 10781 deletions

View File

@@ -5,3 +5,5 @@ 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 i18n -v=0.6.9
gem install builder -v=3.2.2

1
RAILS_VERSION Normal file
View File

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

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,6 @@ 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'
end

View File

@@ -38,7 +38,7 @@ module ActionController
# TODO: Review explicit to see if they will automatically be handled by
# the initilizer if they are really needed.
def self.load_all!
[Base, CGIHandler, CgiRequest, Request, Response, Http::Headers, UrlRewriter, UrlWriter]
[Base, Request, Response, Http::Headers, UrlRewriter, UrlWriter]
end
autoload :Base, 'action_controller/base'
@@ -99,10 +99,6 @@ module ActionController
autoload :CookieStore, 'action_controller/session/cookie_store'
autoload :MemCacheStore, 'action_controller/session/mem_cache_store'
end
# DEPRECATE: Remove CGI support
autoload :CgiRequest, 'action_controller/cgi_process'
autoload :CGIHandler, 'action_controller/cgi_process'
end
autoload :Mime, 'action_controller/mime_type'

View File

@@ -1273,8 +1273,7 @@ module ActionController #:nodoc:
end
def initialize_template_class(response)
response.template = ActionView::Base.new(self.class.view_paths, {}, self)
response.template.helpers.send :include, self.class.master_helper_module
response.template = self.class.master_helper_class.new(self.class.view_paths, {}, self)
response.redirected_to = nil
@performed_render = @performed_redirect = false
end

View File

@@ -87,7 +87,6 @@ module ActionController #:nodoc:
log_message << " [#{complete_request_uri rescue "unknown"}]"
logger.info(log_message)
response.headers["X-Runtime"] = "%.0f" % ms
else
perform_action_without_benchmark
end

View File

@@ -39,9 +39,9 @@ module ActionController #:nodoc:
if cache = read_fragment(name, options)
buffer.safe_concat(cache.html_safe)
else
pos = buffer.length
pos = buffer.bytesize
block.call
write_fragment(name, buffer[pos..-1], options)
write_fragment(name, buffer.byteslice(pos..-1), options)
end
else
block.call

View File

@@ -1,10 +1,6 @@
require 'action_controller/cgi_ext/stdinput'
require 'action_controller/cgi_ext/query_extension'
require 'action_controller/cgi_ext/cookie'
class CGI #:nodoc:
include ActionController::CgiExt::Stdinput
class << self
alias :escapeHTML_fail_on_nil :escapeHTML

View File

@@ -1,4 +1,6 @@
require 'delegate'
require 'cgi'
require 'cgi/cookie'
CGI.module_eval { remove_const "Cookie" }
@@ -24,7 +26,7 @@ class CGI #:nodoc:
# * <tt>:secure</tt> - Whether this cookie is a secure cookie or not (defaults to
# +false+). Secure cookies are only transmitted to HTTPS servers.
# * <tt>:http_only</tt> - Whether this cookie can be accessed by client side scripts (e.g. document.cookie) or only over HTTP.
# More details in http://msdn2.microsoft.com/en-us/library/system.web.httpcookie.httponly.aspx. Defaults to +false+.
# More details in http://msdn2.microsoft.com/en-us/library/system.web.httpcookie.httponly.aspx. Defaults to +false+.
#
# These keywords correspond to attributes of the cookie object.
def initialize(name = '', *value)

View File

@@ -1,22 +0,0 @@
require 'cgi'
class CGI #:nodoc:
module QueryExtension
# Remove the old initialize_query method before redefining it.
remove_method :initialize_query
# Neuter CGI parameter parsing.
def initialize_query
# Fix some strange request environments.
env_table['REQUEST_METHOD'] ||= 'GET'
# POST assumes missing Content-Type is application/x-www-form-urlencoded.
if env_table['CONTENT_TYPE'].blank? && env_table['REQUEST_METHOD'] == 'POST'
env_table['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
end
@cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE'])
@params = {}
end
end
end

View File

@@ -1,24 +0,0 @@
require 'cgi'
module ActionController
module CgiExt
# Publicize the CGI's internal input stream so we can lazy-read
# request.body. Make it writable so we don't have to play $stdin games.
module Stdinput
def self.included(base)
base.class_eval do
remove_method :stdinput
attr_accessor :stdinput
end
base.alias_method_chain :initialize, :stdinput
end
def initialize_with_stdinput(type = nil, stdinput = $stdin)
@stdinput = stdinput
@stdinput.set_encoding(Encoding::BINARY) if @stdinput.respond_to?(:set_encoding)
initialize_without_stdinput(type || 'query')
end
end
end
end

View File

@@ -1,77 +0,0 @@
require 'action_controller/cgi_ext'
module ActionController #:nodoc:
class CGIHandler
module ProperStream
def each
while line = gets
yield line
end
end
def read(*args)
if args.empty?
super || ""
else
super
end
end
end
def self.dispatch_cgi(app, cgi, out = $stdout)
env = cgi.__send__(:env_table)
env.delete "HTTP_CONTENT_LENGTH"
cgi.stdinput.extend ProperStream
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({
"rack.version" => [0,1],
"rack.input" => cgi.stdinput,
"rack.errors" => $stderr,
"rack.multithread" => false,
"rack.multiprocess" => true,
"rack.run_once" => false,
"rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
})
env["QUERY_STRING"] ||= ""
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
env["REQUEST_PATH"] ||= "/"
env.delete "PATH_INFO" if env["PATH_INFO"] == ""
status, headers, body = app.call(env)
begin
out.binmode if out.respond_to?(:binmode)
out.sync = false if out.respond_to?(:sync=)
headers['Status'] = status.to_s
if headers.include?('Set-Cookie')
headers['cookie'] = headers.delete('Set-Cookie').split("\n")
end
out.write(cgi.header(headers))
body.each { |part|
out.write part
out.flush if out.respond_to?(:flush)
}
ensure
body.close if body.respond_to?(:close)
end
end
end
class CgiRequest #:nodoc:
DEFAULT_SESSION_OPTIONS = {
:database_manager => nil,
:prefix => "ruby_sess.",
:session_path => "/",
:session_key => "_session_id",
:cookie_only => true,
:session_http_only => true
}
end
end

View File

@@ -22,11 +22,6 @@ module ActionController
end
end
# DEPRECATE: Remove CGI support
def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
new(output).dispatch_cgi(cgi, session_options)
end
# Add a preparation callback. Preparation callbacks are run before every
# request in development mode, and before the first request in production
# mode.
@@ -42,13 +37,7 @@ module ActionController
end
def run_prepare_callbacks
if defined?(Rails) && Rails.logger
logger = Rails.logger
else
logger = Logger.new($stderr)
end
new(logger).send :run_callbacks, :prepare_dispatch
new.send :run_callbacks, :prepare_dispatch
end
def reload_application
@@ -75,10 +64,8 @@ module ActionController
include ActiveSupport::Callbacks
define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch
# DEPRECATE: Remove arguments, since they are only used by CGI
def initialize(output = $stdout, request = nil, response = nil)
@output = output
build_middleware_stack if @@cache_classes
def initialize
build_middleware_stack
end
def dispatch
@@ -96,21 +83,11 @@ module ActionController
end
end
# DEPRECATE: Remove CGI support
def dispatch_cgi(cgi, session_options)
CGIHandler.dispatch_cgi(self, cgi, @output)
end
def call(env)
if @@cache_classes
@app.call(env)
else
Reloader.run do
# When class reloading is turned on, we will want to rebuild the
# middleware stack every time we process a request. If we don't
# rebuild the middleware stack, then the stack may contain references
# to old classes metal classes, which will b0rk class reloading.
build_middleware_stack
@app.call(env)
end
end

View File

@@ -69,6 +69,22 @@ module ActionController #:nodoc:
# N/A | Carolina Railhaws Training Workshop
#
module ClassMethods
# To avoid extending an instance of ActionView::Base with the master_helper_module
# every single time we render a view, we're caching a class that has
# master_helper_module already included that we can just instantiate.
def master_helper_class
return @master_helper_class if @master_helper_class
@master_helper_class = Class.new(ActionView::Base).tap do |klass|
klass.send(:include, master_helper_module)
end
end
def master_helper_module=(mod)
write_inheritable_attribute(:master_helper_module, mod)
@master_helper_class = nil
end
# Makes all the (instance) methods in the helper module available to templates rendered through this controller.
# See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules
# available to the templates.
@@ -182,8 +198,7 @@ module ActionController #:nodoc:
# Provides a proxy to access helpers methods from outside the view.
def helpers
unless @helper_proxy
@helper_proxy = ActionView::Base.new
@helper_proxy.extend master_helper_module
@helper_proxy = master_helper_class.new
else
@helper_proxy
end

View File

@@ -423,13 +423,13 @@ EOM
# Override Rack's GET method to support indifferent access
def GET
@env["action_controller.request.query_parameters"] ||= normalize_parameters(super)
@env["action_controller.request.query_parameters"] ||= deep_munge(normalize_parameters(super) || {})
end
alias_method :query_parameters, :GET
# Override Rack's POST method to support indifferent access
def POST
@env["action_controller.request.request_parameters"] ||= normalize_parameters(super)
@env["action_controller.request.request_parameters"] ||= deep_munge(normalize_parameters(super) || {})
end
alias_method :request_parameters, :POST
@@ -469,6 +469,22 @@ EOM
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
end
# Remove nils from the params hash
def deep_munge(hash)
hash.each do |k, v|
case v
when Array
v.grep(Hash) { |x| deep_munge(x) }
v.compact!
hash[k] = nil if v.empty?
when Hash
deep_munge(v)
end
end
hash
end
# Convert nested Hashs to HashWithIndifferentAccess and replace
# file upload hashs with UploadedFile objects
def normalize_parameters(value)

View File

@@ -37,7 +37,7 @@ module ActionController
# Note that changing digest or secret invalidates all existing sessions!
class CookieStore
include AbstractStore::SessionUtils
# Cookies can typically store 4096 bytes.
MAX = 4096
SECRET_MIN_LENGTH = 30 # characters
@@ -95,14 +95,21 @@ module ActionController
def call(env)
prepare!(env)
status, headers, body = @app.call(env)
session_data = env[ENV_SESSION_KEY]
options = env[ENV_SESSION_OPTIONS_KEY]
request = ActionController::Request.new(env)
if !(options[:secure] && !request.ssl?) && (!session_data.is_a?(AbstractStore::SessionHash) || session_data.loaded? || options[:expire_after])
# Backport standard Rack::Session::Cookie behavior
# Skip writing session if env['rack.session.options'][:skip] is set
if options[:skip]
return [status, headers, body]
end
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.loaded?
persistent_session_id!(session_data)
@@ -122,7 +129,7 @@ module ActionController
end
private
def prepare!(env)
env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
env[ENV_SESSION_OPTIONS_KEY] = AbstractStore::OptionsHash.new(self, env, @default_options)
@@ -133,7 +140,7 @@ module ActionController
data = persistent_session_id!(data)
[data[:session_id], data]
end
def extract_session_id(env)
if data = unpacked_cookie_data(env)
persistent_session_id!(data) unless data.empty?

View File

@@ -203,24 +203,10 @@ module ActionView #:nodoc:
ActionView::PathSet.new(Array(value))
end
attr_reader :helpers
class ProxyModule < Module
def initialize(receiver)
@receiver = receiver
end
def include(*args)
super(*args)
@receiver.extend(*args)
end
end
def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil)#:nodoc:
@assigns = assigns_for_first_render
@assigns_added = nil
@controller = controller
@helpers = ProxyModule.new(self)
self.view_paths = view_paths
@_first_render = nil

View File

@@ -768,7 +768,11 @@ module ActionView
options = options.stringify_keys
tag_value = options.delete("value")
name_and_id = options.dup
name_and_id["id"] = name_and_id["for"]
if name_and_id.has_key?("for")
name_and_id["id"] = name_and_id["for"]
else
name_and_id.delete("id")
end
add_default_name_and_id_for_value(tag_value, name_and_id)
options.delete("index")
options["for"] ||= name_and_id["id"]
@@ -928,15 +932,15 @@ module ActionView
def add_default_name_and_id(options)
if options.has_key?("index")
options["name"] ||= tag_name_with_index(options["index"])
options["id"] ||= tag_id_with_index(options["index"])
options["name"] = tag_name_with_index(options["index"]) unless options.has_key?("name")
options["id"] = tag_id_with_index(options["index"]) unless options.has_key?("id")
options.delete("index")
elsif defined?(@auto_index)
options["name"] ||= tag_name_with_index(@auto_index)
options["id"] ||= tag_id_with_index(@auto_index)
options["name"] = tag_name_with_index(@auto_index) unless options.has_key?("name")
options["id"] = tag_id_with_index(@auto_index) unless options.has_key?("id")
else
options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '')
options["id"] ||= tag_id
options["name"] = tag_name + (options.has_key?('multiple') ? '[]' : '') unless options.has_key?("name")
options["id"] = tag_id unless options.has_key?("id")
end
end

View File

@@ -73,6 +73,8 @@ module ActionView
def number_to_currency(number, options = {})
options.symbolize_keys!
options[:format] = ERB::Util.html_escape(options[:format]) if options[:format]
defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {}
currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :raise => true) rescue {}
defaults = defaults.merge(currency)
@@ -85,11 +87,13 @@ module ActionView
separator = '' if precision == 0
begin
format.gsub(/%n/, number_with_precision(number,
value = number_with_precision(number,
:precision => precision,
:delimiter => delimiter,
:separator => separator)
).gsub(/%u/, unit).html_safe
value = ERB::Util.html_escape(value) if value
unit = ERB::Util.html_escape(unit)
format.gsub(/%n/, value).gsub(/%u/, unit).html_safe
rescue
number
end

View File

@@ -17,7 +17,7 @@ module ActionView
src << "@output_buffer.safe_append='"
src << "\n" * @newline_pending if @newline_pending > 0
src << escape_text(text)
src << "';"
src << "'.freeze;"
@newline_pending = 0
end
@@ -63,7 +63,7 @@ module ActionView
def flush_newline_if_pending(src)
if @newline_pending > 0
src << "@output_buffer.safe_append='#{"\n" * @newline_pending}';"
src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;"
@newline_pending = 0
end
end

View File

@@ -116,8 +116,7 @@ module ActionView
end
def _view
view = ActionView::Base.new(ActionController::Base.view_paths, _assigns, @controller)
view.helpers.include master_helper_module
view = self.class.master_helper_class.new(ActionController::Base.view_paths, _assigns, @controller)
view.output_buffer = self.output_buffer
view
end

View File

@@ -622,6 +622,19 @@ class FragmentCachingTest < ActionController::TestCase
assert_equal 'generated till now -> fragment content', buffer
end
def test_fragment_for_bytesize
buffer = "\xC4\x8D"
buffer.force_encoding('ASCII-8BIT')
@controller.fragment_for(buffer, 'bytesize') do
buffer.force_encoding('UTF-8')
buffer << "abc"
end
assert_equal Encoding::UTF_8, buffer.encoding
assert_equal "abc", @store.read('views/bytesize')
end
def test_html_safety
assert_nil @store.read('views/name')
content = 'value'.html_safe

View File

@@ -45,7 +45,7 @@ class DispatcherTest < Test::Unit::TestCase
def test_rebuilds_middleware_stack_on_every_request_if_in_loading_mode
dispatcher = create_dispatcher(false)
dispatcher.instance_variable_set(:"@app", lambda { |env| })
dispatcher.expects(:build_middleware_stack).twice
dispatcher.expects(:build_middleware_stack).never
dispatcher.call(nil)
Reloader.default_lock.unlock
dispatcher.call(nil)

View File

@@ -5,7 +5,6 @@ class BaseRackTest < ActiveSupport::TestCase
@env = {
"HTTP_MAX_FORWARDS" => "10",
"SERVER_NAME" => "glu.ttono.us",
"FCGI_ROLE" => "RESPONDER",
"AUTH_TYPE" => "Basic",
"HTTP_X_FORWARDED_HOST" => "glu.ttono.us",
"HTTP_ACCEPT_CHARSET" => "UTF-8",

View File

@@ -175,6 +175,10 @@ class FormHelperTest < ActionView::TestCase
I18n.locale = old_locale
end
def test_label_with_for_attribute_as_nil
assert_dom_equal('<label>Title</label>', label(:post, :title, nil, :for => nil))
end
def test_label_with_for_attribute_as_symbol
assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, :for => "my_for"))
end
@@ -274,6 +278,11 @@ class FormHelperTest < ActionView::TestCase
hidden_field("post", "title", :value => "Something Else")
end
def test_text_field_with_id_as_nil
assert_dom_equal '<input name="post[title]" type="hidden" value="Hello World" />',
hidden_field("post", "title", :id => nil)
end
def test_check_box
assert_dom_equal(
'<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',

View File

@@ -3,6 +3,12 @@ require 'abstract_unit'
class NumberHelperTest < ActionView::TestCase
tests ActionView::Helpers::NumberHelper
def test_number_helpers_escape_delimiter_and_separator
assert_equal "$1&lt;script&gt;&lt;/script&gt;01", number_to_currency(1.01, :separator => "<script></script>")
assert_equal "$1&lt;script&gt;&lt;/script&gt;000.00", number_to_currency(1000, :delimiter => "<script></script>")
assert_equal "&lt;script&gt;1,000.00$&lt;/script&gt;", number_to_currency(1000, :format => "<script>%n%u</script>")
end
def test_number_to_phone
assert_equal("555-1234", number_to_phone(5551234))
assert_equal("800-555-1212", number_to_phone(8005551212))
@@ -24,9 +30,10 @@ class NumberHelperTest < ActionView::TestCase
assert_equal("$1,234,567,890.51", number_to_currency(1234567890.506))
assert_equal("$1,234,567,892", number_to_currency(1234567891.50, {:precision => 0}))
assert_equal("$1,234,567,890.5", number_to_currency(1234567890.50, {:precision => 1}))
assert_equal("&pound;1234567890,50", number_to_currency(1234567890.50, {:unit => "&pound;", :separator => ",", :delimiter => ""}))
assert_equal("&pound;1234567890,50", number_to_currency(1234567890.50, {:unit => raw("&pound;"), :separator => ",", :delimiter => ""}))
assert_equal("&amp;pound;1234567890,50", number_to_currency(1234567890.50, {:unit => "&pound;", :separator => ",", :delimiter => ""}))
assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50"))
assert_equal("1,234,567,890.50 K&#269;", number_to_currency("1234567890.50", {:unit => "K&#269;", :format => "%n %u"}))
assert_equal("1,234,567,890.50 K&#269;", number_to_currency("1234567890.50", {:unit => raw("K&#269;"), :format => "%n %u"}))
#assert_equal("$x.", number_to_currency("x")) # fails due to API consolidation
assert_equal("$x", number_to_currency("x"))
assert_nil number_to_currency(nil)

View File

@@ -9,8 +9,8 @@ module RenderTestCases
# Reload and register danish language for testing
I18n.reload!
I18n.backend.store_translations 'da', {}
I18n.backend.store_translations 'pt-BR', {}
I18n.backend.store_translations 'da', 'da' => {}
I18n.backend.store_translations 'pt-BR', 'pt-BR' => {}
# Ensure original are still the same since we are reindexing view paths
assert_equal ORIGINAL_LOCALES, I18n.available_locales.map(&:to_s).sort

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

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

@@ -195,7 +195,9 @@ module ActiveRecord
def log_info(sql, name, ms)
if @logger && @logger.debug?
name = '%s (%.1fms)' % [name || 'SQL', ms]
sql.force_encoding 'binary' if sql.respond_to?(:force_encoding)
if sql.respond_to?(:force_encoding)
sql = sql.dup.force_encoding 'binary'
end
@logger.debug(format_log_entry(name, sql.squeeze(' ')))
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,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'

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env ruby
Dir.chdir(File.expand_path("..", __FILE__))
$: << File.expand_path("../lib", __FILE__)
require "active_support"
ActiveSupport::TimeZone.all
def flatten_constants(mod, ary = [])
ary << mod
mod.constants.each do |const|
flatten_constants(mod.const_get(const), ary)
end
ary
end
defns = flatten_constants(TZInfo::Definitions).select { |mod|
defined?(mod.get)
}.map { |tz|
tz.get
}
file = "lib/active_support/vendor/tzinfo-0.3.12/tzinfo/definitions.dump"
data = Marshal.dump(defns)
Marshal.load(data)
File.open(file, "wb") do |f|
require "pry"
pry binding
f.write(data)
end
puts "Wrote #{data.size} bytes to #{file}"

View File

@@ -0,0 +1,134 @@
module ActiveSupport
# A typical module looks like this:
#
# module M
# def self.included(base)
# base.extend ClassMethods
# base.class_eval do
# scope :disabled, -> { where(disabled: true) }
# end
# end
#
# module ClassMethods
# ...
# end
# end
#
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be
# written as:
#
# require 'active_support/concern'
#
# module M
# extend ActiveSupport::Concern
#
# included do
# scope :disabled, -> { where(disabled: true) }
# end
#
# module ClassMethods
# ...
# 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:
#
# module Foo
# def self.included(base)
# base.class_eval do
# def self.method_injected_by_foo
# ...
# end
# end
# end
# end
#
# module Bar
# def self.included(base)
# base.method_injected_by_foo
# end
# end
#
# class Host
# include Foo # We need to include this dependency for Bar
# 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+:
#
# module Bar
# include Foo
# def self.included(base)
# base.method_injected_by_foo
# end
# end
#
# class Host
# 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:
#
# require 'active_support/concern'
#
# module Foo
# extend ActiveSupport::Concern
# included do
# def self.method_injected_by_foo
# ...
# end
# end
# end
#
# module Bar
# extend ActiveSupport::Concern
# include Foo
#
# included do
# self.method_injected_by_foo
# end
# end
#
# 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, [])
end
def append_features(base)
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)
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

View File

@@ -1,5 +1,4 @@
require 'active_support/core_ext/kernel/daemonizing'
require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/kernel/agnostics'
require 'active_support/core_ext/kernel/requires'
require 'active_support/core_ext/kernel/debugger'

View File

@@ -1,11 +0,0 @@
class Object
# Makes backticks behave (somewhat more) similarly on all platforms.
# On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the
# spawned shell prints a message to stderr and sets $?. We emulate
# Unix on the former but not the latter.
def `(command) #:nodoc:
super
rescue Errno::ENOENT => e
STDERR.puts "#$0: #{e}"
end
end

View File

@@ -79,12 +79,6 @@ module ActiveSupport
constants(false)
end
end
# Returns the names of the constants defined locally rather than the
# constants themselves. See <tt>local_constants</tt>.
def local_constant_names
local_constants.map { |c| c.to_s }
end
end
end
end

View File

@@ -82,7 +82,7 @@ class String
# 1.8 does not takes [:space:] properly
if encoding_aware?
self !~ /[^[:space:]]/
self =~ /\A[[:space:]]*\z/
else
self !~ NON_WHITESPACE_REGEXP
end

View File

@@ -172,6 +172,7 @@ module ActiveSupport #:nodoc:
else
load_without_new_constant_marking(file, *extras)
end
nil
rescue Exception => exception # errors from loading file
exception.blame_file! file
raise
@@ -180,6 +181,7 @@ module ActiveSupport #:nodoc:
def require(file, *extras) #:nodoc:
if Dependencies.load?
Dependencies.new_constants_in(Object) { super }
true
else
super
end
@@ -521,13 +523,13 @@ module ActiveSupport #:nodoc:
watch_frames = descs.collect do |desc|
if desc.is_a? Module
mod_name = desc.name
initial_constants = desc.local_constant_names
initial_constants = desc.local_constants
elsif desc.is_a?(String) || desc.is_a?(Symbol)
mod_name = desc.to_s
# Handle the case where the module has yet to be defined.
initial_constants = if qualified_const_defined?(mod_name)
mod_name.constantize.local_constant_names
mod_name.constantize.local_constants
else
[]
end
@@ -554,7 +556,7 @@ module ActiveSupport #:nodoc:
mod = mod_name.constantize
next [] unless mod.is_a? Module
new_constants = mod.local_constant_names - prior_constants
new_constants = mod.local_constants - prior_constants
# Make sure no other frames takes credit for these constants.
constant_watch_stack_mutex.synchronize do
@@ -577,7 +579,9 @@ module ActiveSupport #:nodoc:
end
end
return new_constants
# XXX trace callers to this method and make them expect an array of
# symbols instead of strings and remove this to_s - charliesome
return new_constants.map(&:to_s)
ensure
# Remove the stack frames that we added.
if defined?(watch_frames) && ! watch_frames.blank?

View File

@@ -1,11 +1,6 @@
# Prefer gems to the bundled libs.
require 'rubygems'
begin
gem 'builder', '~> 2.1.2'
rescue Gem::LoadError
$:.unshift "#{File.dirname(__FILE__)}/vendor/builder-2.1.2"
end
require 'builder'
begin
@@ -14,17 +9,8 @@ rescue Gem::LoadError
$:.unshift "#{File.dirname(__FILE__)}/vendor/memcache-client-1.7.4"
end
begin
gem 'tzinfo', '~> 0.3.12'
rescue Gem::LoadError
$:.unshift "#{File.dirname(__FILE__)}/vendor/tzinfo-0.3.12"
end
$:.unshift "#{File.dirname(__FILE__)}/vendor/tzinfo-0.3.12"
begin
gem 'i18n', '>= 0.4.1'
rescue Gem::LoadError
$:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.4.1"
end
require 'i18n'
module I18n

View File

@@ -1,114 +0,0 @@
#!/usr/bin/env ruby
#--
# Copyright 2004, 2006 by Jim Weirich (jim@weirichhouse.org).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
######################################################################
# BlankSlate provides an abstract base class with no predefined
# methods (except for <tt>\_\_send__</tt> and <tt>\_\_id__</tt>).
# BlankSlate is useful as a base class when writing classes that
# depend upon <tt>method_missing</tt> (e.g. dynamic proxies).
#
class BlankSlate
class << self
# Hide the method named +name+ in the BlankSlate class. Don't
# hide +instance_eval+ or any method beginning with "__".
def hide(name)
if (instance_methods.include?(name) || instance_methods.include?(name.to_sym)) and
name !~ /^(__|instance_eval|object_id)/
@hidden_methods ||= {}
@hidden_methods[name] = instance_method(name)
undef_method name
end
end
def find_hidden_method(name)
@hidden_methods ||= {}
@hidden_methods[name] || superclass.find_hidden_method(name)
end
# Redefine a previously hidden method so that it may be called on a blank
# slate object.
def reveal(name)
bound_method = nil
unbound_method = find_hidden_method(name)
fail "Don't know how to reveal method '#{name}'" unless unbound_method
define_method(name) do |*args|
bound_method ||= unbound_method.bind(self)
bound_method.call(*args)
end
end
end
instance_methods.each { |m| hide(m) }
end
######################################################################
# Since Ruby is very dynamic, methods added to the ancestors of
# BlankSlate <em>after BlankSlate is defined</em> will show up in the
# list of available BlankSlate methods. We handle this by defining a
# hook in the Object and Kernel classes that will hide any method
# defined after BlankSlate has been loaded.
#
module Kernel
class << self
alias_method :blank_slate_method_added, :method_added
# Detect method additions to Kernel and remove them in the
# BlankSlate class.
def method_added(name)
result = blank_slate_method_added(name)
return result if self != Kernel
BlankSlate.hide(name)
result
end
end
end
######################################################################
# Same as above, except in Object.
#
class Object
class << self
alias_method :blank_slate_method_added, :method_added
# Detect method additions to Object and remove them in the
# BlankSlate class.
def method_added(name)
result = blank_slate_method_added(name)
return result if self != Object
BlankSlate.hide(name)
result
end
def find_hidden_method(name)
nil
end
end
end
######################################################################
# Also, modules included into Object need to be scanned and have their
# instance methods removed from blank slate. In theory, modules
# included into Kernel would have to be removed as well, but a
# "feature" of Ruby prevents late includes into modules from being
# exposed in the first place.
#
class Module
alias blankslate_original_append_features append_features
def append_features(mod)
result = blankslate_original_append_features(mod)
return result if mod != Object
instance_methods.each do |name|
BlankSlate.hide(name)
end
result
end
end

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env ruby
#--
# Copyright 2004 by Jim Weirich (jim@weirichhouse.org).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
require 'builder/xmlmarkup'
require 'builder/xmlevents'

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env ruby
#--
# Copyright 2004, 2006 by Jim Weirich (jim@weirichhouse.org).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
require 'blankslate'
######################################################################
# BlankSlate has been promoted to a top level name and is now
# available as a standalone gem. We make the name available in the
# Builder namespace for compatibility.
#
module Builder
BlankSlate = ::BlankSlate
end

View File

@@ -1,250 +0,0 @@
#!/usr/bin/env ruby
#--
# Copyright 2004, 2005 by Jim Weirich (jim@weirichhouse.org).
# Copyright 2005 by Scott Barron (scott@elitists.net).
# All rights reserved.
#
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#
# Much of this is taken from Jim's work in xmlbase.rb and xmlmarkup.rb.
# Documentation has also been copied and pasted and modified to reflect
# that we're building CSS here instead of XML. Jim is conducting the
# orchestra here and I'm just off in the corner playing a flute.
#++
# Provide a flexible and easy to use Builder for creating Cascading
# Style Sheets (CSS).
require 'builder/blankslate'
module Builder
# Create a Cascading Style Sheet (CSS) using Ruby.
#
# Example usage:
#
# css = Builder::CSS.new
#
# text_color = '#7F7F7F'
# preferred_fonts = 'Helvetica, Arial, sans_serif'
#
# css.comment! 'This is our stylesheet'
# css.body {
# background_color '#FAFAFA'
# font_size 'small'
# font_family preferred_fonts
# color text_color
# }
#
# css.id!('navbar') {
# width '500px'
# }
#
# css.class!('navitem') {
# color 'red'
# }
#
# css.a :hover {
# text_decoration 'underline'
# }
#
# css.div(:id => 'menu') {
# background 'green'
# }
#
# css.div(:class => 'foo') {
# background 'red'
# }
#
# This will yield the following stylesheet:
#
# /* This is our stylesheet */
# body {
# background_color: #FAFAFA;
# font_size: small;
# font_family: Helvetica, Arial, sans_serif;
# color: #7F7F7F;
# }
#
# #navbar {
# width: 500px;
# }
#
# .navitem {
# color: red;
# }
#
# a:hover {
# text_decoration: underline;
# }
#
# div#menu {
# background: green;
# }
#
# div.foo {
# background: red;
# }
#
class CSS < BlankSlate
# Create a CSS builder.
#
# out:: Object receiving the markup.1 +out+ must respond to
# <tt><<</tt>.
# indent:: Number of spaces used for indentation (0 implies no
# indentation and no line breaks).
#
def initialize(indent=2)
@indent = indent
@target = []
@parts = []
@library = {}
end
def +(part)
_join_with_op! '+'
self
end
def >>(part)
_join_with_op! ''
self
end
def >(part)
_join_with_op! '>'
self
end
def |(part)
_join_with_op! ','
self
end
# Return the target of the builder
def target!
@target * ''
end
# Create a comment string in the output.
def comment!(comment_text)
@target << "/* #{comment_text} */\n"
end
def id!(arg, &block)
_start_container('#'+arg.to_s, nil, block_given?)
_css_block(block) if block
_unify_block
self
end
def class!(arg, &block)
_start_container('.'+arg.to_s, nil, block_given?)
_css_block(block) if block
_unify_block
self
end
def store!(sym, &block)
@library[sym] = block.to_proc
end
def group!(*args, &block)
args.each do |arg|
if arg.is_a?(Symbol)
instance_eval(&@library[arg])
else
instance_eval(&arg)
end
_text ', ' unless arg == args.last
end
if block
_css_block(block)
_unify_block
end
end
def method_missing(sym, *args, &block)
sym = "#{sym}:#{args.shift}" if args.first.kind_of?(Symbol)
if block
_start_container(sym, args.first)
_css_block(block)
_unify_block
elsif @in_block
_indent
_css_line(sym, *args)
_newline
return self
else
_start_container(sym, args.first, false)
_unify_block
end
self
end
# "Cargo culted" from Jim who also "cargo culted" it. See xmlbase.rb.
def nil?
false
end
private
def _unify_block
@target << @parts * ''
@parts = []
end
def _join_with_op!(op)
rhs, lhs = @target.pop, @target.pop
@target << "#{lhs} #{op} #{rhs}"
end
def _text(text)
@parts << text
end
def _css_block(block)
_newline
_nested_structures(block)
_end_container
_end_block
end
def _end_block
_newline
_newline
end
def _newline
_text "\n"
end
def _indent
_text ' ' * @indent
end
def _nested_structures(block)
@in_block = true
self.instance_eval(&block)
@in_block = false
end
def _start_container(sym, atts = {}, with_bracket = true)
selector = sym.to_s
selector << ".#{atts[:class]}" if atts && atts[:class]
selector << '#' + "#{atts[:id]}" if atts && atts[:id]
@parts << "#{selector}#{with_bracket ? ' {' : ''}"
end
def _end_container
@parts << "}"
end
def _css_line(sym, *args)
_text("#{sym.to_s.gsub('_','-')}: #{args * ' '};")
end
end
end

View File

@@ -1,115 +0,0 @@
#!/usr/bin/env ruby
# The XChar library is provided courtesy of Sam Ruby (See
# http://intertwingly.net/stories/2005/09/28/xchar.rb)
# --------------------------------------------------------------------
# If the Builder::XChar module is not currently defined, fail on any
# name clashes in standard library classes.
module Builder
def self.check_for_name_collision(klass, method_name, defined_constant=nil)
if klass.instance_methods.include?(method_name.to_s)
fail RuntimeError,
"Name Collision: Method '#{method_name}' is already defined in #{klass}"
end
end
end
if ! defined?(Builder::XChar)
Builder.check_for_name_collision(String, "to_xs")
Builder.check_for_name_collision(Fixnum, "xchr")
end
######################################################################
module Builder
####################################################################
# XML Character converter, from Sam Ruby:
# (see http://intertwingly.net/stories/2005/09/28/xchar.rb).
#
module XChar # :nodoc:
# See
# http://intertwingly.net/stories/2004/04/14/i18n.html#CleaningWindows
# for details.
CP1252 = { # :nodoc:
128 => 8364, # euro sign
130 => 8218, # single low-9 quotation mark
131 => 402, # latin small letter f with hook
132 => 8222, # double low-9 quotation mark
133 => 8230, # horizontal ellipsis
134 => 8224, # dagger
135 => 8225, # double dagger
136 => 710, # modifier letter circumflex accent
137 => 8240, # per mille sign
138 => 352, # latin capital letter s with caron
139 => 8249, # single left-pointing angle quotation mark
140 => 338, # latin capital ligature oe
142 => 381, # latin capital letter z with caron
145 => 8216, # left single quotation mark
146 => 8217, # right single quotation mark
147 => 8220, # left double quotation mark
148 => 8221, # right double quotation mark
149 => 8226, # bullet
150 => 8211, # en dash
151 => 8212, # em dash
152 => 732, # small tilde
153 => 8482, # trade mark sign
154 => 353, # latin small letter s with caron
155 => 8250, # single right-pointing angle quotation mark
156 => 339, # latin small ligature oe
158 => 382, # latin small letter z with caron
159 => 376, # latin capital letter y with diaeresis
}
# See http://www.w3.org/TR/REC-xml/#dt-chardata for details.
PREDEFINED = {
38 => '&amp;', # ampersand
60 => '&lt;', # left angle bracket
62 => '&gt;', # right angle bracket
}
# See http://www.w3.org/TR/REC-xml/#charsets for details.
VALID = [
0x9, 0xA, 0xD,
(0x20..0xD7FF),
(0xE000..0xFFFD),
(0x10000..0x10FFFF)
]
end
end
######################################################################
# Enhance the Fixnum class with a XML escaped character conversion.
#
class Fixnum
XChar = Builder::XChar if ! defined?(XChar)
# XML escaped version of chr
def xchr
n = XChar::CP1252[self] || self
case n when *XChar::VALID
XChar::PREDEFINED[n] or (n<128 ? n.chr : "&##{n};")
else
'*'
end
end
end
######################################################################
# Enhance the String class with a XML escaped character version of
# to_s.
#
class String
# XML escaped version of to_s
def to_xs
unpack('U*').map {|n| n.xchr}.join # ASCII, UTF-8
rescue
unpack('C*').map {|n| n.xchr}.join # ISO-8859-1, WIN-1252
end
end

View File

@@ -1,139 +0,0 @@
#!/usr/bin/env ruby
require 'builder/blankslate'
module Builder
# Generic error for builder
class IllegalBlockError < RuntimeError; end
# XmlBase is a base class for building XML builders. See
# Builder::XmlMarkup and Builder::XmlEvents for examples.
class XmlBase < BlankSlate
# Create an XML markup builder.
#
# out:: Object receiving the markup. +out+ must respond to
# <tt><<</tt>.
# indent:: Number of spaces used for indentation (0 implies no
# indentation and no line breaks).
# initial:: Level of initial indentation.
#
def initialize(indent=0, initial=0)
@indent = indent
@level = initial
end
# Create a tag named +sym+. Other than the first argument which
# is the tag name, the arguments are the same as the tags
# implemented via <tt>method_missing</tt>.
def tag!(sym, *args, &block)
method_missing(sym.to_sym, *args, &block)
end
# Create XML markup based on the name of the method. This method
# is never invoked directly, but is called for each markup method
# in the markup block.
def method_missing(sym, *args, &block)
text = nil
attrs = nil
sym = "#{sym}:#{args.shift}" if args.first.kind_of?(Symbol)
args.each do |arg|
case arg
when Hash
attrs ||= {}
attrs.merge!(arg)
else
text ||= ''
text << arg.to_s
end
end
if block
unless text.nil?
raise ArgumentError, "XmlMarkup cannot mix a text argument with a block"
end
_indent
_start_tag(sym, attrs)
_newline
_nested_structures(block)
_indent
_end_tag(sym)
_newline
elsif text.nil?
_indent
_start_tag(sym, attrs, true)
_newline
else
_indent
_start_tag(sym, attrs)
text! text
_end_tag(sym)
_newline
end
@target
end
# Append text to the output target. Escape any markup. May be
# used within the markup brackets as:
#
# builder.p { |b| b.br; b.text! "HI" } #=> <p><br/>HI</p>
def text!(text)
_text(_escape(text))
end
# Append text to the output target without escaping any markup.
# May be used within the markup brackets as:
#
# builder.p { |x| x << "<br/>HI" } #=> <p><br/>HI</p>
#
# This is useful when using non-builder enabled software that
# generates strings. Just insert the string directly into the
# builder without changing the inserted markup.
#
# It is also useful for stacking builder objects. Builders only
# use <tt><<</tt> to append to the target, so by supporting this
# method/operation builders can use other builders as their
# targets.
def <<(text)
_text(text)
end
# For some reason, nil? is sent to the XmlMarkup object. If nil?
# is not defined and method_missing is invoked, some strange kind
# of recursion happens. Since nil? won't ever be an XML tag, it
# is pretty safe to define it here. (Note: this is an example of
# cargo cult programming,
# cf. http://fishbowl.pastiche.org/2004/10/13/cargo_cult_programming).
def nil?
false
end
private
require 'builder/xchar'
def _escape(text)
text.to_xs
end
def _escape_quote(text)
_escape(text).gsub(%r{"}, '&quot;') # " WART
end
def _newline
return if @indent == 0
text! "\n"
end
def _indent
return if @indent == 0 || @level == 0
text!(" " * (@level * @indent))
end
def _nested_structures(block)
@level += 1
block.call(self)
ensure
@level -= 1
end
end
end

View File

@@ -1,63 +0,0 @@
#!/usr/bin/env ruby
#--
# Copyright 2004 by Jim Weirich (jim@weirichhouse.org).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
require 'builder/xmlmarkup'
module Builder
# Create a series of SAX-like XML events (e.g. start_tag, end_tag)
# from the markup code. XmlEvent objects are used in a way similar
# to XmlMarkup objects, except that a series of events are generated
# and passed to a handler rather than generating character-based
# markup.
#
# Usage:
# xe = Builder::XmlEvents.new(handler)
# xe.title("HI") # Sends start_tag/end_tag/text messages to the handler.
#
# Indentation may also be selected by providing value for the
# indentation size and initial indentation level.
#
# xe = Builder::XmlEvents.new(handler, indent_size, initial_indent_level)
#
# == XML Event Handler
#
# The handler object must expect the following events.
#
# [<tt>start_tag(tag, attrs)</tt>]
# Announces that a new tag has been found. +tag+ is the name of
# the tag and +attrs+ is a hash of attributes for the tag.
#
# [<tt>end_tag(tag)</tt>]
# Announces that an end tag for +tag+ has been found.
#
# [<tt>text(text)</tt>]
# Announces that a string of characters (+text+) has been found.
# A series of characters may be broken up into more than one
# +text+ call, so the client cannot assume that a single
# callback contains all the text data.
#
class XmlEvents < XmlMarkup
def text!(text)
@target.text(text)
end
def _start_tag(sym, attrs, end_too=false)
@target.start_tag(sym, attrs)
_end_tag(sym) if end_too
end
def _end_tag(sym)
@target.end_tag(sym)
end
end
end

View File

@@ -1,328 +0,0 @@
#!/usr/bin/env ruby
#--
# Copyright 2004, 2005 by Jim Weirich (jim@weirichhouse.org).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
# Provide a flexible and easy to use Builder for creating XML markup.
# See XmlBuilder for usage details.
require 'builder/xmlbase'
module Builder
# Create XML markup easily. All (well, almost all) methods sent to
# an XmlMarkup object will be translated to the equivalent XML
# markup. Any method with a block will be treated as an XML markup
# tag with nested markup in the block.
#
# Examples will demonstrate this easier than words. In the
# following, +xm+ is an +XmlMarkup+ object.
#
# xm.em("emphasized") # => <em>emphasized</em>
# xm.em { xmm.b("emp & bold") } # => <em><b>emph &amp; bold</b></em>
# xm.a("A Link", "href"=>"http://onestepback.org")
# # => <a href="http://onestepback.org">A Link</a>
# xm.div { br } # => <div><br/></div>
# xm.target("name"=>"compile", "option"=>"fast")
# # => <target option="fast" name="compile"\>
# # NOTE: order of attributes is not specified.
#
# xm.instruct! # <?xml version="1.0" encoding="UTF-8"?>
# xm.html { # <html>
# xm.head { # <head>
# xm.title("History") # <title>History</title>
# } # </head>
# xm.body { # <body>
# xm.comment! "HI" # <! -- HI -->
# xm.h1("Header") # <h1>Header</h1>
# xm.p("paragraph") # <p>paragraph</p>
# } # </body>
# } # </html>
#
# == Notes:
#
# * The order that attributes are inserted in markup tags is
# undefined.
#
# * Sometimes you wish to insert text without enclosing tags. Use
# the <tt>text!</tt> method to accomplish this.
#
# Example:
#
# xm.div { # <div>
# xm.text! "line"; xm.br # line<br/>
# xm.text! "another line"; xmbr # another line<br/>
# } # </div>
#
# * The special XML characters <, >, and & are converted to &lt;,
# &gt; and &amp; automatically. Use the <tt><<</tt> operation to
# insert text without modification.
#
# * Sometimes tags use special characters not allowed in ruby
# identifiers. Use the <tt>tag!</tt> method to handle these
# cases.
#
# Example:
#
# xml.tag!("SOAP:Envelope") { ... }
#
# will produce ...
#
# <SOAP:Envelope> ... </SOAP:Envelope>"
#
# <tt>tag!</tt> will also take text and attribute arguments (after
# the tag name) like normal markup methods. (But see the next
# bullet item for a better way to handle XML namespaces).
#
# * Direct support for XML namespaces is now available. If the
# first argument to a tag call is a symbol, it will be joined to
# the tag to produce a namespace:tag combination. It is easier to
# show this than describe it.
#
# xml.SOAP :Envelope do ... end
#
# Just put a space before the colon in a namespace to produce the
# right form for builder (e.g. "<tt>SOAP:Envelope</tt>" =>
# "<tt>xml.SOAP :Envelope</tt>")
#
# * XmlMarkup builds the markup in any object (called a _target_)
# that accepts the <tt><<</tt> method. If no target is given,
# then XmlMarkup defaults to a string target.
#
# Examples:
#
# xm = Builder::XmlMarkup.new
# result = xm.title("yada")
# # result is a string containing the markup.
#
# buffer = ""
# xm = Builder::XmlMarkup.new(buffer)
# # The markup is appended to buffer (using <<)
#
# xm = Builder::XmlMarkup.new(STDOUT)
# # The markup is written to STDOUT (using <<)
#
# xm = Builder::XmlMarkup.new
# x2 = Builder::XmlMarkup.new(:target=>xm)
# # Markup written to +x2+ will be send to +xm+.
#
# * Indentation is enabled by providing the number of spaces to
# indent for each level as a second argument to XmlBuilder.new.
# Initial indentation may be specified using a third parameter.
#
# Example:
#
# xm = Builder.new(:indent=>2)
# # xm will produce nicely formatted and indented XML.
#
# xm = Builder.new(:indent=>2, :margin=>4)
# # xm will produce nicely formatted and indented XML with 2
# # spaces per indent and an over all indentation level of 4.
#
# builder = Builder::XmlMarkup.new(:target=>$stdout, :indent=>2)
# builder.name { |b| b.first("Jim"); b.last("Weirich) }
# # prints:
# # <name>
# # <first>Jim</first>
# # <last>Weirich</last>
# # </name>
#
# * The instance_eval implementation which forces self to refer to
# the message receiver as self is now obsolete. We now use normal
# block calls to execute the markup block. This means that all
# markup methods must now be explicitly send to the xml builder.
# For instance, instead of
#
# xml.div { strong("text") }
#
# you need to write:
#
# xml.div { xml.strong("text") }
#
# Although more verbose, the subtle change in semantics within the
# block was found to be prone to error. To make this change a
# little less cumbersome, the markup block now gets the markup
# object sent as an argument, allowing you to use a shorter alias
# within the block.
#
# For example:
#
# xml_builder = Builder::XmlMarkup.new
# xml_builder.div { |xml|
# xml.stong("text")
# }
#
class XmlMarkup < XmlBase
# Create an XML markup builder. Parameters are specified by an
# option hash.
#
# :target=><em>target_object</em>::
# Object receiving the markup. +out+ must respond to the
# <tt><<</tt> operator. The default is a plain string target.
#
# :indent=><em>indentation</em>::
# Number of spaces used for indentation. The default is no
# indentation and no line breaks.
#
# :margin=><em>initial_indentation_level</em>::
# Amount of initial indentation (specified in levels, not
# spaces).
#
# :escape_attrs=><b>OBSOLETE</em>::
# The :escape_attrs option is no longer supported by builder
# (and will be quietly ignored). String attribute values are
# now automatically escaped. If you need unescaped attribute
# values (perhaps you are using entities in the attribute
# values), then give the value as a Symbol. This allows much
# finer control over escaping attribute values.
#
def initialize(options={})
indent = options[:indent] || 0
margin = options[:margin] || 0
super(indent, margin)
@target = options[:target] || ""
end
# Return the target of the builder.
def target!
@target
end
def comment!(comment_text)
_ensure_no_block block_given?
_special("<!-- ", " -->", comment_text, nil)
end
# Insert an XML declaration into the XML markup.
#
# For example:
#
# xml.declare! :ELEMENT, :blah, "yada"
# # => <!ELEMENT blah "yada">
def declare!(inst, *args, &block)
_indent
@target << "<!#{inst}"
args.each do |arg|
case arg
when String
@target << %{ "#{arg}"} # " WART
when Symbol
@target << " #{arg}"
end
end
if block_given?
@target << " ["
_newline
_nested_structures(block)
@target << "]"
end
@target << ">"
_newline
end
# Insert a processing instruction into the XML markup. E.g.
#
# For example:
#
# xml.instruct!
# #=> <?xml version="1.0" encoding="UTF-8"?>
# xml.instruct! :aaa, :bbb=>"ccc"
# #=> <?aaa bbb="ccc"?>
#
def instruct!(directive_tag=:xml, attrs={})
_ensure_no_block block_given?
if directive_tag == :xml
a = { :version=>"1.0", :encoding=>"UTF-8" }
attrs = a.merge attrs
end
_special(
"<?#{directive_tag}",
"?>",
nil,
attrs,
[:version, :encoding, :standalone])
end
# Insert a CDATA section into the XML markup.
#
# For example:
#
# xml.cdata!("text to be included in cdata")
# #=> <![CDATA[text to be included in cdata]]>
#
def cdata!(text)
_ensure_no_block block_given?
_special("<![CDATA[", "]]>", text, nil)
end
private
# NOTE: All private methods of a builder object are prefixed when
# a "_" character to avoid possible conflict with XML tag names.
# Insert text directly in to the builder's target.
def _text(text)
@target << text
end
# Insert special instruction.
def _special(open, close, data=nil, attrs=nil, order=[])
_indent
@target << open
@target << data if data
_insert_attributes(attrs, order) if attrs
@target << close
_newline
end
# Start an XML tag. If <tt>end_too</tt> is true, then the start
# tag is also the end tag (e.g. <br/>
def _start_tag(sym, attrs, end_too=false)
@target << "<#{sym}"
_insert_attributes(attrs)
@target << "/" if end_too
@target << ">"
end
# Insert an ending tag.
def _end_tag(sym)
@target << "</#{sym}>"
end
# Insert the attributes (given in the hash).
def _insert_attributes(attrs, order=[])
return if attrs.nil?
order.each do |k|
v = attrs[k]
@target << %{ #{k}="#{_attr_value(v)}"} if v # " WART
end
attrs.each do |k, v|
@target << %{ #{k}="#{_attr_value(v)}"} unless order.member?(k) # " WART
end
end
def _attr_value(value)
case value
when Symbol
value.to_s
else
_escape_quote(value.to_s)
end
end
def _ensure_no_block(got_block)
if got_block
fail IllegalBlockError,
"Blocks are not allowed on XML instructions"
end
end
end
end

View File

@@ -1,322 +0,0 @@
# encoding: utf-8
# Authors:: Sven Fuchs (http://www.artweb-design.de),
# Joshua Harvey (http://www.workingwithrails.com/person/759-joshua-harvey),
# Stephan Soller (http://www.arkanis-development.de/),
# Saimon Moore (http://saimonmoore.net),
# Matt Aimonetti (http://railsontherun.com/)
# Copyright:: Copyright (c) 2008 The Ruby i18n Team
# License:: MIT
require 'i18n/exceptions'
require 'i18n/core_ext/string/interpolate'
module I18n
autoload :Backend, 'i18n/backend'
autoload :Config, 'i18n/config'
autoload :Gettext, 'i18n/gettext'
autoload :Locale, 'i18n/locale'
class << self
# Gets I18n configuration object.
def config
Thread.current[:i18n_config] ||= I18n::Config.new
end
# Sets I18n configuration object.
def config=(value)
Thread.current[:i18n_config] = value
end
# Write methods which delegates to the configuration object
%w(locale backend default_locale available_locales default_separator
exception_handler load_path).each do |method|
module_eval <<-DELEGATORS, __FILE__, __LINE__ + 1
def #{method}
config.#{method}
end
def #{method}=(value)
config.#{method} = (value)
end
DELEGATORS
end
# Tells the backend to reload translations. Used in situations like the
# Rails development environment. Backends can implement whatever strategy
# is useful.
def reload!
config.backend.reload!
end
# Translates, pluralizes and interpolates a given key using a given locale,
# scope, and default, as well as interpolation values.
#
# *LOOKUP*
#
# Translation data is organized as a nested hash using the upper-level keys
# as namespaces. <em>E.g.</em>, ActionView ships with the translation:
# <tt>:date => {:formats => {:short => "%b %d"}}</tt>.
#
# Translations can be looked up at any level of this hash using the key argument
# and the scope option. <em>E.g.</em>, in this example <tt>I18n.t :date</tt>
# returns the whole translations hash <tt>{:formats => {:short => "%b %d"}}</tt>.
#
# Key can be either a single key or a dot-separated key (both Strings and Symbols
# work). <em>E.g.</em>, the short format can be looked up using both:
# I18n.t 'date.formats.short'
# I18n.t :'date.formats.short'
#
# Scope can be either a single key, a dot-separated key or an array of keys
# or dot-separated keys. Keys and scopes can be combined freely. So these
# examples will all look up the same short date format:
# I18n.t 'date.formats.short'
# I18n.t 'formats.short', :scope => 'date'
# I18n.t 'short', :scope => 'date.formats'
# I18n.t 'short', :scope => %w(date formats)
#
# *INTERPOLATION*
#
# Translations can contain interpolation variables which will be replaced by
# values passed to #translate as part of the options hash, with the keys matching
# the interpolation variable names.
#
# <em>E.g.</em>, with a translation <tt>:foo => "foo %{bar}"</tt> the option
# value for the key +bar+ will be interpolated into the translation:
# I18n.t :foo, :bar => 'baz' # => 'foo baz'
#
# *PLURALIZATION*
#
# Translation data can contain pluralized translations. Pluralized translations
# are arrays of singluar/plural versions of translations like <tt>['Foo', 'Foos']</tt>.
#
# Note that <tt>I18n::Backend::Simple</tt> only supports an algorithm for English
# pluralization rules. Other algorithms can be supported by custom backends.
#
# This returns the singular version of a pluralized translation:
# I18n.t :foo, :count => 1 # => 'Foo'
#
# These both return the plural version of a pluralized translation:
# I18n.t :foo, :count => 0 # => 'Foos'
# I18n.t :foo, :count => 2 # => 'Foos'
#
# The <tt>:count</tt> option can be used both for pluralization and interpolation.
# <em>E.g.</em>, with the translation
# <tt>:foo => ['%{count} foo', '%{count} foos']</tt>, count will
# be interpolated to the pluralized translation:
# I18n.t :foo, :count => 1 # => '1 foo'
#
# *DEFAULTS*
#
# This returns the translation for <tt>:foo</tt> or <tt>default</tt> if no translation was found:
# I18n.t :foo, :default => 'default'
#
# This returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt> if no
# translation for <tt>:foo</tt> was found:
# I18n.t :foo, :default => :bar
#
# Returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt>
# or <tt>default</tt> if no translations for <tt>:foo</tt> and <tt>:bar</tt> were found.
# I18n.t :foo, :default => [:bar, 'default']
#
# *BULK LOOKUP*
#
# This returns an array with the translations for <tt>:foo</tt> and <tt>:bar</tt>.
# I18n.t [:foo, :bar]
#
# Can be used with dot-separated nested keys:
# I18n.t [:'baz.foo', :'baz.bar']
#
# Which is the same as using a scope option:
# I18n.t [:foo, :bar], :scope => :baz
#
# *LAMBDAS*
#
# Both translations and defaults can be given as Ruby lambdas. Lambdas will be
# called and passed the key and options.
#
# E.g. assuming the key <tt>:salutation</tt> resolves to:
# lambda { |key, options| options[:gender] == 'm' ? "Mr. %{options[:name]}" : "Mrs. %{options[:name]}" }
#
# Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith') will result in "Mrs. Smith".
#
# It is recommended to use/implement lambdas in an "idempotent" way. E.g. when
# a cache layer is put in front of I18n.translate it will generate a cache key
# from the argument values passed to #translate. Therefor your lambdas should
# always return the same translations/values per unique combination of argument
# values.
def translate(*args)
options = args.pop if args.last.is_a?(Hash)
key = args.shift
locale = options && options.delete(:locale) || config.locale
raises = options && options.delete(:raise)
config.backend.translate(locale, key, options || {})
rescue I18n::ArgumentError => exception
raise exception if raises
handle_exception(exception, locale, key, options)
end
alias :t :translate
def translate!(key, options = {})
translate(key, options.merge( :raise => true ))
end
alias :t! :translate!
# Transliterates UTF-8 characters to ASCII. By default this method will
# transliterate only Latin strings to an ASCII approximation:
#
# I18n.transliterate("Ærøskøbing")
# # => "AEroskobing"
#
# I18n.transliterate("日本語")
# # => "???"
#
# It's also possible to add support for per-locale transliterations. I18n
# expects transliteration rules to be stored at
# <tt>i18n.transliterate.rule</tt>.
#
# Transliteration rules can either be a Hash or a Proc. Procs must accept a
# single string argument. Hash rules inherit the default transliteration
# rules, while Procs do not.
#
# *Examples*
#
# Setting a Hash in <locale>.yml:
#
# i18n:
# transliterate:
# rule:
# ü: "ue"
# ö: "oe"
#
# Setting a Hash using Ruby:
#
# store_translations(:de, :i18n => {
# :transliterate => {
# :rule => {
# "ü" => "ue",
# "ö" => "oe"
# }
# }
# )
#
# Setting a Proc:
#
# translit = lambda {|string| MyTransliterator.transliterate(string) }
# store_translations(:xx, :i18n => {:transliterate => {:rule => translit})
#
# Transliterating strings:
#
# I18n.locale = :en
# I18n.transliterate("Jürgen") # => "Jurgen"
# I18n.locale = :de
# I18n.transliterate("Jürgen") # => "Juergen"
# I18n.transliterate("Jürgen", :locale => :en) # => "Jurgen"
# I18n.transliterate("Jürgen", :locale => :de) # => "Juergen"
def transliterate(*args)
options = args.pop if args.last.is_a?(Hash)
key = args.shift
locale = options && options.delete(:locale) || config.locale
raises = options && options.delete(:raise)
replacement = options && options.delete(:replacement)
config.backend.transliterate(locale, key, replacement)
rescue I18n::ArgumentError => exception
raise exception if raises
handle_exception(exception, locale, key, options)
end
# Localizes certain objects, such as dates and numbers to local formatting.
def localize(object, options = {})
locale = options.delete(:locale) || config.locale
format = options.delete(:format) || :default
config.backend.localize(locale, object, format, options)
end
alias :l :localize
# Executes block with given I18n.locale set.
def with_locale(tmp_locale = nil)
if tmp_locale
current_locale = self.locale
self.locale = tmp_locale
end
yield
ensure
self.locale = current_locale if tmp_locale
end
# Merges the given locale, key and scope into a single array of keys.
# Splits keys that contain dots into multiple keys. Makes sure all
# keys are Symbols.
def normalize_keys(locale, key, scope, separator = nil)
separator ||= I18n.default_separator
keys = []
keys.concat normalize_key(locale, separator)
keys.concat normalize_key(scope, separator)
keys.concat normalize_key(key, separator)
keys
end
# making these private until Ruby 1.9.2 can send to protected methods again
# see http://redmine.ruby-lang.org/repositories/revision/ruby-19?rev=24280
private
# Handles exceptions raised in the backend. All exceptions except for
# MissingTranslationData exceptions are re-raised. When a MissingTranslationData
# was caught and the option :raise is not set the handler returns an error
# message string containing the key/scope.
def default_exception_handler(exception, locale, key, options)
return exception.message if MissingTranslationData === exception
raise exception
end
# Any exceptions thrown in translate will be sent to the @@exception_handler
# which can be a Symbol, a Proc or any other Object.
#
# If exception_handler is a Symbol then it will simply be sent to I18n as
# a method call. A Proc will simply be called. In any other case the
# method #call will be called on the exception_handler object.
#
# Examples:
#
# I18n.exception_handler = :default_exception_handler # this is the default
# I18n.default_exception_handler(exception, locale, key, options) # will be called like this
#
# I18n.exception_handler = lambda { |*args| ... } # a lambda
# I18n.exception_handler.call(exception, locale, key, options) # will be called like this
#
# I18n.exception_handler = I18nExceptionHandler.new # an object
# I18n.exception_handler.call(exception, locale, key, options) # will be called like this
def handle_exception(exception, locale, key, options)
case config.exception_handler
when Symbol
send(config.exception_handler, exception, locale, key, options)
else
config.exception_handler.call(exception, locale, key, options)
end
end
# Deprecated. Will raise a warning in future versions and then finally be
# removed. Use I18n.normalize_keys instead.
def normalize_translation_keys(locale, key, scope, separator = nil)
normalize_keys(locale, key, scope, separator)
end
def normalize_key(key, separator)
normalized_key_cache[separator][key] ||=
case key
when Array
key.map { |k| normalize_key(k, separator) }.flatten
else
keys = key.to_s.split(separator)
keys.delete('')
keys.map!{ |k| k.to_sym }
keys
end
end
def normalized_key_cache
@normalized_key_cache ||= Hash.new { |h,k| h[k] = {} }
end
end
end

View File

@@ -1,20 +0,0 @@
module I18n
module Backend
autoload :ActiveRecord, 'i18n/backend/active_record'
autoload :Base, 'i18n/backend/base'
autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
autoload :Cache, 'i18n/backend/cache'
autoload :Cascade, 'i18n/backend/cascade'
autoload :Chain, 'i18n/backend/chain'
autoload :Cldr, 'i18n/backend/cldr'
autoload :Fallbacks, 'i18n/backend/fallbacks'
autoload :Flatten, 'i18n/backend/flatten'
autoload :Gettext, 'i18n/backend/gettext'
autoload :KeyValue, 'i18n/backend/key_value'
autoload :Memoize, 'i18n/backend/memoize'
autoload :Metadata, 'i18n/backend/metadata'
autoload :Pluralization, 'i18n/backend/pluralization'
autoload :Simple, 'i18n/backend/simple'
autoload :Transliterator, 'i18n/backend/transliterator'
end
end

View File

@@ -1,61 +0,0 @@
require 'i18n/backend/base'
require 'i18n/backend/active_record/translation'
module I18n
module Backend
class ActiveRecord
autoload :Missing, 'i18n/backend/active_record/missing'
autoload :StoreProcs, 'i18n/backend/active_record/store_procs'
autoload :Translation, 'i18n/backend/active_record/translation'
module Implementation
include Base, Flatten
def available_locales
begin
Translation.available_locales
rescue ::ActiveRecord::StatementInvalid
[]
end
end
def store_translations(locale, data, options = {})
escape = options.fetch(:escape, true)
flatten_translations(locale, data, escape, false).each do |key, value|
Translation.locale(locale).lookup(expand_keys(key)).delete_all
Translation.create(:locale => locale.to_s, :key => key.to_s, :value => value)
end
end
protected
def lookup(locale, key, scope = [], options = {})
key = normalize_flat_keys(locale, key, scope, options[:separator])
result = Translation.locale(locale).lookup(key).all
if result.empty?
nil
elsif result.first.key == key
result.first.value
else
chop_range = (key.size + FLATTEN_SEPARATOR.size)..-1
result = result.inject({}) do |hash, r|
hash[r.key.slice(chop_range)] = r.value
hash
end
result.deep_symbolize_keys
end
end
# For a key :'foo.bar.baz' return ['foo', 'foo.bar', 'foo.bar.baz']
def expand_keys(key)
key.to_s.split(FLATTEN_SEPARATOR).inject([]) do |keys, key|
keys << [keys.last, key].compact.join(FLATTEN_SEPARATOR)
end
end
end
include Implementation
end
end
end

View File

@@ -1,65 +0,0 @@
# This extension stores translation stub records for missing translations to
# the database.
#
# This is useful if you have a web based translation tool. It will populate
# the database with untranslated keys as the application is being used. A
# translator can then go through these and add missing translations.
#
# Example usage:
#
# I18n::Backend::Chain.send(:include, I18n::Backend::ActiveRecord::Missing)
# I18n.backend = I18nChainBackend.new(I18n::Backend::ActiveRecord.new, I18n::Backend::Simple.new)
#
# Stub records for pluralizations will also be created for each key defined
# in i18n.plural.keys.
#
# For example:
#
# # en.yml
# en:
# i18n:
# plural:
# keys: [:zero, :one, :other]
#
# # pl.yml
# pl:
# i18n:
# plural:
# keys: [:zero, :one, :few, :other]
#
# It will also persist interpolation keys in Translation#interpolations so
# translators will be able to review and use them.
module I18n
module Backend
class ActiveRecord
module Missing
def store_default_translations(locale, key, options = {})
count, scope, default, separator = options.values_at(:count, *Base::RESERVED_KEYS)
separator ||= I18n.default_separator
keys = I18n.normalize_keys(locale, key, scope, separator)[1..-1]
key = keys.join(separator || I18n.default_separator)
unless ActiveRecord::Translation.locale(locale).lookup(key).exists?
interpolations = options.reject { |name, value| Base::RESERVED_KEYS.include?(name) }.keys
keys = count ? I18n.t('i18n.plural.keys', :locale => locale).map { |k| [key, k].join(separator) } : [key]
keys.each { |key| store_default_translation(locale, key, interpolations) }
end
end
def store_default_translation(locale, key, interpolations)
translation = ActiveRecord::Translation.new :locale => locale.to_s, :key => key
translation.interpolations = interpolations
translation.save
end
def translate(locale, key, options = {})
super
rescue I18n::MissingTranslationData => e
self.store_default_translations(locale, key, options)
raise e
end
end
end
end
end

View File

@@ -1,38 +0,0 @@
# This module is intended to be mixed into the ActiveRecord backend to allow
# storing Ruby Procs as translation values in the database.
#
# I18n.backend = I18n::Backend::ActiveRecord.new
# I18n::Backend::ActiveRecord::Translation.send(:include, I18n::Backend::ActiveRecord::StoreProcs)
#
# The StoreProcs module requires the ParseTree and ruby2ruby gems and therefor
# was extracted from the original backend.
#
# ParseTree is not compatible with Ruby 1.9.
begin
require 'ruby2ruby'
require 'parse_tree'
require 'parse_tree_extensions'
rescue LoadError => e
puts "can't use StoreProcs because: #{e.message}"
end
module I18n
module Backend
class ActiveRecord
module StoreProcs
def value=(v)
case v
when Proc
write_attribute(:value, v.to_ruby)
write_attribute(:is_proc, true)
else
write_attribute(:value, v)
end
end
Translation.send(:include, self) if method(:to_s).respond_to?(:to_ruby)
end
end
end
end

View File

@@ -1,93 +0,0 @@
require 'active_record'
module I18n
module Backend
# ActiveRecord model used to store actual translations to the database.
#
# This model expects a table like the following to be already set up in
# your the database:
#
# create_table :translations do |t|
# t.string :locale
# t.string :key
# t.text :value
# t.text :interpolations
# t.boolean :is_proc, :default => false
# end
#
# This model supports to named scopes :locale and :lookup. The :locale
# scope simply adds a condition for a given locale:
#
# I18n::Backend::ActiveRecord::Translation.locale(:en).all
# # => all translation records that belong to the :en locale
#
# The :lookup scope adds a condition for looking up all translations
# that either start with the given keys (joined by an optionally given
# separator or I18n.default_separator) or that exactly have this key.
#
# # with translations present for :"foo.bar" and :"foo.baz"
# I18n::Backend::ActiveRecord::Translation.lookup(:foo)
# # => an array with both translation records :"foo.bar" and :"foo.baz"
#
# I18n::Backend::ActiveRecord::Translation.lookup([:foo, :bar])
# I18n::Backend::ActiveRecord::Translation.lookup(:"foo.bar")
# # => an array with the translation record :"foo.bar"
#
# When the StoreProcs module was mixed into this model then Procs will
# be stored to the database as Ruby code and evaluated when :value is
# called.
#
# Translation = I18n::Backend::ActiveRecord::Translation
# Translation.create \
# :locale => 'en'
# :key => 'foo'
# :value => lambda { |key, options| 'FOO' }
# Translation.find_by_locale_and_key('en', 'foo').value
# # => 'FOO'
class ActiveRecord
class Translation < ::ActiveRecord::Base
set_table_name 'translations'
attr_protected :is_proc, :interpolations
serialize :value
serialize :interpolations, Array
scope_method = ::ActiveRecord::VERSION::MAJOR == 2 ? :named_scope : :scope
send scope_method, :locale, lambda { |locale|
{ :conditions => { :locale => locale.to_s } }
}
send scope_method, :lookup, lambda { |keys, *separator|
column_name = connection.quote_column_name('key')
keys = Array(keys).map! { |key| key.to_s }
unless separator.empty?
warn "[DEPRECATION] Giving a separator to Translation.lookup is deprecated. " <<
"You can change the internal separator by overwriting FLATTEN_SEPARATOR."
end
namespace = "#{keys.last}#{I18n::Backend::Flatten::FLATTEN_SEPARATOR}%"
{ :conditions => ["#{column_name} IN (?) OR #{column_name} LIKE ?", keys, namespace] }
}
def self.available_locales
Translation.find(:all, :select => 'DISTINCT locale').map { |t| t.locale.to_sym }
end
def interpolates?(key)
self.interpolations.include?(key) if self.interpolations
end
def value
if is_proc
Kernel.eval(read_attribute(:value))
else
value = read_attribute(:value)
value == 'f' ? false : value
end
end
end
end
end
end

View File

@@ -1,237 +0,0 @@
# encoding: utf-8
require 'yaml'
require 'i18n/core_ext/hash'
module I18n
module Backend
module Base
include I18n::Backend::Transliterator
RESERVED_KEYS = [:scope, :default, :separator, :resolve]
RESERVED_KEYS_PATTERN = /%\{(#{RESERVED_KEYS.join("|")})\}/
DEPRECATED_INTERPOLATION_SYNTAX_PATTERN = /(\\)?\{\{([^\}]+)\}\}/
INTERPOLATION_SYNTAX_PATTERN = /%\{([^\}]+)\}/
# Accepts a list of paths to translation files. Loads translations from
# plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
# for details.
def load_translations(*filenames)
filenames = I18n.load_path.flatten if filenames.empty?
filenames.each { |filename| load_file(filename) }
end
# This method receives a locale, a data hash and options for storing translations.
# Should be implemented
def store_translations(locale, data, options = {})
raise NotImplementedError
end
def translate(locale, key, options = {})
raise InvalidLocale.new(locale) unless locale
return key.map { |k| translate(locale, k, options) } if key.is_a?(Array)
entry = key && lookup(locale, key, options[:scope], options)
if options.empty?
entry = resolve(locale, key, entry, options)
else
count, default = options.values_at(:count, :default)
values = options.except(*RESERVED_KEYS)
entry = entry.nil? && default ?
default(locale, key, default, options) : resolve(locale, key, entry, options)
end
raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil?
entry = entry.dup if entry.is_a?(String)
entry = pluralize(locale, entry, count) if count
entry = interpolate(locale, entry, values) if values
entry
end
# Acts the same as +strftime+, but uses a localized version of the
# format string. Takes a key from the date/time formats translations as
# a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
def localize(locale, object, format = :default, options = {})
raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
if Symbol === format
key = format
type = object.respond_to?(:sec) ? 'time' : 'date'
format = I18n.t(:"#{type}.formats.#{key}", options.merge(:raise => true, :object => object, :locale => locale))
end
# format = resolve(locale, object, format, options)
format = format.to_s.gsub(/%[aAbBp]/) do |match|
case match
when '%a' then I18n.t(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
when '%A' then I18n.t(:"date.day_names", :locale => locale, :format => format)[object.wday]
when '%b' then I18n.t(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
when '%B' then I18n.t(:"date.month_names", :locale => locale, :format => format)[object.mon]
when '%p' then I18n.t(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format) if object.respond_to? :hour
end
end
object.strftime(format)
end
# Returns an array of locales for which translations are available
# ignoring the reserved translation meta data key :i18n.
def available_locales
raise NotImplementedError
end
def reload!
@skip_syntax_deprecation = false
end
protected
# The method which actually looks up for the translation in the store.
def lookup(locale, key, scope = [], options = {})
raise NotImplementedError
end
# Evaluates defaults.
# If given subject is an Array, it walks the array and returns the
# first translation that can be resolved. Otherwise it tries to resolve
# the translation directly.
def default(locale, object, subject, options = {})
options = options.dup.reject { |key, value| key == :default }
case subject
when Array
subject.each do |item|
result = resolve(locale, object, item, options) and return result
end and nil
else
resolve(locale, object, subject, options)
end
end
# Resolves a translation.
# If the given subject is a Symbol, it will be translated with the
# given options. If it is a Proc then it will be evaluated. All other
# subjects will be returned directly.
def resolve(locale, object, subject, options = nil)
return subject if options[:resolve] == false
case subject
when Symbol
I18n.translate(subject, (options || {}).merge(:locale => locale, :raise => true))
when Proc
date_or_time = options.delete(:object) || object
resolve(locale, object, subject.call(date_or_time, options), options = {})
else
subject
end
rescue MissingTranslationData
nil
end
# Picks a translation from an array according to English pluralization
# rules. It will pick the first translation if count is not equal to 1
# and the second translation if it is equal to 1. Other backends can
# implement more flexible or complex pluralization rules.
def pluralize(locale, entry, count)
return entry unless entry.is_a?(Hash) && count
key = :zero if count == 0 && entry.has_key?(:zero)
key ||= count == 1 ? :one : :other
raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
entry[key]
end
# Interpolates values into a given string.
#
# interpolate "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X'
# # => "file test.txt opened by %{user}"
#
# Note that you have to double escape the <tt>\\</tt> when you want to escape
# the <tt>{{...}}</tt> key in a string (once for the string and once for the
# interpolation).
def interpolate(locale, string, values = {})
return string unless string.is_a?(::String) && !values.empty?
original_values = values.dup
preserve_encoding(string) do
string = string.gsub(DEPRECATED_INTERPOLATION_SYNTAX_PATTERN) do
escaped, key = $1, $2.to_sym
if escaped
"{{#{key}}}"
else
warn_syntax_deprecation!
"%{#{key}}"
end
end
keys = string.scan(INTERPOLATION_SYNTAX_PATTERN).flatten
return string if keys.empty?
values.each do |key, value|
if keys.include?(key.to_s)
value = value.call(values) if interpolate_lambda?(value, string, key)
value = value.to_s unless value.is_a?(::String)
values[key] = value
else
values.delete(key)
end
end
string % values
end
rescue KeyError => e
if string =~ RESERVED_KEYS_PATTERN
raise ReservedInterpolationKey.new($1.to_sym, string)
else
raise MissingInterpolationArgument.new(original_values, string)
end
end
def preserve_encoding(string)
if string.respond_to?(:encoding)
encoding = string.encoding
result = yield
result.force_encoding(encoding) if result.respond_to?(:force_encoding)
result
else
yield
end
end
# returns true when the given value responds to :call and the key is
# an interpolation placeholder in the given string
def interpolate_lambda?(object, string, key)
object.respond_to?(:call) && string =~ /%\{#{key}\}|%\<#{key}>.*?\d*\.?\d*[bBdiouxXeEfgGcps]\}/
end
# Loads a single translations file by delegating to #load_rb or
# #load_yml depending on the file extension and directly merges the
# data to the existing translations. Raises I18n::UnknownFileType
# for all other file extensions.
def load_file(filename)
type = File.extname(filename).tr('.', '').downcase
raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
data = send(:"load_#{type}", filename) # TODO raise a meaningful exception if this does not yield a Hash
data.each { |locale, d| store_translations(locale, d) }
end
# Loads a plain Ruby translations file. eval'ing the file must yield
# a Hash containing translation data with locales as toplevel keys.
def load_rb(filename)
eval(IO.read(filename), binding, filename)
end
# Loads a YAML translations file. The data must have locales as
# toplevel keys.
def load_yml(filename)
YAML::load(IO.read(filename))
end
def warn_syntax_deprecation! #:nodoc:
return if @skip_syntax_deprecation
warn "The {{key}} interpolation syntax in I18n messages is deprecated. Please use %{key} instead.\n#{caller.join("\n")}"
@skip_syntax_deprecation = true
end
end
end
end

View File

@@ -1,77 +0,0 @@
# encoding: utf-8
# This module allows you to easily cache all responses from the backend - thus
# speeding up the I18n aspects of your application quite a bit.
#
# To enable caching you can simply include the Cache module to the Simple
# backend - or whatever other backend you are using:
#
# I18n::Backend::Simple.send(:include, I18n::Backend::Cache)
#
# You will also need to set a cache store implementation that you want to use:
#
# I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
#
# You can use any cache implementation you want that provides the same API as
# ActiveSupport::Cache (only the methods #fetch and #write are being used).
#
# The cache_key implementation assumes that you only pass values to
# I18n.translate that return a valid key from #hash (see
# http://www.ruby-doc.org/core/classes/Object.html#M000337).
module I18n
class << self
@@cache_store = nil
@@cache_namespace = nil
def cache_store
@@cache_store
end
def cache_store=(store)
@@cache_store = store
end
def cache_namespace
@@cache_namespace
end
def cache_namespace=(namespace)
@@cache_namespace = namespace
end
def perform_caching?
!cache_store.nil?
end
end
module Backend
# TODO Should the cache be cleared if new translations are stored?
module Cache
def translate(*args)
I18n.perform_caching? ? fetch(*args) { super } : super
end
protected
def fetch(*args, &block)
result = I18n.cache_store.fetch(cache_key(*args), &block)
raise result if result.is_a?(Exception)
result = result.dup if result.frozen? rescue result
result
rescue MissingTranslationData => exception
I18n.cache_store.write(cache_key(*args), exception)
raise exception
end
def cache_key(*args)
# This assumes that only simple, native Ruby values are passed to I18n.translate.
# Also, in Ruby < 1.8.7 {}.hash != {}.hash
# (see http://paulbarry.com/articles/2009/09/14/why-rails-3-will-require-ruby-1-8-7)
# If args.inspect does not work for you for some reason, patches are very welcome :)
hash = RUBY_VERSION >= "1.8.7" ? args.hash : args.inspect
keys = ['i18n', I18n.cache_namespace, hash]
keys.compact.join('-')
end
end
end
end

View File

@@ -1,57 +0,0 @@
# encoding: utf-8
# EXPERIMENTAL
#
# The Cascade module adds the ability to do cascading lookups to backends that
# are compatible to the Simple backend.
#
# By cascading lookups we mean that for any key that can not be found the
# Cascade module strips one segment off the scope part of the key and then
# tries to look up the key in that scope.
#
# E.g. when a lookup for the key :"foo.bar.baz" does not yield a result then
# the segment :bar will be stripped off the scope part :"foo.bar" and the new
# scope :foo will be used to look up the key :baz. If that does not succeed
# then the remaining scope segment :foo will be omitted, too, and again the
# key :baz will be looked up (now with no scope).
#
# To enable a cascading lookup one passes the :cascade option:
#
# I18n.t(:'foo.bar.baz', :cascade => true)
#
# This will return the first translation found for :"foo.bar.baz", :"foo.baz"
# or :baz in this order.
#
# The cascading lookup takes precedence over resolving any given defaults.
# I.e. defaults will kick in after the cascading lookups haven't succeeded.
#
# This behavior is useful for libraries like ActiveRecord validations where
# the library wants to give users a bunch of more or less fine-grained options
# of scopes for a particular key.
#
# Thanks to Clemens Kofler for the initial idea and implementation! See
# http://github.com/clemens/i18n-cascading-backend
module I18n
module Backend
module Cascade
def lookup(locale, key, scope = [], options = {})
return super unless cascade = options[:cascade]
separator = options[:separator] || I18n.default_separator
skip_root = cascade.has_key?(:skip_root) ? cascade[:skip_root] : true
step = cascade[:step]
keys = I18n.normalize_keys(nil, key, nil, separator)
offset = options[:cascade][:offset] || keys.length
scope = I18n.normalize_keys(nil, nil, scope, separator) + keys
key = scope.slice!(-offset, offset).join(separator)
begin
result = super
return result unless result.nil?
end while !scope.empty? && scope.slice!(-step, step) && (!scope.empty? || !skip_root)
end
end
end
end

View File

@@ -1,77 +0,0 @@
# encoding: utf-8
module I18n
module Backend
# Backend that chains multiple other backends and checks each of them when
# a translation needs to be looked up. This is useful when you want to use
# standard translations with a Simple backend but store custom application
# translations in a database or other backends.
#
# To use the Chain backend instantiate it and set it to the I18n module.
# You can add chained backends through the initializer or backends
# accessor:
#
# # preserves the existing Simple backend set to I18n.backend
# I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
#
# The implementation assumes that all backends added to the Chain implement
# a lookup method with the same API as Simple backend does.
class Chain
include Base
attr_accessor :backends
def initialize(*backends)
self.backends = backends
end
def reload!
backends.each { |backend| backend.reload! }
end
def store_translations(locale, data, options = {})
backends.first.store_translations(locale, data, options = {})
end
def available_locales
backends.map { |backend| backend.available_locales }.flatten.uniq
end
def translate(locale, key, options = {})
return key.map { |k| translate(locale, k, options) } if key.is_a?(Array)
default = options.delete(:default)
namespace = {}
backends.each do |backend|
begin
options.update(:default => default) if default and backend == backends.last
translation = backend.translate(locale, key, options)
if namespace_lookup?(translation, options)
namespace.update(translation)
elsif !translation.nil?
return translation
end
rescue MissingTranslationData
end
end
return namespace unless namespace.empty?
raise(I18n::MissingTranslationData.new(locale, key, options))
end
def localize(locale, object, format = :default, options = {})
backends.each do |backend|
begin
result = backend.localize(locale, object, format, options) and return result
rescue MissingTranslationData
end
end
raise(I18n::MissingTranslationData.new(locale, format, options))
end
protected
def namespace_lookup?(result, options)
result.is_a?(Hash) and not options.has_key?(:count)
end
end
end
end

View File

@@ -1,100 +0,0 @@
# encoding: utf-8
require 'cldr'
module I18n
module Backend
module Cldr
include ::Cldr::Format
def localize(locale, object, format = :default, options = {})
options[:as] ||= detect_type(object, options)
send(:"format_#{options[:as]}", locale, object, format, options)
end
def format_decimal(locale, object, format = :default, options = {})
formatter(locale, :decimal, format).apply(object, options)
end
def format_integer(locale, object, format = :default, options = {})
format_object(number, options.merge(:precision => 0))
end
def format_currency(locale, object, format = :default, options = {})
options.merge!(:currency => lookup_currency(locale, options[:currency], object)) if options[:currency].is_a?(Symbol)
formatter(locale, :currency, format).apply(object, options)
end
def format_percent(locale, object, format = :default, options = {})
formatter(locale, :percent, format).apply(object, options)
end
def format_date(locale, object, format = :default, options = {})
formatter(locale, :date, format).apply(object, options)
end
def format_time(locale, object, format = :default, options = {})
formatter(locale, :time, format).apply(object, options)
end
def format_datetime(locale, object, format = :default, options = {})
key = :"calendars.gregorian.formats.datetime.#{format}.pattern"
date = I18n.l(object, :format => options[:date_format] || format, :locale => locale, :as => :date)
time = I18n.l(object, :format => options[:time_format] || format, :locale => locale, :as => :time)
I18n.t(key, :date => date, :time => time, :locale => locale, :raise => true)
end
protected
def detect_type(object, options)
options.has_key?(:currency) ? :currency : case object
when ::Numeric
:decimal
when ::Date, ::DateTime, ::Time
object.class.name.downcase.to_sym
else
raise_unspecified_format_type!
end
end
def formatter(locale, type, format)
(@formatters ||= {})[:"#{locale}.#{type}.#{format}"] ||= begin
format = lookup_format(locale, type, format)
data = lookup_format_data(locale, type)
::Cldr::Format.const_get(type.to_s.camelize).new(format, data)
end
end
def lookup_format(locale, type, format)
key = case type
when :date, :time, :datetime
:"calendars.gregorian.formats.#{type}.#{format}.pattern"
else
:"numbers.formats.#{type}.patterns.#{format || :default}"
end
I18n.t(key, :locale => locale, :raise => true)
end
def lookup_format_data(locale, type)
key = case type
when :date, :time, :datetime
:'calendars.gregorian'
else
:'numbers.symbols'
end
I18n.t(key, :locale => locale, :raise => true)
end
def lookup_currency(locale, currency, count)
I18n.t(:"currencies.#{currency}", :locale => locale, :count => count)
end
def raise_unspecified_format_type!
raise ArgumentError.new("You have to specify a format type, e.g. :as => :number.")
end
def raise_unspecified_currency!
raise ArgumentError.new("You have to specify a currency, e.g. :currency => 'EUR'.")
end
end
end
end

View File

@@ -1,69 +0,0 @@
# encoding: utf-8
# I18n locale fallbacks are useful when you want your application to use
# translations from other locales when translations for the current locale are
# missing. E.g. you might want to use :en translations when translations in
# your applications main locale :de are missing.
#
# To enable locale fallbacks you can simply include the Fallbacks module to
# the Simple backend - or whatever other backend you are using:
#
# I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
module I18n
@@fallbacks = nil
class << self
# Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+.
def fallbacks
@@fallbacks ||= I18n::Locale::Fallbacks.new
end
# Sets the current fallbacks implementation. Use this to set a different fallbacks implementation.
def fallbacks=(fallbacks)
@@fallbacks = fallbacks
end
end
module Backend
module Fallbacks
# Overwrites the Base backend translate method so that it will try each
# locale given by I18n.fallbacks for the given locale. E.g. for the
# locale :"de-DE" it might try the locales :"de-DE", :de and :en
# (depends on the fallbacks implementation) until it finds a result with
# the given options. If it does not find any result for any of the
# locales it will then raise a MissingTranslationData exception as
# usual.
#
# The default option takes precedence over fallback locales
# only when it's not a String. When default contains String it
# is evaluated after fallback locales.
def translate(locale, key, options = {})
default = extract_string_default!(options) if options[:default]
I18n.fallbacks[locale].each do |fallback|
begin
result = super(fallback, key, options)
return result unless result.nil?
rescue I18n::MissingTranslationData
end
end
return super(locale, nil, options.merge(:default => default)) if default
raise(I18n::MissingTranslationData.new(locale, key, options))
end
def extract_string_default!(options)
defaults = Array(options[:default])
if index = find_first_string_default(defaults)
options[:default] = defaults[0, index]
defaults[index]
end
end
def find_first_string_default(defaults)
defaults.each_index { |ix| return ix if String === defaults[ix] }
nil
end
end
end
end

View File

@@ -1,113 +0,0 @@
module I18n
module Backend
# This module contains several helpers to assist flattening translations.
# You may want to flatten translations for:
#
# 1) speed up lookups, as in the Memoize backend;
# 2) In case you want to store translations in a data store, as in ActiveRecord backend;
#
# You can check both backends above for some examples.
# This module also keeps all links in a hash so they can be properly resolved when flattened.
module Flatten
SEPARATOR_ESCAPE_CHAR = "\001"
FLATTEN_SEPARATOR = "."
# normalize_keys the flatten way. This method is significantly faster
# and creates way less objects than the one at I18n.normalize_keys.
# It also handles escaping the translation keys.
def self.normalize_flat_keys(locale, key, scope, separator)
keys = [scope, key].flatten.compact
separator ||= I18n.default_separator
if separator != FLATTEN_SEPARATOR
keys.map! do |k|
k.to_s.tr("#{FLATTEN_SEPARATOR}#{separator}",
"#{SEPARATOR_ESCAPE_CHAR}#{FLATTEN_SEPARATOR}")
end
end
keys.join(".")
end
# Receives a string and escape the default separator.
def self.escape_default_separator(key) #:nodoc:
key.to_s.tr(FLATTEN_SEPARATOR, SEPARATOR_ESCAPE_CHAR)
end
# Shortcut to I18n::Backend::Flatten.normalize_flat_keys
# and then resolve_links.
def normalize_flat_keys(locale, key, scope, separator)
key = I18n::Backend::Flatten.normalize_flat_keys(locale, key, scope, separator)
resolve_link(locale, key)
end
# Store flattened links.
def links
@links ||= Hash.new { |h,k| h[k] = {} }
end
# Flatten keys for nested Hashes by chaining up keys:
#
# >> { "a" => { "b" => { "c" => "d", "e" => "f" }, "g" => "h" }, "i" => "j"}.wind
# => { "a.b.c" => "d", "a.b.e" => "f", "a.g" => "h", "i" => "j" }
#
def flatten_keys(hash, escape, prev_key=nil, &block)
hash.each_pair do |key, value|
key = escape_default_separator(key) if escape
curr_key = [prev_key, key].compact.join(FLATTEN_SEPARATOR).to_sym
yield curr_key, value
flatten_keys(value, escape, curr_key, &block) if value.is_a?(Hash)
end
end
# Receives a hash of translations (where the key is a locale and
# the value is another hash) and return a hash with all
# translations flattened.
#
# Nested hashes are included in the flattened hash just if subtree
# is true and Symbols are automatically stored as links.
def flatten_translations(locale, data, escape, subtree)
hash = {}
flatten_keys(data, escape) do |key, value|
if value.is_a?(Hash)
hash[key] = value if subtree
else
store_link(locale, key, value) if value.is_a?(Symbol)
hash[key] = value
end
end
hash
end
protected
def store_link(locale, key, link)
links[locale.to_sym][key.to_s] = link.to_s
end
def resolve_link(locale, key)
key, locale = key.to_s, locale.to_sym
links = self.links[locale]
if links.key?(key)
links[key]
elsif link = find_link(locale, key)
store_link(locale, key, key.gsub(*link))
else
key
end
end
def find_link(locale, key) #:nodoc:
links[locale].each do |from, to|
return [from, to] if key[0, from.length] == from
end && nil
end
def escape_default_separator(key) #:nodoc:
I18n::Backend::Flatten.escape_default_separator(key)
end
end
end
end

View File

@@ -1,75 +0,0 @@
# encoding: utf-8
require 'i18n/gettext'
require 'i18n/gettext/po_parser'
# Experimental support for using Gettext po files to store translations.
#
# To use this you can simply include the module to the Simple backend - or
# whatever other backend you are using.
#
# I18n::Backend::Simple.send(:include, I18n::Backend::Gettext)
#
# Now you should be able to include your Gettext translation (*.po) files to
# the I18n.load_path so they're loaded to the backend and you can use them as
# usual:
#
# I18n.load_path += Dir["path/to/locales/*.po"]
#
# Following the Gettext convention this implementation expects that your
# translation files are named by their locales. E.g. the file en.po would
# contain the translations for the English locale.
module I18n
module Backend
module Gettext
class PoData < Hash
def set_comment(msgid_or_sym, comment)
# ignore
end
end
protected
def load_po(filename)
locale = ::File.basename(filename, '.po').to_sym
data = normalize(locale, parse(filename))
{ locale => data }
end
def parse(filename)
GetText::PoParser.new.parse(::File.read(filename), PoData.new)
end
def normalize(locale, data)
data.inject({}) do |result, (key, value)|
unless key.nil? || key.empty?
key, value = normalize_pluralization(locale, key, value) if key.index("\000")
parts = key.split('|').reverse
normalized = parts.inject({}) do |normalized, part|
normalized = { part => normalized.empty? ? value : normalized }
end
# deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
result.merge!(normalized, &merger)
end
result
end
end
def normalize_pluralization(locale, key, value)
# FIXME po_parser includes \000 chars that can not be turned into Symbols
key = key.gsub("\000", I18n::Gettext::PLURAL_SEPARATOR).split(I18n::Gettext::PLURAL_SEPARATOR).first
keys = I18n::Gettext.plural_keys(locale)
values = value.split("\000")
raise "invalid number of plurals: #{values.size}, keys: #{keys.inspect}" if values.size != keys.size
result = {}
values.each_with_index { |value, ix| result[keys[ix]] = value }
[key, result]
end
end
end
end

View File

@@ -1,123 +0,0 @@
# encoding: utf-8
# The InterpolationCompiler module contains optimizations that can tremendously
# speed up the interpolation process on the Simple backend.
#
# It works by defining a pre-compiled method on stored translation Strings that
# already bring all the knowledge about contained interpolation variables etc.
# so that the actual recurring interpolation will be very fast.
#
# To enable pre-compiled interpolations you can simply include the
# InterpolationCompiler module to the Simple backend:
#
# I18n::Backend::Simple.send(:include, I18n::Backend::InterpolationCompiler)
#
# Note that InterpolationCompiler does not yield meaningful results and consequently
# should not be used with Ruby 1.9 (YARV) but improves performance everywhere else
# (jRuby, Rubinius and 1.8.7).
module I18n
module Backend
module InterpolationCompiler
module Compiler
extend self
TOKENIZER = /(%%\{[^\}]+\}|%\{[^\}]+\})/
INTERPOLATION_SYNTAX_PATTERN = /(%)?(%\{([^\}]+)\})/
def compile_if_an_interpolation(string)
if interpolated_str?(string)
string.instance_eval <<-RUBY_EVAL, __FILE__, __LINE__
def i18n_interpolate(v = {})
"#{compiled_interpolation_body(string)}"
end
RUBY_EVAL
end
string
end
def interpolated_str?(str)
str.kind_of?(::String) && str =~ INTERPOLATION_SYNTAX_PATTERN
end
protected
# tokenize("foo %{bar} baz %%{buz}") # => ["foo ", "%{bar}", " baz ", "%%{buz}"]
def tokenize(str)
str.split(TOKENIZER)
end
def compiled_interpolation_body(str)
tokenize(str).map do |token|
(matchdata = token.match(INTERPOLATION_SYNTAX_PATTERN)) ? handle_interpolation_token(token, matchdata) : escape_plain_str(token)
end.join
end
def handle_interpolation_token(interpolation, matchdata)
escaped, pattern, key = matchdata.values_at(1, 2, 3)
escaped ? pattern : compile_interpolation_token(key.to_sym)
end
def compile_interpolation_token(key)
"\#{#{interpolate_or_raise_missing(key)}}"
end
def interpolate_or_raise_missing(key)
escaped_key = escape_key_sym(key)
Base::RESERVED_KEYS.include?(key) ? reserved_key(escaped_key) : interpolate_key(escaped_key)
end
def interpolate_key(key)
[direct_key(key), nil_key(key), missing_key(key)].join('||')
end
def direct_key(key)
"((t = v[#{key}]) && t.respond_to?(:call) ? t.call : t)"
end
def nil_key(key)
"(v.has_key?(#{key}) && '')"
end
def missing_key(key)
"raise(MissingInterpolationArgument.new(#{key}, self))"
end
def reserved_key(key)
"raise(ReservedInterpolationKey.new(#{key}, self))"
end
def escape_plain_str(str)
str.gsub(/"|\\|#/) {|x| "\\#{x}"}
end
def escape_key_sym(key)
# rely on Ruby to do all the hard work :)
key.to_sym.inspect
end
end
def interpolate(locale, string, values)
if string.respond_to?(:i18n_interpolate)
string.i18n_interpolate(values)
elsif values
super
else
string
end
end
def store_translations(locale, data, options = {})
compile_all_strings_in(data)
super
end
protected
def compile_all_strings_in(data)
data.each_value do |value|
Compiler.compile_if_an_interpolation(value)
compile_all_strings_in(value) if value.kind_of?(Hash)
end
end
end
end
end

View File

@@ -1,102 +0,0 @@
# encoding: utf-8
require 'i18n/backend/base'
require 'active_support/json'
module I18n
module Backend
# This is a basic backend for key value stores. It receives on
# initialization the store, which should respond to three methods:
#
# * store#[](key) - Used to get a value
# * store#[]=(key, value) - Used to set a value
# * store#keys - Used to get all keys
#
# Since these stores only supports string, all values are converted
# to JSON before being stored, allowing it to also store booleans,
# hashes and arrays. However, this store does not support Procs.
#
# As the ActiveRecord backend, Symbols are just supported when loading
# translations from the filesystem or through explicit store translations.
#
# Also, avoid calling I18n.available_locales since it's a somehow
# expensive operation in most stores.
#
# == Example
#
# To setup I18n to use TokyoCabinet in memory is quite straightforward:
#
# require 'rufus/tokyo/cabinet' # gem install rufus-tokyo
# I18n.backend = I18n::Backend::KeyValue.new(Rufus::Tokyo::Cabinet.new('*'))
#
# == Performance
#
# You may make this backend even faster by including the Memoize module.
# However, notice that you should properly clear the cache if you change
# values directly in the key-store.
#
# == Subtrees
#
# In most backends, you are allowed to retrieve part of a translation tree:
#
# I18n.backend.store_translations :en, :foo => { :bar => :baz }
# I18n.t "foo" #=> { :bar => :baz }
#
# This backend supports this feature by default, but it slows down the storage
# of new data considerably and makes hard to delete entries. That said, you are
# allowed to disable the storage of subtrees on initialization:
#
# I18n::Backend::KeyValue.new(@store, false)
#
# This is useful if you are using a KeyValue backend chained to a Simple backend.
class KeyValue
module Implementation
attr_accessor :store
include Base, Flatten
def initialize(store, subtrees=true)
@store, @subtrees = store, subtrees
end
def store_translations(locale, data, options = {})
escape = options.fetch(:escape, true)
flatten_translations(locale, data, escape, @subtrees).each do |key, value|
key = "#{locale}.#{key}"
case value
when Hash
if @subtrees && (old_value = @store[key])
old_value = ActiveSupport::JSON.decode(old_value)
value = old_value.deep_symbolize_keys.deep_merge!(value) if old_value.is_a?(Hash)
end
when Proc
raise "Key-value stores cannot handle procs"
end
@store[key] = ActiveSupport::JSON.encode(value) unless value.is_a?(Symbol)
end
end
def available_locales
locales = @store.keys.map { |k| k =~ /\./; $` }
locales.uniq!
locales.compact!
locales.map! { |k| k.to_sym }
locales
end
protected
def lookup(locale, key, scope = [], options = {})
key = normalize_flat_keys(locale, key, scope, options[:separator])
value = @store["#{locale}.#{key}"]
value = ActiveSupport::JSON.decode(value) if value
value.is_a?(Hash) ? value.deep_symbolize_keys : value
end
end
include Implementation
end
end
end

View File

@@ -1,48 +0,0 @@
# encoding: utf-8
#
# Memoize module simply memoizes the values returned by lookup using
# a flat hash and can tremendously speed up the lookup process in a backend.
#
# To enable it you can simply include the Memoize module to your backend:
#
# I18n::Backend::Simple.send(:include, I18n::Backend::Memoize)
#
# Notice that it's the responsibility of the backend to define whenever the
# cache should be cleaned.
module I18n
module Backend
module Memoize
def available_locales
@memoized_locales ||= super
end
def store_translations(locale, data, options = {})
reset_memoizations!(locale)
super
end
def reload!
reset_memoizations!
super
end
protected
def lookup(locale, key, scope = nil, options = {})
flat_key = I18n::Backend::Flatten.normalize_flat_keys(locale,
key, scope, options[:separator]).to_sym
flat_hash = memoized_lookup[locale.to_sym]
flat_hash.key?(flat_key) ? flat_hash[flat_key] : (flat_hash[flat_key] = super)
end
def memoized_lookup
@memoized_lookup ||= Hash.new { |h, k| h[k] = {} }
end
def reset_memoizations!(locale=nil)
@memoized_locales = nil
(locale ? memoized_lookup[locale.to_sym] : memoized_lookup).clear
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More