mirror of
https://github.com/github/rails.git
synced 2026-01-12 08:08:31 -05:00
Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0e554d231 | ||
|
|
d38b7664cc | ||
|
|
e4cd9caf02 | ||
|
|
89e4514704 | ||
|
|
0a0d975f51 | ||
|
|
62daf4cb6f | ||
|
|
24711e1e29 | ||
|
|
cf8f36930c | ||
|
|
d622643e47 | ||
|
|
3f0241a613 | ||
|
|
38a7432590 | ||
|
|
1220d3c3ed | ||
|
|
3d72818356 | ||
|
|
221477dc21 | ||
|
|
975155c110 | ||
|
|
2931987892 | ||
|
|
e3290b98dd | ||
|
|
20088080a5 | ||
|
|
24e348489d | ||
|
|
ba4f4f8a01 | ||
|
|
ccf254b6cb | ||
|
|
3766b1b377 | ||
|
|
d3f87776a3 | ||
|
|
18c7c1f753 | ||
|
|
f63b0340ff | ||
|
|
7224ee1419 | ||
|
|
0c52ae6df3 | ||
|
|
f8b7cd2df7 | ||
|
|
c73ba86136 | ||
|
|
98fa5dd465 | ||
|
|
fa41bedf6b | ||
|
|
0a8282c557 | ||
|
|
d4a4facfcc | ||
|
|
dd4146854a | ||
|
|
cedf026a14 | ||
|
|
7ac3b0fa4f | ||
|
|
31cd7ea26d | ||
|
|
df387ab385 | ||
|
|
0118959601 | ||
|
|
83448c7de5 | ||
|
|
8f99d00868 | ||
|
|
987b61bd1d | ||
|
|
f05e54a9f3 | ||
|
|
b9918117bb | ||
|
|
42f85d118d | ||
|
|
acb182d094 | ||
|
|
6e0fcb788d | ||
|
|
fed4fafa8a | ||
|
|
f699184047 | ||
|
|
55d6a9f2df | ||
|
|
e5bebc01a8 | ||
|
|
a019f07a39 | ||
|
|
d13866d75d | ||
|
|
dfa2f469a4 | ||
|
|
bf0d43bb77 | ||
|
|
72cebbcb59 | ||
|
|
379dd9071c | ||
|
|
a743f17dbd | ||
|
|
25b896611d | ||
|
|
b988837359 | ||
|
|
890aff3b9d | ||
|
|
c0124ba8f3 | ||
|
|
455cd8c060 | ||
|
|
5d322ad957 | ||
|
|
3b6b4578c4 | ||
|
|
981016be60 | ||
|
|
3c1e01068b | ||
|
|
e42c679e43 | ||
|
|
5c4dfa63f7 | ||
|
|
c394fd82fa | ||
|
|
49933594c1 | ||
|
|
94fae25703 | ||
|
|
05cb9e6854 | ||
|
|
1a5734e0b5 | ||
|
|
24e5712294 | ||
|
|
8f6bafc333 | ||
|
|
c717a84b5d | ||
|
|
d537304b20 | ||
|
|
ca90ecf2cb | ||
|
|
4bb1d3ef20 | ||
|
|
3b7754c950 | ||
|
|
75638c576b | ||
|
|
76884dd7f7 | ||
|
|
29a72262aa | ||
|
|
76c5bf4f4b | ||
|
|
416b7171b8 | ||
|
|
e82a3ba2a0 | ||
|
|
8837faac73 | ||
|
|
20b12c3b42 | ||
|
|
0cf06787af | ||
|
|
5efad05b11 |
@@ -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
1
RAILS_VERSION
Normal file
@@ -0,0 +1 @@
|
||||
2.3.14.github39
|
||||
7
Rakefile
7
Rakefile
@@ -3,7 +3,7 @@ require 'rdoc/task'
|
||||
|
||||
env = %(PKG_BUILD="#{ENV['PKG_BUILD']}") if ENV['PKG_BUILD']
|
||||
|
||||
PROJECTS = %w(activesupport railties actionpack actionmailer activeresource activerecord)
|
||||
PROJECTS = %w(activesupport railties actionpack actionmailer activerecord)
|
||||
|
||||
Dir["#{File.dirname(__FILE__)}/*/lib/*/version.rb"].each do |version_path|
|
||||
require version_path
|
||||
@@ -48,11 +48,6 @@ RDoc::Task.new do |rdoc|
|
||||
rdoc.rdoc_files.include('activerecord/lib/active_record/**/*.rb')
|
||||
rdoc.rdoc_files.exclude('activerecord/lib/active_record/vendor/*')
|
||||
|
||||
rdoc.rdoc_files.include('activeresource/README')
|
||||
rdoc.rdoc_files.include('activeresource/CHANGELOG')
|
||||
rdoc.rdoc_files.include('activeresource/lib/active_resource.rb')
|
||||
rdoc.rdoc_files.include('activeresource/lib/active_resource/*')
|
||||
|
||||
rdoc.rdoc_files.include('actionpack/README')
|
||||
rdoc.rdoc_files.include('actionpack/CHANGELOG')
|
||||
rdoc.rdoc_files.include('actionpack/lib/action_controller/**/*.rb')
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).chomp
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'actionmailer'
|
||||
s.version = '2.3.18'
|
||||
s.version = version
|
||||
s.summary = 'Service layer for easy email delivery and testing.'
|
||||
s.description = 'Makes it trivial to test and deliver emails sent from a single service layer.'
|
||||
|
||||
@@ -10,5 +12,5 @@ Gem::Specification.new do |s|
|
||||
|
||||
s.require_path = 'lib'
|
||||
|
||||
s.add_dependency 'actionpack', '= 2.3.18'
|
||||
s.add_dependency 'actionpack', "= #{version}"
|
||||
end
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).chomp
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'actionpack'
|
||||
s.version = '2.3.18'
|
||||
s.version = version
|
||||
s.summary = 'Web-flow and rendering framework putting the VC in MVC.'
|
||||
s.description = 'Eases web-request routing, handling, and response as a half-way front, half-way page controller. Implemented with specific emphasis on enabling easy unit/integration testing that doesn\'t require a browser.'
|
||||
|
||||
@@ -10,6 +12,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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -85,11 +85,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />',
|
||||
|
||||
@@ -24,9 +24,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("£1234567890,50", number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""}))
|
||||
assert_equal("£1234567890,50", number_to_currency(1234567890.50, {:unit => raw("£"), :separator => ",", :delimiter => ""}))
|
||||
assert_equal("&pound;1234567890,50", number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""}))
|
||||
assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50"))
|
||||
assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"}))
|
||||
assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => raw("Kč"), :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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ module ActiveRecord
|
||||
# those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
|
||||
class MacroReflection
|
||||
attr_reader :active_record
|
||||
attr_accessor :cached_extend_class
|
||||
|
||||
def initialize(macro, name, options, active_record)
|
||||
@macro, @name, @options, @active_record = macro, name, options, active_record
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
*2.3.11 (February 9, 2011)*
|
||||
*2.3.10 (October 15, 2010)*
|
||||
*2.3.9 (September 4, 2010)*
|
||||
*2.3.8 (May 24, 2010)*
|
||||
*2.3.7 (May 24, 2010)*
|
||||
|
||||
* Version bump.
|
||||
|
||||
|
||||
*2.3.6 (May 23, 2010)*
|
||||
|
||||
* No changes, just a version bump.
|
||||
|
||||
|
||||
*2.3.5 (November 25, 2009)*
|
||||
|
||||
* Minor Bug Fixes and deprecation warnings
|
||||
|
||||
* More flexible content type handling when parsing responses.
|
||||
|
||||
Ensures that ARes will handle responses like test/xml, or content types
|
||||
with charsets included.
|
||||
|
||||
*2.3.4 (September 4, 2009)*
|
||||
|
||||
* Add support for errors in JSON format. #1956 [Fabien Jakimowicz]
|
||||
|
||||
* Recognizes 410 as Resource Gone. #2316 [Jordan Brough, Jatinder Singh]
|
||||
|
||||
* More thorough SSL support. #2370 [Roy Nicholson]
|
||||
|
||||
* HTTP proxy support. #2133 [Marshall Huss, Sébastien Dabet]
|
||||
|
||||
|
||||
*2.3.3 (July 12, 2009)*
|
||||
|
||||
* No changes, just a version bump.
|
||||
|
||||
|
||||
*2.3.2 [Final] (March 15, 2009)*
|
||||
|
||||
* Nothing new, just included in 2.3.2
|
||||
|
||||
|
||||
*2.2.1 [RC2] (November 14th, 2008)*
|
||||
|
||||
* Fixed that ActiveResource#post would post an empty string when it shouldn't be posting anything #525 [Paolo Angelini]
|
||||
|
||||
|
||||
*2.2.0 [RC1] (October 24th, 2008)*
|
||||
|
||||
* Add ActiveResource::Base#to_xml and ActiveResource::Base#to_json. #1011 [Rasik Pandey, Cody Fauser]
|
||||
|
||||
* Add ActiveResource::Base.find(:last). [#754 state:resolved] (Adrian Mugnolo)
|
||||
|
||||
* Fixed problems with the logger used if the logging string included %'s [#840 state:resolved] (Jamis Buck)
|
||||
|
||||
* Fixed Base#exists? to check status code as integer [#299 state:resolved] (Wes Oldenbeuving)
|
||||
|
||||
|
||||
*2.1.0 (May 31st, 2008)*
|
||||
|
||||
* Fixed response logging to use length instead of the entire thing (seangeo) [#27]
|
||||
|
||||
* Fixed that to_param should be used and honored instead of hardcoding the id #11406 [gspiers]
|
||||
|
||||
* Improve documentation. [Ryan Bigg, Jan De Poorter, Cheah Chu Yeow, Xavier Shay, Jack Danger Canty, Emilio Tagua, Xavier Noria, Sunny Ripert]
|
||||
|
||||
* Use HEAD instead of GET in exists? [bscofield]
|
||||
|
||||
* Fix small documentation typo. Closes #10670 [Luca Guidi]
|
||||
|
||||
* find_or_create_resource_for handles module nesting. #10646 [xavier]
|
||||
|
||||
* Allow setting ActiveResource::Base#format before #site. [Rick Olson]
|
||||
|
||||
* Support agnostic formats when calling custom methods. Closes #10635 [joerichsen]
|
||||
|
||||
* Document custom methods. #10589 [Cheah Chu Yeow]
|
||||
|
||||
* Ruby 1.9 compatibility. [Jeremy Kemper]
|
||||
|
||||
|
||||
*2.0.2* (December 16th, 2007)
|
||||
|
||||
* Added more specific exceptions for 400, 401, and 403 (all descending from ClientError so existing rescues will work) #10326 [trek]
|
||||
|
||||
* Correct empty response handling. #10445 [seangeo]
|
||||
|
||||
|
||||
*2.0.1* (December 7th, 2007)
|
||||
|
||||
* Don't cache net/http object so that ActiveResource is more thread-safe. Closes #10142 [kou]
|
||||
|
||||
* Update XML documentation examples to include explicit type attributes. Closes #9754 [Josh Susser]
|
||||
|
||||
* Added one-off declarations of mock behavior [David Heinemeier Hansson]. Example:
|
||||
|
||||
Before:
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/1.xml", {}, "<person><name>David</name></person>"
|
||||
end
|
||||
|
||||
Now:
|
||||
ActiveResource::HttpMock.respond_to.get "/people/1.xml", {}, "<person><name>David</name></person>"
|
||||
|
||||
* Added ActiveResource.format= which defaults to :xml but can also be set to :json [David Heinemeier Hansson]. Example:
|
||||
|
||||
class Person < ActiveResource::Base
|
||||
self.site = "http://app/"
|
||||
self.format = :json
|
||||
end
|
||||
|
||||
person = Person.find(1) # => GET http://app/people/1.json
|
||||
person.name = "David"
|
||||
person.save # => PUT http://app/people/1.json {name: "David"}
|
||||
|
||||
Person.format = :xml
|
||||
person.name = "Mary"
|
||||
person.save # => PUT http://app/people/1.json <person><name>Mary</name></person>
|
||||
|
||||
* Fix reload error when path prefix is used. #8727 [Ian Warshak]
|
||||
|
||||
* Remove ActiveResource::Struct because it hasn't proven very useful. Creating a new ActiveResource::Base subclass is often less code and always clearer. #8612 [Josh Peek]
|
||||
|
||||
* Fix query methods on resources. [Cody Fauser]
|
||||
|
||||
* pass the prefix_options to the instantiated record when using find without a specific id. Closes #8544 [Eloy Duran]
|
||||
|
||||
* Recognize and raise an exception on 405 Method Not Allowed responses. #7692 [Josh Peek]
|
||||
|
||||
* Handle string and symbol param keys when splitting params into prefix params and query params.
|
||||
|
||||
Comment.find(:all, :params => { :article_id => 5, :page => 2 }) or Comment.find(:all, :params => { 'article_id' => 5, :page => 2 })
|
||||
|
||||
* Added find-one with symbol [David Heinemeier Hansson]. Example: Person.find(:one, :from => :leader) # => GET /people/leader.xml
|
||||
|
||||
* BACKWARDS INCOMPATIBLE: Changed the finder API to be more extensible with :params and more strict usage of scopes [David Heinemeier Hansson]. Changes:
|
||||
|
||||
Person.find(:all, :title => "CEO") ...becomes: Person.find(:all, :params => { :title => "CEO" })
|
||||
Person.find(:managers) ...becomes: Person.find(:all, :from => :managers)
|
||||
Person.find("/companies/1/manager.xml") ...becomes: Person.find(:one, :from => "/companies/1/manager.xml")
|
||||
|
||||
* Add support for setting custom headers per Active Resource model [Rick Olson]
|
||||
|
||||
class Project
|
||||
headers['X-Token'] = 'foo'
|
||||
end
|
||||
|
||||
# makes the GET request with the custom X-Token header
|
||||
Project.find(:all)
|
||||
|
||||
* Added find-by-path options to ActiveResource::Base.find [David Heinemeier Hansson]. Examples:
|
||||
|
||||
employees = Person.find(:all, :from => "/companies/1/people.xml") # => GET /companies/1/people.xml
|
||||
manager = Person.find("/companies/1/manager.xml") # => GET /companies/1/manager.xml
|
||||
|
||||
|
||||
* Added support for using classes from within a single nested module [David Heinemeier Hansson]. Example:
|
||||
|
||||
module Highrise
|
||||
class Note < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
|
||||
class Comment < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
end
|
||||
|
||||
assert_kind_of Highrise::Comment, Note.find(1).comments.first
|
||||
|
||||
|
||||
* Added load_attributes_from_response as a way of loading attributes from other responses than just create [David Heinemeier Hansson]
|
||||
|
||||
class Highrise::Task < ActiveResource::Base
|
||||
def complete
|
||||
load_attributes_from_response(post(:complete))
|
||||
end
|
||||
end
|
||||
|
||||
...will set "done_at" when complete is called.
|
||||
|
||||
|
||||
* Added support for calling custom methods #6979 [rwdaigle]
|
||||
|
||||
Person.find(:managers) # => GET /people/managers.xml
|
||||
Kase.find(1).post(:close) # => POST /kases/1/close.xml
|
||||
|
||||
* Remove explicit prefix_options parameter for ActiveResource::Base#initialize. [Rick Olson]
|
||||
ActiveResource splits the prefix_options from it automatically.
|
||||
|
||||
* Allow ActiveResource::Base.delete with custom prefix. [Rick Olson]
|
||||
|
||||
* Add ActiveResource::Base#dup [Rick Olson]
|
||||
|
||||
* Fixed constant warning when fetching the same object multiple times [David Heinemeier Hansson]
|
||||
|
||||
* Added that saves which get a body response (and not just a 201) will use that response to update themselves [David Heinemeier Hansson]
|
||||
|
||||
* Disregard namespaces from the default element name, so Highrise::Person will just try to fetch from "/people", not "/highrise/people" [David Heinemeier Hansson]
|
||||
|
||||
* Allow array and hash query parameters. #7756 [Greg Spurrier]
|
||||
|
||||
* Loading a resource preserves its prefix_options. #7353 [Ryan Daigle]
|
||||
|
||||
* Carry over the convenience of #create from ActiveRecord. Closes #7340. [Ryan Daigle]
|
||||
|
||||
* Increase ActiveResource::Base test coverage. Closes #7173, #7174 [Rich Collins]
|
||||
|
||||
* Interpret 422 Unprocessable Entity as ResourceInvalid. #7097 [dkubb]
|
||||
|
||||
* Mega documentation patches. #7025, #7069 [rwdaigle]
|
||||
|
||||
* Base.exists?(id, options) and Base#exists? check whether the resource is found. #6970 [rwdaigle]
|
||||
|
||||
* Query string support. [untext, Jeremy Kemper]
|
||||
# GET /forums/1/topics.xml?sort=created_at
|
||||
Topic.find(:all, :forum_id => 1, :sort => 'created_at')
|
||||
|
||||
* Base#==, eql?, and hash methods. == returns true if its argument is identical to self or if it's an instance of the same class, is not new?, and has the same id. eql? is an alias for ==. hash delegates to id. [Jeremy Kemper]
|
||||
|
||||
* Allow subclassed resources to share the site info [Rick Olson, Jeremy Kemper]
|
||||
d
|
||||
class BeastResource < ActiveResource::Base
|
||||
self.site = 'http://beast.caboo.se'
|
||||
end
|
||||
|
||||
class Forum < BeastResource
|
||||
# taken from BeastResource
|
||||
# self.site = 'http://beast.caboo.se'
|
||||
end
|
||||
|
||||
class Topic < BeastResource
|
||||
self.site += '/forums/:forum_id'
|
||||
end
|
||||
|
||||
* Fix issues with ActiveResource collection handling. Closes #6291. [bmilekic]
|
||||
|
||||
* Use attr_accessor_with_default to dry up attribute initialization. References #6538. [Stuart Halloway]
|
||||
|
||||
* Add basic logging support for logging outgoing requests. [Jamis Buck]
|
||||
|
||||
* Add Base.delete for deleting resources without having to instantiate them first. [Jamis Buck]
|
||||
|
||||
* Make #save behavior mimic AR::Base#save (true on success, false on failure). [Jamis Buck]
|
||||
|
||||
* Add Basic HTTP Authentication to ActiveResource (closes #6305). [jonathan]
|
||||
|
||||
* Extracted #id_from_response as an entry point for customizing how a created resource gets its own ID.
|
||||
By default, it extracts from the Location response header.
|
||||
|
||||
* Optimistic locking: raise ActiveResource::ResourceConflict on 409 Conflict response. [Jeremy Kemper]
|
||||
|
||||
# Example controller action
|
||||
def update
|
||||
@person.save!
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
render :xml => @person.reload.to_xml, :status => '409 Conflict'
|
||||
end
|
||||
|
||||
* Basic validation support [Rick Olson]
|
||||
|
||||
Parses the xml response of ActiveRecord::Errors#to_xml with a similar interface to ActiveRecord::Errors.
|
||||
|
||||
render :xml => @person.errors.to_xml, :status => '400 Validation Error'
|
||||
|
||||
* Deep hashes are converted into collections of resources. [Jeremy Kemper]
|
||||
Person.new :name => 'Bob',
|
||||
:address => { :id => 1, :city => 'Portland' },
|
||||
:contacts => [{ :id => 1 }, { :id => 2 }]
|
||||
Looks for Address and Contact resources and creates them if unavailable.
|
||||
So clients can fetch a complex resource in a single request if you e.g.
|
||||
render :xml => @person.to_xml(:include => [:address, :contacts])
|
||||
in your controller action.
|
||||
|
||||
* Major updates [Rick Olson]
|
||||
|
||||
* Add full support for find/create/update/destroy
|
||||
* Add support for specifying prefixes.
|
||||
* Allow overriding of element_name, collection_name, and primary key
|
||||
* Provide simpler HTTP mock interface for testing
|
||||
|
||||
# rails routing code
|
||||
map.resources :posts do |post|
|
||||
post.resources :comments
|
||||
end
|
||||
|
||||
# ActiveResources
|
||||
class Post < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000/"
|
||||
end
|
||||
|
||||
class Comment < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000/posts/:post_id/"
|
||||
end
|
||||
|
||||
@post = Post.find 5
|
||||
@comments = Comment.find :all, :post_id => @post.id
|
||||
|
||||
@comment = Comment.new({:body => 'hello world'}, {:post_id => @post.id})
|
||||
@comment.save
|
||||
|
||||
* Base.site= accepts URIs. 200...400 are valid response codes. PUT and POST request bodies default to ''. [Jeremy Kemper]
|
||||
|
||||
* Initial checkin: object-oriented client for restful HTTP resources which follow the Rails convention. [David Heinemeier Hansson]
|
||||
@@ -1,20 +0,0 @@
|
||||
Copyright (c) 2006-2010 David Heinemeier Hansson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,165 +0,0 @@
|
||||
= Active Resource
|
||||
|
||||
Active Resource (ARes) connects business objects and Representational State Transfer (REST)
|
||||
web services. It implements object-relational mapping for REST webservices to provide transparent
|
||||
proxying capabilities between a client (ActiveResource) and a RESTful service (which is provided by Simply RESTful routing
|
||||
in ActionController::Resources).
|
||||
|
||||
== Philosophy
|
||||
|
||||
Active Resource attempts to provide a coherent wrapper object-relational mapping for REST
|
||||
web services. It follows the same philosophy as Active Record, in that one of its prime aims
|
||||
is to reduce the amount of code needed to map to these resources. This is made possible
|
||||
by relying on a number of code- and protocol-based conventions that make it easy for Active Resource
|
||||
to infer complex relations and structures. These conventions are outlined in detail in the documentation
|
||||
for ActiveResource::Base.
|
||||
|
||||
== Overview
|
||||
|
||||
Model classes are mapped to remote REST resources by Active Resource much the same way Active Record maps model classes to database
|
||||
tables. When a request is made to a remote resource, a REST XML request is generated, transmitted, and the result
|
||||
received and serialized into a usable Ruby object.
|
||||
|
||||
=== Configuration and Usage
|
||||
|
||||
Putting ActiveResource to use is very similar to ActiveRecord. It's as simple as creating a model class
|
||||
that inherits from ActiveResource::Base and providing a <tt>site</tt> class variable to it:
|
||||
|
||||
class Person < ActiveResource::Base
|
||||
self.site = "http://api.people.com:3000/"
|
||||
end
|
||||
|
||||
Now the Person class is REST enabled and can invoke REST services very similarly to how ActiveRecord invokes
|
||||
lifecycle methods that operate against a persistent store.
|
||||
|
||||
# Find a person with id = 1
|
||||
ryan = Person.find(1)
|
||||
Person.exists?(1) #=> true
|
||||
|
||||
As you can see, the methods are quite similar to Active Record's methods for dealing with database
|
||||
records. But rather than dealing directly with a database record, you're dealing with HTTP resources (which may or may not be database records).
|
||||
|
||||
==== Protocol
|
||||
|
||||
Active Resource is built on a standard XML format for requesting and submitting resources over HTTP. It mirrors the RESTful routing
|
||||
built into ActionController but will also work with any other REST service that properly implements the protocol.
|
||||
REST uses HTTP, but unlike "typical" web applications, it makes use of all the verbs available in the HTTP specification:
|
||||
|
||||
* GET requests are used for finding and retrieving resources.
|
||||
* POST requests are used to create new resources.
|
||||
* PUT requests are used to update existing resources.
|
||||
* DELETE requests are used to delete resources.
|
||||
|
||||
For more information on how this protocol works with Active Resource, see the ActiveResource::Base documentation;
|
||||
for more general information on REST web services, see the article here[http://en.wikipedia.org/wiki/Representational_State_Transfer].
|
||||
|
||||
==== Find
|
||||
|
||||
GET Http requests expect the XML form of whatever resource/resources is/are being requested. So,
|
||||
for a request for a single element - the XML of that item is expected in response:
|
||||
|
||||
# Expects a response of
|
||||
#
|
||||
# <person><id type="integer">1</id><attribute1>value1</attribute1><attribute2>..</attribute2></person>
|
||||
#
|
||||
# for GET http://api.people.com:3000/people/1.xml
|
||||
#
|
||||
ryan = Person.find(1)
|
||||
|
||||
The XML document that is received is used to build a new object of type Person, with each
|
||||
XML element becoming an attribute on the object.
|
||||
|
||||
ryan.is_a? Person #=> true
|
||||
ryan.attribute1 #=> 'value1'
|
||||
|
||||
Any complex element (one that contains other elements) becomes its own object:
|
||||
|
||||
# With this response:
|
||||
#
|
||||
# <person><id>1</id><attribute1>value1</attribute1><complex><attribute2>value2</attribute2></complex></person>
|
||||
#
|
||||
# for GET http://api.people.com:3000/people/1.xml
|
||||
#
|
||||
ryan = Person.find(1)
|
||||
ryan.complex #=> <Person::Complex::xxxxx>
|
||||
ryan.complex.attribute2 #=> 'value2'
|
||||
|
||||
Collections can also be requested in a similar fashion
|
||||
|
||||
# Expects a response of
|
||||
#
|
||||
# <people type="array">
|
||||
# <person><id type="integer">1</id><first>Ryan</first></person>
|
||||
# <person><id type="integer">2</id><first>Jim</first></person>
|
||||
# </people>
|
||||
#
|
||||
# for GET http://api.people.com:3000/people.xml
|
||||
#
|
||||
people = Person.find(:all)
|
||||
people.first #=> <Person::xxx 'first' => 'Ryan' ...>
|
||||
people.last #=> <Person::xxx 'first' => 'Jim' ...>
|
||||
|
||||
==== Create
|
||||
|
||||
Creating a new resource submits the xml form of the resource as the body of the request and expects
|
||||
a 'Location' header in the response with the RESTful URL location of the newly created resource. The
|
||||
id of the newly created resource is parsed out of the Location response header and automatically set
|
||||
as the id of the ARes object.
|
||||
|
||||
# <person><first>Ryan</first></person>
|
||||
#
|
||||
# is submitted as the body on
|
||||
#
|
||||
# POST http://api.people.com:3000/people.xml
|
||||
#
|
||||
# when save is called on a new Person object. An empty response is
|
||||
# is expected with a 'Location' header value:
|
||||
#
|
||||
# Response (201): Location: http://api.people.com:3000/people/2
|
||||
#
|
||||
ryan = Person.new(:first => 'Ryan')
|
||||
ryan.new? #=> true
|
||||
ryan.save #=> true
|
||||
ryan.new? #=> false
|
||||
ryan.id #=> 2
|
||||
|
||||
==== Update
|
||||
|
||||
'save' is also used to update an existing resource - and follows the same protocol as creating a resource
|
||||
with the exception that no response headers are needed - just an empty response when the update on the
|
||||
server side was successful.
|
||||
|
||||
# <person><first>Ryan</first></person>
|
||||
#
|
||||
# is submitted as the body on
|
||||
#
|
||||
# PUT http://api.people.com:3000/people/1.xml
|
||||
#
|
||||
# when save is called on an existing Person object. An empty response is
|
||||
# is expected with code (204)
|
||||
#
|
||||
ryan = Person.find(1)
|
||||
ryan.first #=> 'Ryan'
|
||||
ryan.first = 'Rizzle'
|
||||
ryan.save #=> true
|
||||
|
||||
==== Delete
|
||||
|
||||
Destruction of a resource can be invoked as a class and instance method of the resource.
|
||||
|
||||
# A request is made to
|
||||
#
|
||||
# DELETE http://api.people.com:3000/people/1.xml
|
||||
#
|
||||
# for both of these forms. An empty response with
|
||||
# is expected with response code (200)
|
||||
#
|
||||
ryan = Person.find(1)
|
||||
ryan.destroy #=> true
|
||||
ryan.exists? #=> false
|
||||
Person.delete(2) #=> true
|
||||
Person.exists?(2) #=> false
|
||||
|
||||
|
||||
You can find more usage information in the ActiveResource::Base documentation.
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
require 'rubygems'
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rdoc/task'
|
||||
require 'rake/packagetask'
|
||||
require 'rubygems/package_task'
|
||||
|
||||
require File.join(File.dirname(__FILE__), 'lib', 'active_resource', 'version')
|
||||
|
||||
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
||||
PKG_NAME = 'activeresource'
|
||||
PKG_VERSION = ActiveResource::VERSION::STRING + PKG_BUILD
|
||||
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
||||
|
||||
RELEASE_NAME = "REL #{PKG_VERSION}"
|
||||
|
||||
RUBY_FORGE_PROJECT = "activerecord"
|
||||
RUBY_FORGE_USER = "webster132"
|
||||
|
||||
PKG_FILES = FileList[
|
||||
"lib/**/*", "test/**/*", "[A-Z]*", "Rakefile"
|
||||
].exclude(/\bCVS\b|~$/)
|
||||
|
||||
desc "Default Task"
|
||||
task :default => [ :test ]
|
||||
|
||||
# Run the unit tests
|
||||
|
||||
Rake::TestTask.new { |t|
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../activesupport/lib"
|
||||
t.libs << activesupport_path if File.directory?(activesupport_path)
|
||||
t.libs << "test"
|
||||
t.pattern = 'test/**/*_test.rb'
|
||||
}
|
||||
|
||||
|
||||
# Generate the RDoc documentation
|
||||
|
||||
RDoc::Task.new { |rdoc|
|
||||
rdoc.rdoc_dir = 'doc'
|
||||
rdoc.title = "Active Resource -- Object-oriented REST services"
|
||||
rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
|
||||
rdoc.options << '--charset' << 'utf-8'
|
||||
rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo'
|
||||
rdoc.rdoc_files.include('README', 'CHANGELOG')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
rdoc.rdoc_files.exclude('lib/activeresource.rb')
|
||||
}
|
||||
|
||||
|
||||
# Create compressed packages
|
||||
|
||||
dist_dirs = [ "lib", "test", "examples", "dev-utils" ]
|
||||
|
||||
spec = Gem::Specification.new do |s|
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.name = PKG_NAME
|
||||
s.version = PKG_VERSION
|
||||
s.summary = "Think Active Record for web resources."
|
||||
s.description = %q{Wraps web resources in model classes that can be manipulated through XML over REST.}
|
||||
|
||||
s.files = [ "Rakefile", "README", "CHANGELOG" ]
|
||||
dist_dirs.each do |dir|
|
||||
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
end
|
||||
|
||||
s.add_dependency('activesupport', '= 2.3.14' + PKG_BUILD)
|
||||
|
||||
s.require_path = 'lib'
|
||||
|
||||
s.extra_rdoc_files = %w( README )
|
||||
s.rdoc_options.concat ['--main', 'README']
|
||||
|
||||
s.author = "David Heinemeier Hansson"
|
||||
s.email = "david@loudthinking.com"
|
||||
s.homepage = "http://www.rubyonrails.org"
|
||||
s.rubyforge_project = "activeresource"
|
||||
end
|
||||
|
||||
Gem::PackageTask.new(spec) do |p|
|
||||
p.gem_spec = spec
|
||||
p.need_tar = true
|
||||
p.need_zip = true
|
||||
end
|
||||
|
||||
task :lines do
|
||||
lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
|
||||
|
||||
for file_name in FileList["lib/active_resource/**/*.rb"]
|
||||
next if file_name =~ /vendor/
|
||||
f = File.open(file_name)
|
||||
|
||||
while line = f.gets
|
||||
lines += 1
|
||||
next if line =~ /^\s*$/
|
||||
next if line =~ /^\s*#/
|
||||
codelines += 1
|
||||
end
|
||||
puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
|
||||
|
||||
total_lines += lines
|
||||
total_codelines += codelines
|
||||
|
||||
lines, codelines = 0, 0
|
||||
end
|
||||
|
||||
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
|
||||
end
|
||||
|
||||
|
||||
# Publishing ------------------------------------------------------
|
||||
|
||||
desc "Publish the beta gem"
|
||||
task :pgem => [:package] do
|
||||
require 'rake/contrib/sshpublisher'
|
||||
Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
|
||||
`ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
|
||||
end
|
||||
|
||||
desc "Publish the API documentation"
|
||||
task :pdoc => [:rdoc] do
|
||||
require 'rake/contrib/sshpublisher'
|
||||
Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/ar", "doc").upload
|
||||
end
|
||||
|
||||
desc "Publish the release files to RubyForge."
|
||||
task :release => [ :package ] do
|
||||
`rubyforge login`
|
||||
|
||||
for ext in %w( gem tgz zip )
|
||||
release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}"
|
||||
puts release_command
|
||||
system(release_command)
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'activeresource'
|
||||
s.version = '2.3.18'
|
||||
s.summary = 'Think Active Record for web resources.'
|
||||
s.description = 'Wraps web resources in model classes that can be manipulated through XML over REST.'
|
||||
|
||||
s.author = 'David Heinemeier Hansson'
|
||||
s.email = 'david@loudthinking.com'
|
||||
s.homepage = 'http://www.rubyonrails.org'
|
||||
|
||||
s.require_path = 'lib'
|
||||
s.files = ['README']
|
||||
s.rdoc_options = ['--main', 'README']
|
||||
s.extra_rdoc_files = ['README']
|
||||
|
||||
s.add_dependency 'activesupport', '= 2.3.18'
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
#--
|
||||
# Copyright (c) 2006 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'active_support'
|
||||
rescue LoadError
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
if File.directory?(activesupport_path)
|
||||
$:.unshift activesupport_path
|
||||
require 'active_support'
|
||||
end
|
||||
end
|
||||
|
||||
require 'active_resource/formats'
|
||||
require 'active_resource/base'
|
||||
require 'active_resource/validations'
|
||||
require 'active_resource/custom_methods'
|
||||
|
||||
module ActiveResource
|
||||
Base.class_eval do
|
||||
include Validations
|
||||
include CustomMethods
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,283 +0,0 @@
|
||||
require 'net/https'
|
||||
require 'date'
|
||||
require 'time'
|
||||
require 'uri'
|
||||
require 'benchmark'
|
||||
|
||||
module ActiveResource
|
||||
class ConnectionError < StandardError # :nodoc:
|
||||
attr_reader :response
|
||||
|
||||
def initialize(response, message = nil)
|
||||
@response = response
|
||||
@message = message
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Raised when a Timeout::Error occurs.
|
||||
class TimeoutError < ConnectionError
|
||||
def initialize(message)
|
||||
@message = message
|
||||
end
|
||||
def to_s; @message ;end
|
||||
end
|
||||
|
||||
# Raised when a OpenSSL::SSL::SSLError occurs.
|
||||
class SSLError < ConnectionError
|
||||
def initialize(message)
|
||||
@message = message
|
||||
end
|
||||
def to_s; @message ;end
|
||||
end
|
||||
|
||||
# 3xx Redirection
|
||||
class Redirection < ConnectionError # :nodoc:
|
||||
def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
|
||||
end
|
||||
|
||||
# 4xx Client Error
|
||||
class ClientError < ConnectionError; end # :nodoc:
|
||||
|
||||
# 400 Bad Request
|
||||
class BadRequest < ClientError; end # :nodoc
|
||||
|
||||
# 401 Unauthorized
|
||||
class UnauthorizedAccess < ClientError; end # :nodoc
|
||||
|
||||
# 403 Forbidden
|
||||
class ForbiddenAccess < ClientError; end # :nodoc
|
||||
|
||||
# 404 Not Found
|
||||
class ResourceNotFound < ClientError; end # :nodoc:
|
||||
|
||||
# 409 Conflict
|
||||
class ResourceConflict < ClientError; end # :nodoc:
|
||||
|
||||
# 410 Gone
|
||||
class ResourceGone < ClientError; end # :nodoc:
|
||||
|
||||
# 5xx Server Error
|
||||
class ServerError < ConnectionError; end # :nodoc:
|
||||
|
||||
# 405 Method Not Allowed
|
||||
class MethodNotAllowed < ClientError # :nodoc:
|
||||
def allowed_methods
|
||||
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
|
||||
end
|
||||
end
|
||||
|
||||
# Class to handle connections to remote web services.
|
||||
# This class is used by ActiveResource::Base to interface with REST
|
||||
# services.
|
||||
class Connection
|
||||
|
||||
HTTP_FORMAT_HEADER_NAMES = { :get => 'Accept',
|
||||
:put => 'Content-Type',
|
||||
:post => 'Content-Type',
|
||||
:delete => 'Accept',
|
||||
:head => 'Accept'
|
||||
}
|
||||
|
||||
attr_reader :site, :user, :password, :timeout, :proxy, :ssl_options
|
||||
attr_accessor :format
|
||||
|
||||
class << self
|
||||
def requests
|
||||
@@requests ||= []
|
||||
end
|
||||
end
|
||||
|
||||
# The +site+ parameter is required and will set the +site+
|
||||
# attribute to the URI for the remote resource service.
|
||||
def initialize(site, format = ActiveResource::Formats[:xml])
|
||||
raise ArgumentError, 'Missing site URI' unless site
|
||||
@user = @password = nil
|
||||
self.site = site
|
||||
self.format = format
|
||||
end
|
||||
|
||||
# Set URI for remote service.
|
||||
def site=(site)
|
||||
@site = site.is_a?(URI) ? site : URI.parse(site)
|
||||
@user = URI::DEFAULT_PARSER.unescape(@site.user) if @site.user
|
||||
@password = URI::DEFAULT_PARSER.unescape(@site.password) if @site.password
|
||||
end
|
||||
|
||||
# Set the proxy for remote service.
|
||||
def proxy=(proxy)
|
||||
@proxy = proxy.is_a?(URI) ? proxy : URI.parse(proxy)
|
||||
end
|
||||
|
||||
# Set the user for remote service.
|
||||
def user=(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
# Set password for remote service.
|
||||
def password=(password)
|
||||
@password = password
|
||||
end
|
||||
|
||||
# Set the number of seconds after which HTTP requests to the remote service should time out.
|
||||
def timeout=(timeout)
|
||||
@timeout = timeout
|
||||
end
|
||||
|
||||
# Hash of options applied to Net::HTTP instance when +site+ protocol is 'https'.
|
||||
def ssl_options=(opts={})
|
||||
@ssl_options = opts
|
||||
end
|
||||
|
||||
# Execute a GET request.
|
||||
# Used to get (find) resources.
|
||||
def get(path, headers = {})
|
||||
format.decode(request(:get, path, build_request_headers(headers, :get)).body)
|
||||
end
|
||||
|
||||
# Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
|
||||
# Used to delete resources.
|
||||
def delete(path, headers = {})
|
||||
request(:delete, path, build_request_headers(headers, :delete))
|
||||
end
|
||||
|
||||
# Execute a PUT request (see HTTP protocol documentation if unfamiliar).
|
||||
# Used to update resources.
|
||||
def put(path, body = '', headers = {})
|
||||
request(:put, path, body.to_s, build_request_headers(headers, :put))
|
||||
end
|
||||
|
||||
# Execute a POST request.
|
||||
# Used to create new resources.
|
||||
def post(path, body = '', headers = {})
|
||||
request(:post, path, body.to_s, build_request_headers(headers, :post))
|
||||
end
|
||||
|
||||
# Execute a HEAD request.
|
||||
# Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
|
||||
def head(path, headers = {})
|
||||
request(:head, path, build_request_headers(headers, :head))
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
# Makes request to remote service.
|
||||
def request(method, path, *arguments)
|
||||
logger.info "#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}" if logger
|
||||
result = nil
|
||||
ms = Benchmark.ms { result = http.send(method, path, *arguments) }
|
||||
logger.info "--> %d %s (%d %.0fms)" % [result.code, result.message, result.body ? result.body.length : 0, ms] if logger
|
||||
handle_response(result)
|
||||
rescue Timeout::Error => e
|
||||
raise TimeoutError.new(e.message)
|
||||
rescue OpenSSL::SSL::SSLError => e
|
||||
raise SSLError.new(e.message)
|
||||
end
|
||||
|
||||
# Handles response and error codes from remote service.
|
||||
def handle_response(response)
|
||||
case response.code.to_i
|
||||
when 301,302
|
||||
raise(Redirection.new(response))
|
||||
when 200...400
|
||||
response
|
||||
when 400
|
||||
raise(BadRequest.new(response))
|
||||
when 401
|
||||
raise(UnauthorizedAccess.new(response))
|
||||
when 403
|
||||
raise(ForbiddenAccess.new(response))
|
||||
when 404
|
||||
raise(ResourceNotFound.new(response))
|
||||
when 405
|
||||
raise(MethodNotAllowed.new(response))
|
||||
when 409
|
||||
raise(ResourceConflict.new(response))
|
||||
when 410
|
||||
raise(ResourceGone.new(response))
|
||||
when 422
|
||||
raise(ResourceInvalid.new(response))
|
||||
when 401...500
|
||||
raise(ClientError.new(response))
|
||||
when 500...600
|
||||
raise(ServerError.new(response))
|
||||
else
|
||||
raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
|
||||
end
|
||||
end
|
||||
|
||||
# Creates new Net::HTTP instance for communication with
|
||||
# remote service and resources.
|
||||
def http
|
||||
configure_http(new_http)
|
||||
end
|
||||
|
||||
def new_http
|
||||
if @proxy
|
||||
Net::HTTP.new(@site.host, @site.port, @proxy.host, @proxy.port, @proxy.user, @proxy.password)
|
||||
else
|
||||
Net::HTTP.new(@site.host, @site.port)
|
||||
end
|
||||
end
|
||||
|
||||
def configure_http(http)
|
||||
http = apply_ssl_options(http)
|
||||
|
||||
# Net::HTTP timeouts default to 60 seconds.
|
||||
if @timeout
|
||||
http.open_timeout = @timeout
|
||||
http.read_timeout = @timeout
|
||||
end
|
||||
|
||||
http
|
||||
end
|
||||
|
||||
def apply_ssl_options(http)
|
||||
return http unless @site.is_a?(URI::HTTPS)
|
||||
|
||||
http.use_ssl = true
|
||||
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||
return http unless defined?(@ssl_options)
|
||||
|
||||
http.ca_path = @ssl_options[:ca_path] if @ssl_options[:ca_path]
|
||||
http.ca_file = @ssl_options[:ca_file] if @ssl_options[:ca_file]
|
||||
|
||||
http.cert = @ssl_options[:cert] if @ssl_options[:cert]
|
||||
http.key = @ssl_options[:key] if @ssl_options[:key]
|
||||
|
||||
http.cert_store = @ssl_options[:cert_store] if @ssl_options[:cert_store]
|
||||
http.ssl_timeout = @ssl_options[:ssl_timeout] if @ssl_options[:ssl_timeout]
|
||||
|
||||
http.verify_mode = @ssl_options[:verify_mode] if @ssl_options[:verify_mode]
|
||||
http.verify_callback = @ssl_options[:verify_callback] if @ssl_options[:verify_callback]
|
||||
http.verify_depth = @ssl_options[:verify_depth] if @ssl_options[:verify_depth]
|
||||
|
||||
http
|
||||
end
|
||||
|
||||
def default_header
|
||||
@default_header ||= {}
|
||||
end
|
||||
|
||||
# Builds headers for request to remote service.
|
||||
def build_request_headers(headers, http_method=nil)
|
||||
authorization_header.update(default_header).update(http_format_header(http_method)).update(headers)
|
||||
end
|
||||
|
||||
# Sets authorization header
|
||||
def authorization_header
|
||||
(@user || @password ? { 'Authorization' => 'Basic ' + ["#{@user}:#{ @password}"].pack('m').delete("\r\n") } : {})
|
||||
end
|
||||
|
||||
def http_format_header(http_method)
|
||||
{HTTP_FORMAT_HEADER_NAMES[http_method] => format.mime_type}
|
||||
end
|
||||
|
||||
def logger #:nodoc:
|
||||
Base.logger
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,120 +0,0 @@
|
||||
module ActiveResource
|
||||
# A module to support custom REST methods and sub-resources, allowing you to break out
|
||||
# of the "default" REST methods with your own custom resource requests. For example,
|
||||
# say you use Rails to expose a REST service and configure your routes with:
|
||||
#
|
||||
# map.resources :people, :new => { :register => :post },
|
||||
# :member => { :promote => :put, :deactivate => :delete }
|
||||
# :collection => { :active => :get }
|
||||
#
|
||||
# This route set creates routes for the following HTTP requests:
|
||||
#
|
||||
# POST /people/new/register.xml # PeopleController.register
|
||||
# PUT /people/1/promote.xml # PeopleController.promote with :id => 1
|
||||
# DELETE /people/1/deactivate.xml # PeopleController.deactivate with :id => 1
|
||||
# GET /people/active.xml # PeopleController.active
|
||||
#
|
||||
# Using this module, Active Resource can use these custom REST methods just like the
|
||||
# standard methods.
|
||||
#
|
||||
# class Person < ActiveResource::Base
|
||||
# self.site = "http://37s.sunrise.i:3000"
|
||||
# end
|
||||
#
|
||||
# Person.new(:name => 'Ryan).post(:register) # POST /people/new/register.xml
|
||||
# # => { :id => 1, :name => 'Ryan' }
|
||||
#
|
||||
# Person.find(1).put(:promote, :position => 'Manager') # PUT /people/1/promote.xml
|
||||
# Person.find(1).delete(:deactivate) # DELETE /people/1/deactivate.xml
|
||||
#
|
||||
# Person.get(:active) # GET /people/active.xml
|
||||
# # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
|
||||
#
|
||||
module CustomMethods
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
extend ActiveResource::CustomMethods::ClassMethods
|
||||
include ActiveResource::CustomMethods::InstanceMethods
|
||||
|
||||
class << self
|
||||
alias :orig_delete :delete
|
||||
|
||||
# Invokes a GET to a given custom REST method. For example:
|
||||
#
|
||||
# Person.get(:active) # GET /people/active.xml
|
||||
# # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
|
||||
#
|
||||
# Person.get(:active, :awesome => true) # GET /people/active.xml?awesome=true
|
||||
# # => [{:id => 1, :name => 'Ryan'}]
|
||||
#
|
||||
# Note: the objects returned from this method are not automatically converted
|
||||
# into ActiveResource::Base instances - they are ordinary Hashes. If you are expecting
|
||||
# ActiveResource::Base instances, use the <tt>find</tt> class method with the
|
||||
# <tt>:from</tt> option. For example:
|
||||
#
|
||||
# Person.find(:all, :from => :active)
|
||||
def get(custom_method_name, options = {})
|
||||
connection.get(custom_method_collection_url(custom_method_name, options), headers)
|
||||
end
|
||||
|
||||
def post(custom_method_name, options = {}, body = '')
|
||||
connection.post(custom_method_collection_url(custom_method_name, options), body, headers)
|
||||
end
|
||||
|
||||
def put(custom_method_name, options = {}, body = '')
|
||||
connection.put(custom_method_collection_url(custom_method_name, options), body, headers)
|
||||
end
|
||||
|
||||
def delete(custom_method_name, options = {})
|
||||
# Need to jump through some hoops to retain the original class 'delete' method
|
||||
if custom_method_name.is_a?(Symbol)
|
||||
connection.delete(custom_method_collection_url(custom_method_name, options), headers)
|
||||
else
|
||||
orig_delete(custom_method_name, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def custom_method_collection_url(method_name, options = {})
|
||||
prefix_options, query_options = split_options(options)
|
||||
"#{prefix(prefix_options)}#{collection_name}/#{method_name}.#{format.extension}#{query_string(query_options)}"
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def get(method_name, options = {})
|
||||
connection.get(custom_method_element_url(method_name, options), self.class.headers)
|
||||
end
|
||||
|
||||
def post(method_name, options = {}, body = nil)
|
||||
request_body = body.blank? ? encode : body
|
||||
if new?
|
||||
connection.post(custom_method_new_element_url(method_name, options), request_body, self.class.headers)
|
||||
else
|
||||
connection.post(custom_method_element_url(method_name, options), request_body, self.class.headers)
|
||||
end
|
||||
end
|
||||
|
||||
def put(method_name, options = {}, body = '')
|
||||
connection.put(custom_method_element_url(method_name, options), body, self.class.headers)
|
||||
end
|
||||
|
||||
def delete(method_name, options = {})
|
||||
connection.delete(custom_method_element_url(method_name, options), self.class.headers)
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def custom_method_element_url(method_name, options = {})
|
||||
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{id}/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}"
|
||||
end
|
||||
|
||||
def custom_method_new_element_url(method_name, options = {})
|
||||
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/new/#{method_name}.#{self.class.format.extension}#{self.class.__send__(:query_string, options)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,66 +0,0 @@
|
||||
module ActiveResource
|
||||
class ConnectionError < StandardError # :nodoc:
|
||||
attr_reader :response
|
||||
|
||||
def initialize(response, message = nil)
|
||||
@response = response
|
||||
@message = message
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Raised when a Timeout::Error occurs.
|
||||
class TimeoutError < ConnectionError
|
||||
def initialize(message)
|
||||
@message = message
|
||||
end
|
||||
def to_s; @message ;end
|
||||
end
|
||||
|
||||
# Raised when a OpenSSL::SSL::SSLError occurs.
|
||||
class SSLError < ConnectionError
|
||||
def initialize(message)
|
||||
@message = message
|
||||
end
|
||||
def to_s; @message ;end
|
||||
end
|
||||
|
||||
# 3xx Redirection
|
||||
class Redirection < ConnectionError # :nodoc:
|
||||
def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
|
||||
end
|
||||
|
||||
# 4xx Client Error
|
||||
class ClientError < ConnectionError; end # :nodoc:
|
||||
|
||||
# 400 Bad Request
|
||||
class BadRequest < ClientError; end # :nodoc
|
||||
|
||||
# 401 Unauthorized
|
||||
class UnauthorizedAccess < ClientError; end # :nodoc
|
||||
|
||||
# 403 Forbidden
|
||||
class ForbiddenAccess < ClientError; end # :nodoc
|
||||
|
||||
# 404 Not Found
|
||||
class ResourceNotFound < ClientError; end # :nodoc:
|
||||
|
||||
# 409 Conflict
|
||||
class ResourceConflict < ClientError; end # :nodoc:
|
||||
|
||||
# 410 Gone
|
||||
class ResourceGone < ClientError; end # :nodoc:
|
||||
|
||||
# 5xx Server Error
|
||||
class ServerError < ConnectionError; end # :nodoc:
|
||||
|
||||
# 405 Method Not Allowed
|
||||
class MethodNotAllowed < ClientError # :nodoc:
|
||||
def allowed_methods
|
||||
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
module ActiveResource
|
||||
module Formats
|
||||
# Lookup the format class from a mime type reference symbol. Example:
|
||||
#
|
||||
# ActiveResource::Formats[:xml] # => ActiveResource::Formats::XmlFormat
|
||||
# ActiveResource::Formats[:json] # => ActiveResource::Formats::JsonFormat
|
||||
def self.[](mime_type_reference)
|
||||
ActiveResource::Formats.const_get(mime_type_reference.to_s.camelize + "Format")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'active_resource/formats/xml_format'
|
||||
require 'active_resource/formats/json_format'
|
||||
@@ -1,23 +0,0 @@
|
||||
module ActiveResource
|
||||
module Formats
|
||||
module JsonFormat
|
||||
extend self
|
||||
|
||||
def extension
|
||||
"json"
|
||||
end
|
||||
|
||||
def mime_type
|
||||
"application/json"
|
||||
end
|
||||
|
||||
def encode(hash, options = nil)
|
||||
ActiveSupport::JSON.encode(hash, options)
|
||||
end
|
||||
|
||||
def decode(json)
|
||||
ActiveSupport::JSON.decode(json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
module ActiveResource
|
||||
module Formats
|
||||
module XmlFormat
|
||||
extend self
|
||||
|
||||
def extension
|
||||
"xml"
|
||||
end
|
||||
|
||||
def mime_type
|
||||
"application/xml"
|
||||
end
|
||||
|
||||
def encode(hash, options={})
|
||||
hash.to_xml(options)
|
||||
end
|
||||
|
||||
def decode(xml)
|
||||
from_xml_data(Hash.from_xml(xml))
|
||||
end
|
||||
|
||||
private
|
||||
# Manipulate from_xml Hash, because xml_simple is not exactly what we
|
||||
# want for Active Resource.
|
||||
def from_xml_data(data)
|
||||
if data.is_a?(Hash) && data.keys.size == 1
|
||||
data.values.first
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,302 +0,0 @@
|
||||
require 'active_resource/connection'
|
||||
|
||||
module ActiveResource
|
||||
class InvalidRequestError < StandardError; end #:nodoc:
|
||||
|
||||
# One thing that has always been a pain with remote web services is testing. The HttpMock
|
||||
# class makes it easy to test your Active Resource models by creating a set of mock responses to specific
|
||||
# requests.
|
||||
#
|
||||
# To test your Active Resource model, you simply call the ActiveResource::HttpMock.respond_to
|
||||
# method with an attached block. The block declares a set of URIs with expected input, and the output
|
||||
# each request should return. The passed in block has any number of entries in the following generalized
|
||||
# format:
|
||||
#
|
||||
# mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
||||
#
|
||||
# * <tt>http_method</tt> - The HTTP method to listen for. This can be +get+, +post+, +put+, +delete+ or
|
||||
# +head+.
|
||||
# * <tt>path</tt> - A string, starting with a "/", defining the URI that is expected to be
|
||||
# called.
|
||||
# * <tt>request_headers</tt> - Headers that are expected along with the request. This argument uses a
|
||||
# hash format, such as <tt>{ "Content-Type" => "application/xml" }</tt>. This mock will only trigger
|
||||
# if your tests sends a request with identical headers.
|
||||
# * <tt>body</tt> - The data to be returned. This should be a string of Active Resource parseable content,
|
||||
# such as XML.
|
||||
# * <tt>status</tt> - The HTTP response code, as an integer, to return with the response.
|
||||
# * <tt>response_headers</tt> - Headers to be returned with the response. Uses the same hash format as
|
||||
# <tt>request_headers</tt> listed above.
|
||||
#
|
||||
# In order for a mock to deliver its content, the incoming request must match by the <tt>http_method</tt>,
|
||||
# +path+ and <tt>request_headers</tt>. If no match is found an InvalidRequestError exception
|
||||
# will be raised showing you what request it could not find a response for and also what requests and response
|
||||
# pairs have been recorded so you can create a new mock for that request.
|
||||
#
|
||||
# ==== Example
|
||||
# def setup
|
||||
# @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
# ActiveResource::HttpMock.respond_to do |mock|
|
||||
# mock.post "/people.xml", {}, @matz, 201, "Location" => "/people/1.xml"
|
||||
# mock.get "/people/1.xml", {}, @matz
|
||||
# mock.put "/people/1.xml", {}, nil, 204
|
||||
# mock.delete "/people/1.xml", {}, nil, 200
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def test_get_matz
|
||||
# person = Person.find(1)
|
||||
# assert_equal "Matz", person.name
|
||||
# end
|
||||
#
|
||||
class HttpMock
|
||||
class Responder #:nodoc:
|
||||
def initialize(responses)
|
||||
@responses = responses
|
||||
end
|
||||
|
||||
for method in [ :post, :put, :get, :delete, :head ]
|
||||
# def post(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
||||
# @responses[Request.new(:post, path, nil, request_headers)] = Response.new(body || "", status, response_headers)
|
||||
# end
|
||||
module_eval <<-EOE, __FILE__, __LINE__ + 1
|
||||
def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
||||
@responses << [Request.new(:#{method}, path, nil, request_headers), Response.new(body || "", status, response_headers)]
|
||||
end
|
||||
EOE
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
|
||||
# Returns an array of all request objects that have been sent to the mock. You can use this to check
|
||||
# if your model actually sent an HTTP request.
|
||||
#
|
||||
# ==== Example
|
||||
# def setup
|
||||
# @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
# ActiveResource::HttpMock.respond_to do |mock|
|
||||
# mock.get "/people/1.xml", {}, @matz
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def test_should_request_remote_service
|
||||
# person = Person.find(1) # Call the remote service
|
||||
#
|
||||
# # This request object has the same HTTP method and path as declared by the mock
|
||||
# expected_request = ActiveResource::Request.new(:get, "/people/1.xml")
|
||||
#
|
||||
# # Assert that the mock received, and responded to, the expected request from the model
|
||||
# assert ActiveResource::HttpMock.requests.include?(expected_request)
|
||||
# end
|
||||
def requests
|
||||
@@requests ||= []
|
||||
end
|
||||
|
||||
# Returns the list of requests and their mocked responses. Look up a
|
||||
# response for a request using responses.assoc(request).
|
||||
def responses
|
||||
@@responses ||= []
|
||||
end
|
||||
|
||||
# Accepts a block which declares a set of requests and responses for the HttpMock to respond to in
|
||||
# the following format:
|
||||
#
|
||||
# mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
# ActiveResource::HttpMock.respond_to do |mock|
|
||||
# mock.post "/people.xml", {}, @matz, 201, "Location" => "/people/1.xml"
|
||||
# mock.get "/people/1.xml", {}, @matz
|
||||
# mock.put "/people/1.xml", {}, nil, 204
|
||||
# mock.delete "/people/1.xml", {}, nil, 200
|
||||
# end
|
||||
#
|
||||
# Alternatively, accepts a hash of <tt>{Request => Response}</tt> pairs allowing you to generate
|
||||
# these the following format:
|
||||
#
|
||||
# ActiveResource::Request.new(method, path, body, request_headers)
|
||||
# ActiveResource::Response.new(body, status, response_headers)
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# Request.new(:#{method}, path, nil, request_headers)
|
||||
#
|
||||
# @matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
#
|
||||
# create_matz = ActiveResource::Request.new(:post, '/people.xml', @matz, {})
|
||||
# created_response = ActiveResource::Response.new("", 201, {"Location" => "/people/1.xml"})
|
||||
# get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil)
|
||||
# ok_response = ActiveResource::Response.new("", 200, {})
|
||||
#
|
||||
# pairs = {create_matz => created_response, get_matz => ok_response}
|
||||
#
|
||||
# ActiveResource::HttpMock.respond_to(pairs)
|
||||
#
|
||||
# Note, by default, every time you call +respond_to+, any previous request and response pairs stored
|
||||
# in HttpMock will be deleted giving you a clean slate to work on.
|
||||
#
|
||||
# If you want to override this behaviour, pass in +false+ as the last argument to +respond_to+
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# ActiveResource::HttpMock.respond_to do |mock|
|
||||
# mock.send(:get, "/people/1", {}, "XML1")
|
||||
# end
|
||||
# ActiveResource::HttpMock.responses.length #=> 1
|
||||
#
|
||||
# ActiveResource::HttpMock.respond_to(false) do |mock|
|
||||
# mock.send(:get, "/people/2", {}, "XML2")
|
||||
# end
|
||||
# ActiveResource::HttpMock.responses.length #=> 2
|
||||
#
|
||||
# This also works with passing in generated pairs of requests and responses, again, just pass in false
|
||||
# as the last argument:
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# ActiveResource::HttpMock.respond_to do |mock|
|
||||
# mock.send(:get, "/people/1", {}, "XML1")
|
||||
# end
|
||||
# ActiveResource::HttpMock.responses.length #=> 1
|
||||
#
|
||||
# get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil)
|
||||
# ok_response = ActiveResource::Response.new("", 200, {})
|
||||
#
|
||||
# pairs = {get_matz => ok_response}
|
||||
#
|
||||
# ActiveResource::HttpMock.respond_to(pairs, false)
|
||||
# ActiveResource::HttpMock.responses.length #=> 2
|
||||
def respond_to(*args) #:yields: mock
|
||||
pairs = args.first || {}
|
||||
reset! if args.last.class != FalseClass
|
||||
responses.concat pairs.to_a
|
||||
if block_given?
|
||||
yield Responder.new(responses)
|
||||
else
|
||||
Responder.new(responses)
|
||||
end
|
||||
end
|
||||
|
||||
# Deletes all logged requests and responses.
|
||||
def reset!
|
||||
requests.clear
|
||||
responses.clear
|
||||
end
|
||||
end
|
||||
|
||||
# body? methods
|
||||
{ true => %w(post put),
|
||||
false => %w(get delete head) }.each do |has_body, methods|
|
||||
methods.each do |method|
|
||||
# def post(path, body, headers)
|
||||
# request = ActiveResource::Request.new(:post, path, body, headers)
|
||||
# self.class.requests << request
|
||||
# if response = self.class.responses.assoc(request)
|
||||
# response[1]
|
||||
# else
|
||||
# raise InvalidRequestError.new("Could not find a response recorded for #{request.to_s} - Responses recorded are: - #{inspect_responses}")
|
||||
# end
|
||||
# end
|
||||
module_eval <<-EOE, __FILE__, __LINE__ + 1
|
||||
def #{method}(path, #{'body, ' if has_body}headers)
|
||||
request = ActiveResource::Request.new(:#{method}, path, #{has_body ? 'body, ' : 'nil, '}headers)
|
||||
self.class.requests << request
|
||||
if response = self.class.responses.assoc(request)
|
||||
response[1]
|
||||
else
|
||||
raise InvalidRequestError.new("Could not find a response recorded for \#{request.to_s} - Responses recorded are: \#{inspect_responses}")
|
||||
end
|
||||
end
|
||||
EOE
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(site) #:nodoc:
|
||||
@site = site
|
||||
end
|
||||
|
||||
def inspect_responses #:nodoc:
|
||||
self.class.responses.map { |r| r[0].to_s }.inspect
|
||||
end
|
||||
end
|
||||
|
||||
class Request
|
||||
attr_accessor :path, :method, :body, :headers
|
||||
|
||||
def initialize(method, path, body = nil, headers = {})
|
||||
@method, @path, @body, @headers = method, path, body, headers
|
||||
end
|
||||
|
||||
def ==(req)
|
||||
path == req.path && method == req.method && headers_match?(req)
|
||||
end
|
||||
|
||||
def to_s
|
||||
"<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def headers_match?(req)
|
||||
# Ignore format header on equality if it's not defined
|
||||
format_header = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[method]
|
||||
if headers[format_header].present? || req.headers[format_header].blank?
|
||||
headers == req.headers
|
||||
else
|
||||
headers.dup.merge(format_header => req.headers[format_header]) == req.headers
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class Response
|
||||
attr_accessor :body, :message, :code, :headers
|
||||
|
||||
def initialize(body, message = 200, headers = {})
|
||||
@body, @message, @headers = body, message.to_s, headers
|
||||
@code = @message[0,3].to_i
|
||||
|
||||
resp_cls = Net::HTTPResponse::CODE_TO_OBJ[@code.to_s]
|
||||
if resp_cls && !resp_cls.body_permitted?
|
||||
@body = nil
|
||||
end
|
||||
|
||||
if @body.nil?
|
||||
self['Content-Length'] = "0"
|
||||
else
|
||||
self['Content-Length'] = body.size.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def success?
|
||||
(200..299).include?(code)
|
||||
end
|
||||
|
||||
def [](key)
|
||||
headers[key]
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
headers[key] = value
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
if (other.is_a?(Response))
|
||||
other.body == body && other.message == message && other.headers == headers
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Connection
|
||||
private
|
||||
silence_warnings do
|
||||
def http
|
||||
@http ||= HttpMock.new(@site)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,290 +0,0 @@
|
||||
module ActiveResource
|
||||
class ResourceInvalid < ClientError #:nodoc:
|
||||
end
|
||||
|
||||
# Active Resource validation is reported to and from this object, which is used by Base#save
|
||||
# to determine whether the object in a valid state to be saved. See usage example in Validations.
|
||||
class Errors
|
||||
include Enumerable
|
||||
attr_reader :errors
|
||||
|
||||
delegate :empty?, :to => :errors
|
||||
|
||||
def initialize(base) # :nodoc:
|
||||
@base, @errors = base, {}
|
||||
end
|
||||
|
||||
# Add an error to the base Active Resource object rather than an attribute.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_folder = Folder.find(1)
|
||||
# my_folder.errors.add_to_base("You can't edit an existing folder")
|
||||
# my_folder.errors.on_base
|
||||
# # => "You can't edit an existing folder"
|
||||
#
|
||||
# my_folder.errors.add_to_base("This folder has been tagged as frozen")
|
||||
# my_folder.valid?
|
||||
# # => false
|
||||
# my_folder.errors.on_base
|
||||
# # => ["You can't edit an existing folder", "This folder has been tagged as frozen"]
|
||||
#
|
||||
def add_to_base(msg)
|
||||
add(:base, msg)
|
||||
end
|
||||
|
||||
# Adds an error to an Active Resource object's attribute (named for the +attribute+ parameter)
|
||||
# with the error message in +msg+.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_resource = Node.find(1)
|
||||
# my_resource.errors.add('name', 'can not be "base"') if my_resource.name == 'base'
|
||||
# my_resource.errors.on('name')
|
||||
# # => 'can not be "base"!'
|
||||
#
|
||||
# my_resource.errors.add('desc', 'can not be blank') if my_resource.desc == ''
|
||||
# my_resource.valid?
|
||||
# # => false
|
||||
# my_resource.errors.on('desc')
|
||||
# # => 'can not be blank!'
|
||||
#
|
||||
def add(attribute, msg)
|
||||
@errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
|
||||
@errors[attribute.to_s] << msg
|
||||
end
|
||||
|
||||
# Returns true if the specified +attribute+ has errors associated with it.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_resource = Disk.find(1)
|
||||
# my_resource.errors.add('location', 'must be Main') unless my_resource.location == 'Main'
|
||||
# my_resource.errors.on('location')
|
||||
# # => 'must be Main!'
|
||||
#
|
||||
# my_resource.errors.invalid?('location')
|
||||
# # => true
|
||||
# my_resource.errors.invalid?('name')
|
||||
# # => false
|
||||
def invalid?(attribute)
|
||||
!@errors[attribute.to_s].nil?
|
||||
end
|
||||
|
||||
# A method to return the errors associated with +attribute+, which returns nil, if no errors are
|
||||
# associated with the specified +attribute+, the error message if one error is associated with the specified +attribute+,
|
||||
# or an array of error messages if more than one error is associated with the specified +attribute+.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_person = Person.new(params[:person])
|
||||
# my_person.errors.on('login')
|
||||
# # => nil
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||
# my_person.errors.on('login')
|
||||
# # => 'can not be empty'
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be longer than 10 characters') if my_person.login.length > 10
|
||||
# my_person.errors.on('login')
|
||||
# # => ['can not be empty', 'can not be longer than 10 characters']
|
||||
def on(attribute)
|
||||
errors = @errors[attribute.to_s]
|
||||
return nil if errors.nil?
|
||||
errors.size == 1 ? errors.first : errors
|
||||
end
|
||||
|
||||
alias :[] :on
|
||||
|
||||
# A method to return errors assigned to +base+ object through add_to_base, which returns nil, if no errors are
|
||||
# associated with the specified +attribute+, the error message if one error is associated with the specified +attribute+,
|
||||
# or an array of error messages if more than one error is associated with the specified +attribute+.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_account = Account.find(1)
|
||||
# my_account.errors.on_base
|
||||
# # => nil
|
||||
#
|
||||
# my_account.errors.add_to_base("This account is frozen")
|
||||
# my_account.errors.on_base
|
||||
# # => "This account is frozen"
|
||||
#
|
||||
# my_account.errors.add_to_base("This account has been closed")
|
||||
# my_account.errors.on_base
|
||||
# # => ["This account is frozen", "This account has been closed"]
|
||||
#
|
||||
def on_base
|
||||
on(:base)
|
||||
end
|
||||
|
||||
# Yields each attribute and associated message per error added.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_person = Person.new(params[:person])
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||
# my_person.errors.add('password', 'can not be empty') if my_person.password == ''
|
||||
# messages = ''
|
||||
# my_person.errors.each {|attr, msg| messages += attr.humanize + " " + msg + "<br />"}
|
||||
# messages
|
||||
# # => "Login can not be empty<br />Password can not be empty<br />"
|
||||
#
|
||||
def each
|
||||
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
|
||||
end
|
||||
|
||||
# Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
|
||||
# through iteration as "First name can't be empty".
|
||||
#
|
||||
# ==== Examples
|
||||
# my_person = Person.new(params[:person])
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||
# my_person.errors.add('password', 'can not be empty') if my_person.password == ''
|
||||
# messages = ''
|
||||
# my_person.errors.each_full {|msg| messages += msg + "<br/>"}
|
||||
# messages
|
||||
# # => "Login can not be empty<br />Password can not be empty<br />"
|
||||
#
|
||||
def each_full
|
||||
full_messages.each { |msg| yield msg }
|
||||
end
|
||||
|
||||
# Returns all the full error messages in an array.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_person = Person.new(params[:person])
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||
# my_person.errors.add('password', 'can not be empty') if my_person.password == ''
|
||||
# messages = ''
|
||||
# my_person.errors.full_messages.each {|msg| messages += msg + "<br/>"}
|
||||
# messages
|
||||
# # => "Login can not be empty<br />Password can not be empty<br />"
|
||||
#
|
||||
def full_messages
|
||||
full_messages = []
|
||||
|
||||
@errors.each_key do |attr|
|
||||
@errors[attr].each do |msg|
|
||||
next if msg.nil?
|
||||
|
||||
if attr == "base"
|
||||
full_messages << msg
|
||||
else
|
||||
full_messages << [attr.humanize, msg].join(' ')
|
||||
end
|
||||
end
|
||||
end
|
||||
full_messages
|
||||
end
|
||||
|
||||
def clear
|
||||
@errors = {}
|
||||
end
|
||||
|
||||
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such
|
||||
# with this as well.
|
||||
#
|
||||
# ==== Examples
|
||||
# my_person = Person.new(params[:person])
|
||||
# my_person.errors.size
|
||||
# # => 0
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||
# my_person.errors.add('password', 'can not be empty') if my_person.password == ''
|
||||
# my_person.error.size
|
||||
# # => 2
|
||||
#
|
||||
def size
|
||||
@errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
|
||||
end
|
||||
|
||||
alias_method :count, :size
|
||||
alias_method :length, :size
|
||||
|
||||
# Grabs errors from an array of messages (like ActiveRecord::Validations)
|
||||
def from_array(messages)
|
||||
clear
|
||||
humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) }
|
||||
messages.each do |message|
|
||||
attr_message = humanized_attributes.keys.detect do |attr_name|
|
||||
if message[0, attr_name.size + 1] == "#{attr_name} "
|
||||
add humanized_attributes[attr_name], message[(attr_name.size + 1)..-1]
|
||||
end
|
||||
end
|
||||
|
||||
add_to_base message if attr_message.nil?
|
||||
end
|
||||
end
|
||||
|
||||
# Grabs errors from the json response.
|
||||
def from_json(json)
|
||||
array = ActiveSupport::JSON.decode(json)['errors'] rescue []
|
||||
from_array array
|
||||
end
|
||||
|
||||
# Grabs errors from the XML response.
|
||||
def from_xml(xml)
|
||||
array = Array.wrap(Hash.from_xml(xml)['errors']['error']) rescue []
|
||||
from_array array
|
||||
end
|
||||
end
|
||||
|
||||
# Module to support validation and errors with Active Resource objects. The module overrides
|
||||
# Base#save to rescue ActiveResource::ResourceInvalid exceptions and parse the errors returned
|
||||
# in the web service response. The module also adds an +errors+ collection that mimics the interface
|
||||
# of the errors provided by ActiveRecord::Errors.
|
||||
#
|
||||
# ==== Example
|
||||
#
|
||||
# Consider a Person resource on the server requiring both a +first_name+ and a +last_name+ with a
|
||||
# <tt>validates_presence_of :first_name, :last_name</tt> declaration in the model:
|
||||
#
|
||||
# person = Person.new(:first_name => "Jim", :last_name => "")
|
||||
# person.save # => false (server returns an HTTP 422 status code and errors)
|
||||
# person.valid? # => false
|
||||
# person.errors.empty? # => false
|
||||
# person.errors.count # => 1
|
||||
# person.errors.full_messages # => ["Last name can't be empty"]
|
||||
# person.errors.on(:last_name) # => "can't be empty"
|
||||
# person.last_name = "Halpert"
|
||||
# person.save # => true (and person is now saved to the remote service)
|
||||
#
|
||||
module Validations
|
||||
def self.included(base) # :nodoc:
|
||||
base.class_eval do
|
||||
alias_method_chain :save, :validation
|
||||
end
|
||||
end
|
||||
|
||||
# Validate a resource and save (POST) it to the remote web service.
|
||||
def save_with_validation
|
||||
save_without_validation
|
||||
true
|
||||
rescue ResourceInvalid => error
|
||||
case self.class.format
|
||||
when ActiveResource::Formats[:xml]
|
||||
errors.from_xml(error.response.body)
|
||||
when ActiveResource::Formats[:json]
|
||||
errors.from_json(error.response.body)
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Checks for errors on an object (i.e., is resource.errors empty?).
|
||||
#
|
||||
# ==== Examples
|
||||
# my_person = Person.create(params[:person])
|
||||
# my_person.valid?
|
||||
# # => true
|
||||
#
|
||||
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||
# my_person.valid?
|
||||
# # => false
|
||||
def valid?
|
||||
errors.empty?
|
||||
end
|
||||
|
||||
# Returns the Errors object that holds all information about attribute error messages.
|
||||
def errors
|
||||
@errors ||= Errors.new(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
module ActiveResource
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 14
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
require 'active_resource'
|
||||
ActiveSupport::Deprecation.warn 'require "activeresource" is deprecated and will be removed in Rails 3. Use require "active_resource" instead.'
|
||||
@@ -1,21 +0,0 @@
|
||||
require 'rubygems'
|
||||
require 'test/unit'
|
||||
require 'active_support/test_case'
|
||||
|
||||
$:.unshift File.expand_path('../../lib', __FILE__)
|
||||
$:.unshift File.expand_path('../../../activesupport/lib', __FILE__)
|
||||
require 'active_resource'
|
||||
require 'active_resource/http_mock'
|
||||
|
||||
$:.unshift "#{File.dirname(__FILE__)}/../test"
|
||||
require 'setter_trap'
|
||||
|
||||
ActiveResource::Base.logger = Logger.new("#{File.dirname(__FILE__)}/debug.log")
|
||||
|
||||
def uses_gem(gem_name, test_name, version = '> 0')
|
||||
gem gem_name.to_s, version
|
||||
require gem_name.to_s
|
||||
yield
|
||||
rescue LoadError
|
||||
$stderr.puts "Skipping #{test_name} tests. `gem install #{gem_name}` and try again."
|
||||
end
|
||||
@@ -1,122 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
class AuthorizationTest < Test::Unit::TestCase
|
||||
Response = Struct.new(:code)
|
||||
|
||||
def setup
|
||||
@conn = ActiveResource::Connection.new('http://localhost')
|
||||
@matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
|
||||
@david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
|
||||
@authenticated_conn = ActiveResource::Connection.new("http://david:test123@localhost")
|
||||
@authorization_request_header = { 'Authorization' => 'Basic ZGF2aWQ6dGVzdDEyMw==' }
|
||||
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/2.xml", @authorization_request_header, @david
|
||||
mock.put "/people/2.xml", @authorization_request_header, nil, 204
|
||||
mock.delete "/people/2.xml", @authorization_request_header, nil, 200
|
||||
mock.post "/people/2/addresses.xml", @authorization_request_header, nil, 201, 'Location' => '/people/1/addresses/5'
|
||||
end
|
||||
end
|
||||
|
||||
def test_authorization_header
|
||||
authorization_header = @authenticated_conn.__send__(:authorization_header)
|
||||
assert_equal @authorization_request_header['Authorization'], authorization_header['Authorization']
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["david", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_with_username_but_no_password
|
||||
@conn = ActiveResource::Connection.new("http://david:@localhost")
|
||||
authorization_header = @conn.__send__(:authorization_header)
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["david"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_with_password_but_no_username
|
||||
@conn = ActiveResource::Connection.new("http://:test123@localhost")
|
||||
authorization_header = @conn.__send__(:authorization_header)
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_with_decoded_credentials_from_url
|
||||
@conn = ActiveResource::Connection.new("http://my%40email.com:%31%32%33@localhost")
|
||||
authorization_header = @conn.__send__(:authorization_header)
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["my@email.com", "123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_explicitly_setting_username_and_password
|
||||
@authenticated_conn = ActiveResource::Connection.new("http://@localhost")
|
||||
@authenticated_conn.user = 'david'
|
||||
@authenticated_conn.password = 'test123'
|
||||
authorization_header = @authenticated_conn.__send__(:authorization_header)
|
||||
assert_equal @authorization_request_header['Authorization'], authorization_header['Authorization']
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["david", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_explicitly_setting_username_but_no_password
|
||||
@conn = ActiveResource::Connection.new("http://@localhost")
|
||||
@conn.user = "david"
|
||||
authorization_header = @conn.__send__(:authorization_header)
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["david"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_authorization_header_explicitly_setting_password_but_no_username
|
||||
@conn = ActiveResource::Connection.new("http://@localhost")
|
||||
@conn.password = "test123"
|
||||
authorization_header = @conn.__send__(:authorization_header)
|
||||
authorization = authorization_header["Authorization"].to_s.split
|
||||
|
||||
assert_equal "Basic", authorization[0]
|
||||
assert_equal ["", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
|
||||
end
|
||||
|
||||
def test_get
|
||||
david = @authenticated_conn.get("/people/2.xml")
|
||||
assert_equal "David", david["name"]
|
||||
end
|
||||
|
||||
def test_post
|
||||
response = @authenticated_conn.post("/people/2/addresses.xml")
|
||||
assert_equal "/people/1/addresses/5", response["Location"]
|
||||
end
|
||||
|
||||
def test_put
|
||||
response = @authenticated_conn.put("/people/2.xml")
|
||||
assert_equal 204, response.code
|
||||
end
|
||||
|
||||
def test_delete
|
||||
response = @authenticated_conn.delete("/people/2.xml")
|
||||
assert_equal 200, response.code
|
||||
end
|
||||
|
||||
def test_raises_invalid_request_on_unauthorized_requests
|
||||
assert_raise(ActiveResource::InvalidRequestError) { @conn.post("/people/2.xml") }
|
||||
assert_raise(ActiveResource::InvalidRequestError) { @conn.post("/people/2/addresses.xml") }
|
||||
assert_raise(ActiveResource::InvalidRequestError) { @conn.put("/people/2.xml") }
|
||||
assert_raise(ActiveResource::InvalidRequestError) { @conn.delete("/people/2.xml") }
|
||||
end
|
||||
|
||||
protected
|
||||
def assert_response_raises(klass, code)
|
||||
assert_raise(klass, "Expected response code #{code} to raise #{klass}") do
|
||||
@conn.__send__(:handle_response, Response.new(code))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,100 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
require 'fixtures/person'
|
||||
require 'fixtures/street_address'
|
||||
|
||||
class CustomMethodsTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
|
||||
@matz_deep = { :id => 1, :name => 'Matz', :other => 'other' }.to_xml(:root => 'person')
|
||||
@matz_array = [{ :id => 1, :name => 'Matz' }].to_xml(:root => 'people')
|
||||
@ryan = { :name => 'Ryan' }.to_xml(:root => 'person')
|
||||
@addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address')
|
||||
@addy_deep = { :id => 1, :street => '12345 Street', :zip => "27519" }.to_xml(:root => 'address')
|
||||
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/1.xml", {}, @matz
|
||||
mock.get "/people/1/shallow.xml", {}, @matz
|
||||
mock.get "/people/1/deep.xml", {}, @matz_deep
|
||||
mock.get "/people/retrieve.xml?name=Matz", {}, @matz_array
|
||||
mock.get "/people/managers.xml", {}, @matz_array
|
||||
mock.post "/people/hire.xml?name=Matz", {}, nil, 201
|
||||
mock.put "/people/1/promote.xml?position=Manager", {}, nil, 204
|
||||
mock.put "/people/promote.xml?name=Matz", {}, nil, 204, {}
|
||||
mock.put "/people/sort.xml?by=name", {}, nil, 204
|
||||
mock.delete "/people/deactivate.xml?name=Matz", {}, nil, 200
|
||||
mock.delete "/people/1/deactivate.xml", {}, nil, 200
|
||||
mock.post "/people/new/register.xml", {}, @ryan, 201, 'Location' => '/people/5.xml'
|
||||
mock.post "/people/1/register.xml", {}, @matz, 201
|
||||
mock.get "/people/1/addresses/1.xml", {}, @addy
|
||||
mock.get "/people/1/addresses/1/deep.xml", {}, @addy_deep
|
||||
mock.put "/people/1/addresses/1/normalize_phone.xml?locale=US", {}, nil, 204
|
||||
mock.put "/people/1/addresses/sort.xml?by=name", {}, nil, 204
|
||||
mock.post "/people/1/addresses/new/link.xml", {}, { :street => '12345 Street' }.to_xml(:root => 'address'), 201, 'Location' => '/people/1/addresses/2.xml'
|
||||
end
|
||||
|
||||
Person.user = nil
|
||||
Person.password = nil
|
||||
end
|
||||
|
||||
def teardown
|
||||
ActiveResource::HttpMock.reset!
|
||||
end
|
||||
|
||||
def test_custom_collection_method
|
||||
# GET
|
||||
assert_equal([{ "id" => 1, "name" => 'Matz' }], Person.get(:retrieve, :name => 'Matz'))
|
||||
|
||||
# POST
|
||||
assert_equal(ActiveResource::Response.new("", 201, {}), Person.post(:hire, :name => 'Matz'))
|
||||
|
||||
# PUT
|
||||
assert_equal ActiveResource::Response.new("", 204, {}),
|
||||
Person.put(:promote, {:name => 'Matz'}, 'atestbody')
|
||||
assert_equal ActiveResource::Response.new("", 204, {}), Person.put(:sort, :by => 'name')
|
||||
|
||||
# DELETE
|
||||
Person.delete :deactivate, :name => 'Matz'
|
||||
|
||||
# Nested resource
|
||||
assert_equal ActiveResource::Response.new("", 204, {}), StreetAddress.put(:sort, :person_id => 1, :by => 'name')
|
||||
end
|
||||
|
||||
def test_custom_element_method
|
||||
# Test GET against an element URL
|
||||
assert_equal Person.find(1).get(:shallow), {"id" => 1, "name" => 'Matz'}
|
||||
assert_equal Person.find(1).get(:deep), {"id" => 1, "name" => 'Matz', "other" => 'other'}
|
||||
|
||||
# Test PUT against an element URL
|
||||
assert_equal ActiveResource::Response.new("", 204, {}), Person.find(1).put(:promote, {:position => 'Manager'}, 'body')
|
||||
|
||||
# Test DELETE against an element URL
|
||||
assert_equal ActiveResource::Response.new("", 200, {}), Person.find(1).delete(:deactivate)
|
||||
|
||||
# With nested resources
|
||||
assert_equal StreetAddress.find(1, :params => { :person_id => 1 }).get(:deep),
|
||||
{ "id" => 1, "street" => '12345 Street', "zip" => "27519" }
|
||||
assert_equal ActiveResource::Response.new("", 204, {}),
|
||||
StreetAddress.find(1, :params => { :person_id => 1 }).put(:normalize_phone, :locale => 'US')
|
||||
end
|
||||
|
||||
def test_custom_new_element_method
|
||||
# Test POST against a new element URL
|
||||
ryan = Person.new(:name => 'Ryan')
|
||||
assert_equal ActiveResource::Response.new(@ryan, 201, {'Location' => '/people/5.xml'}), ryan.post(:register)
|
||||
expected_request = ActiveResource::Request.new(:post, '/people/new/register.xml', @ryan)
|
||||
assert_equal expected_request.body, ActiveResource::HttpMock.requests.first.body
|
||||
|
||||
# Test POST against a nested collection URL
|
||||
addy = StreetAddress.new(:street => '123 Test Dr.', :person_id => 1)
|
||||
assert_equal ActiveResource::Response.new({ :street => '12345 Street' }.to_xml(:root => 'address'),
|
||||
201, {'Location' => '/people/1/addresses/2.xml'}),
|
||||
addy.post(:link)
|
||||
|
||||
matz = Person.new(:id => 1, :name => 'Matz')
|
||||
assert_equal ActiveResource::Response.new(@matz, 201), matz.post(:register)
|
||||
end
|
||||
|
||||
def test_find_custom_resources
|
||||
assert_equal 'Matz', Person.find(:all, :from => :managers).first.name
|
||||
end
|
||||
end
|
||||
@@ -1,52 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
require "fixtures/person"
|
||||
require "fixtures/street_address"
|
||||
|
||||
class BaseEqualityTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@new = Person.new
|
||||
@one = Person.new(:id => 1)
|
||||
@two = Person.new(:id => 2)
|
||||
@street = StreetAddress.new(:id => 2)
|
||||
end
|
||||
|
||||
def test_should_equal_self
|
||||
assert @new == @new, '@new == @new'
|
||||
assert @one == @one, '@one == @one'
|
||||
end
|
||||
|
||||
def test_shouldnt_equal_new_resource
|
||||
assert @new != @one, '@new != @one'
|
||||
assert @one != @new, '@one != @new'
|
||||
end
|
||||
|
||||
def test_shouldnt_equal_different_class
|
||||
assert @two != @street, 'person != street_address with same id'
|
||||
assert @street != @two, 'street_address != person with same id'
|
||||
end
|
||||
|
||||
def test_eql_should_alias_equals_operator
|
||||
assert_equal @new == @new, @new.eql?(@new)
|
||||
assert_equal @new == @one, @new.eql?(@one)
|
||||
|
||||
assert_equal @one == @one, @one.eql?(@one)
|
||||
assert_equal @one == @new, @one.eql?(@new)
|
||||
|
||||
assert_equal @one == @street, @one.eql?(@street)
|
||||
end
|
||||
|
||||
def test_hash_should_be_id_hash
|
||||
[@new, @one, @two, @street].each do |resource|
|
||||
assert_equal resource.id.hash, resource.hash
|
||||
end
|
||||
end
|
||||
|
||||
def test_with_prefix_options
|
||||
assert_equal @one == @one, @one.eql?(@one)
|
||||
assert_equal @one == @one.dup, @one.eql?(@one.dup)
|
||||
new_one = @one.dup
|
||||
new_one.prefix_options = {:foo => 'bar'}
|
||||
assert_not_equal @one, new_one
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,161 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
require "fixtures/person"
|
||||
require "fixtures/street_address"
|
||||
|
||||
module Highrise
|
||||
class Note < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
|
||||
class Comment < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
|
||||
module Deeply
|
||||
module Nested
|
||||
|
||||
class Note < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
|
||||
class Comment < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
|
||||
module TestDifferentLevels
|
||||
|
||||
class Note < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class BaseLoadTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@matz = { :id => 1, :name => 'Matz' }
|
||||
|
||||
@first_address = { :id => 1, :street => '12345 Street' }
|
||||
@addresses = [@first_address, { :id => 2, :street => '67890 Street' }]
|
||||
@addresses_from_xml = { :street_addresses => @addresses }
|
||||
@addresses_from_xml_single = { :street_addresses => [ @first_address ] }
|
||||
|
||||
@deep = { :id => 1, :street => {
|
||||
:id => 1, :state => { :id => 1, :name => 'Oregon',
|
||||
:notable_rivers => [
|
||||
{ :id => 1, :name => 'Willamette' },
|
||||
{ :id => 2, :name => 'Columbia', :rafted_by => @matz }],
|
||||
:postal_codes => [ 97018, 1234567890 ],
|
||||
:places => [ "Columbia City", "Unknown" ]}}}
|
||||
|
||||
@person = Person.new
|
||||
end
|
||||
|
||||
def test_load_expects_hash
|
||||
assert_raise(ArgumentError) { @person.load nil }
|
||||
assert_raise(ArgumentError) { @person.load '<person id="1"/>' }
|
||||
end
|
||||
|
||||
def test_load_simple_hash
|
||||
assert_equal Hash.new, @person.attributes
|
||||
assert_equal @matz.stringify_keys, @person.load(@matz).attributes
|
||||
end
|
||||
|
||||
def test_load_one_with_existing_resource
|
||||
address = @person.load(:street_address => @first_address).street_address
|
||||
assert_kind_of StreetAddress, address
|
||||
assert_equal @first_address.stringify_keys, address.attributes
|
||||
end
|
||||
|
||||
def test_load_one_with_unknown_resource
|
||||
address = silence_warnings { @person.load(:address => @first_address).address }
|
||||
assert_kind_of Person::Address, address
|
||||
assert_equal @first_address.stringify_keys, address.attributes
|
||||
end
|
||||
|
||||
def test_load_collection_with_existing_resource
|
||||
addresses = @person.load(@addresses_from_xml).street_addresses
|
||||
assert_kind_of Array, addresses
|
||||
addresses.each { |address| assert_kind_of StreetAddress, address }
|
||||
assert_equal @addresses.map(&:stringify_keys), addresses.map(&:attributes)
|
||||
end
|
||||
|
||||
def test_load_collection_with_unknown_resource
|
||||
Person.__send__(:remove_const, :Address) if Person.const_defined?(:Address)
|
||||
assert !Person.const_defined?(:Address), "Address shouldn't exist until autocreated"
|
||||
addresses = silence_warnings { @person.load(:addresses => @addresses).addresses }
|
||||
assert Person.const_defined?(:Address), "Address should have been autocreated"
|
||||
addresses.each { |address| assert_kind_of Person::Address, address }
|
||||
assert_equal @addresses.map(&:stringify_keys), addresses.map(&:attributes)
|
||||
end
|
||||
|
||||
def test_load_collection_with_single_existing_resource
|
||||
addresses = @person.load(@addresses_from_xml_single).street_addresses
|
||||
assert_kind_of Array, addresses
|
||||
addresses.each { |address| assert_kind_of StreetAddress, address }
|
||||
assert_equal [ @first_address ].map(&:stringify_keys), addresses.map(&:attributes)
|
||||
end
|
||||
|
||||
def test_load_collection_with_single_unknown_resource
|
||||
Person.__send__(:remove_const, :Address) if Person.const_defined?(:Address)
|
||||
assert !Person.const_defined?(:Address), "Address shouldn't exist until autocreated"
|
||||
addresses = silence_warnings { @person.load(:addresses => [ @first_address ]).addresses }
|
||||
assert Person.const_defined?(:Address), "Address should have been autocreated"
|
||||
addresses.each { |address| assert_kind_of Person::Address, address }
|
||||
assert_equal [ @first_address ].map(&:stringify_keys), addresses.map(&:attributes)
|
||||
end
|
||||
|
||||
def test_recursively_loaded_collections
|
||||
person = @person.load(@deep)
|
||||
assert_equal @deep[:id], person.id
|
||||
|
||||
street = person.street
|
||||
assert_kind_of Person::Street, street
|
||||
assert_equal @deep[:street][:id], street.id
|
||||
|
||||
state = street.state
|
||||
assert_kind_of Person::Street::State, state
|
||||
assert_equal @deep[:street][:state][:id], state.id
|
||||
|
||||
rivers = state.notable_rivers
|
||||
assert_kind_of Array, rivers
|
||||
assert_kind_of Person::Street::State::NotableRiver, rivers.first
|
||||
assert_equal @deep[:street][:state][:notable_rivers].first[:id], rivers.first.id
|
||||
assert_equal @matz[:id], rivers.last.rafted_by.id
|
||||
|
||||
postal_codes = state.postal_codes
|
||||
assert_kind_of Array, postal_codes
|
||||
assert_equal 2, postal_codes.size
|
||||
assert_kind_of Fixnum, postal_codes.first
|
||||
assert_equal @deep[:street][:state][:postal_codes].first, postal_codes.first
|
||||
assert_kind_of Numeric, postal_codes.last
|
||||
assert_equal @deep[:street][:state][:postal_codes].last, postal_codes.last
|
||||
|
||||
places = state.places
|
||||
assert_kind_of Array, places
|
||||
assert_kind_of String, places.first
|
||||
assert_equal @deep[:street][:state][:places].first, places.first
|
||||
end
|
||||
|
||||
def test_nested_collections_within_the_same_namespace
|
||||
n = Highrise::Note.new(:comments => [{ :name => "1" }])
|
||||
assert_kind_of Highrise::Comment, n.comments.first
|
||||
end
|
||||
|
||||
def test_nested_collections_within_deeply_nested_namespace
|
||||
n = Highrise::Deeply::Nested::Note.new(:comments => [{ :name => "1" }])
|
||||
assert_kind_of Highrise::Deeply::Nested::Comment, n.comments.first
|
||||
end
|
||||
|
||||
def test_nested_collections_in_different_levels_of_namespaces
|
||||
n = Highrise::Deeply::Nested::TestDifferentLevels::Note.new(:comments => [{ :name => "1" }])
|
||||
assert_kind_of Highrise::Deeply::Nested::Comment, n.comments.first
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
@@ -1,98 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
require "fixtures/person"
|
||||
|
||||
class BaseErrorsTest < Test::Unit::TestCase
|
||||
def setup
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.post "/people.xml", {}, %q(<?xml version="1.0" encoding="UTF-8"?><errors><error>Age can't be blank</error><error>Name can't be blank</error><error>Name must start with a letter</error><error>Person quota full for today.</error></errors>), 422, {'Content-Type' => 'application/xml; charset=utf-8'}
|
||||
mock.post "/people.json", {}, %q({"errors":["Age can't be blank","Name can't be blank","Name must start with a letter","Person quota full for today."]}), 422, {'Content-Type' => 'application/json; charset=utf-8'}
|
||||
end
|
||||
@person = Person.new(:name => '', :age => '')
|
||||
assert_equal @person.save, false
|
||||
end
|
||||
|
||||
def test_should_mark_as_invalid
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
assert !@person.valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_parse_xml_errors
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
assert_kind_of ActiveResource::Errors, @person.errors
|
||||
assert_equal 4, @person.errors.size
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_parse_errors_to_individual_attributes
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
assert @person.errors[:name].any?
|
||||
assert_equal "can't be blank", @person.errors[:age]
|
||||
assert_equal ["can't be blank", "must start with a letter"], @person.errors[:name]
|
||||
assert_equal "Person quota full for today.", @person.errors[:base]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_iterate_over_errors
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
errors = []
|
||||
@person.errors.each { |attribute, message| errors << [attribute, message] }
|
||||
assert errors.include?(['name', "can't be blank"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_iterate_over_full_errors
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
errors = []
|
||||
@person.errors.to_a.each { |message| errors << message }
|
||||
assert errors.include?(["name", "can't be blank"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_format_full_errors
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
full = @person.errors.full_messages
|
||||
assert full.include?("Age can't be blank")
|
||||
assert full.include?("Name can't be blank")
|
||||
assert full.include?("Name must start with a letter")
|
||||
assert full.include?("Person quota full for today.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_mark_as_invalid_when_content_type_is_unavailable_in_response_header
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.post "/people.xml", {}, %q(<?xml version="1.0" encoding="UTF-8"?><errors><error>Age can't be blank</error><error>Name can't be blank</error><error>Name must start with a letter</error><error>Person quota full for today.</error></errors>), 422, {}
|
||||
mock.post "/people.json", {}, %q({"errors":["Age can't be blank","Name can't be blank","Name must start with a letter","Person quota full for today."]}), 422, {}
|
||||
end
|
||||
|
||||
[ :json, :xml ].each do |format|
|
||||
invalid_user_using_format(format) do
|
||||
assert !@person.valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def invalid_user_using_format(mime_type_reference)
|
||||
previous_format = Person.format
|
||||
Person.format = mime_type_reference
|
||||
@person = Person.new(:name => '', :age => '')
|
||||
assert_equal false, @person.save
|
||||
|
||||
yield
|
||||
ensure
|
||||
Person.format = previous_format
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,238 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
class ConnectionTest < Test::Unit::TestCase
|
||||
ResponseCodeStub = Struct.new(:code)
|
||||
|
||||
def setup
|
||||
@conn = ActiveResource::Connection.new('http://localhost')
|
||||
@matz = { :id => 1, :name => 'Matz' }
|
||||
@david = { :id => 2, :name => 'David' }
|
||||
@people = [ @matz, @david ].to_xml(:root => 'people')
|
||||
@people_single = [ @matz ].to_xml(:root => 'people-single-elements')
|
||||
@people_empty = [ ].to_xml(:root => 'people-empty-elements')
|
||||
@matz = @matz.to_xml(:root => 'person')
|
||||
@david = @david.to_xml(:root => 'person')
|
||||
@header = {'key' => 'value'}.freeze
|
||||
|
||||
@default_request_headers = { 'Content-Type' => 'application/xml' }
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/2.xml", @header, @david
|
||||
mock.get "/people.xml", {}, @people
|
||||
mock.get "/people_single_elements.xml", {}, @people_single
|
||||
mock.get "/people_empty_elements.xml", {}, @people_empty
|
||||
mock.get "/people/1.xml", {}, @matz
|
||||
mock.put "/people/1.xml", {}, nil, 204
|
||||
mock.put "/people/2.xml", {}, @header, 204
|
||||
mock.delete "/people/1.xml", {}, nil, 200
|
||||
mock.delete "/people/2.xml", @header, nil, 200
|
||||
mock.post "/people.xml", {}, nil, 201, 'Location' => '/people/5.xml'
|
||||
mock.post "/members.xml", {}, @header, 201, 'Location' => '/people/6.xml'
|
||||
mock.head "/people/1.xml", {}, nil, 200
|
||||
end
|
||||
end
|
||||
|
||||
def test_handle_response
|
||||
# 2xx and 3xx are valid responses.
|
||||
[200, 299, 300, 399].each do |code|
|
||||
expected = ResponseCodeStub.new(code)
|
||||
assert_equal expected, handle_response(expected)
|
||||
end
|
||||
|
||||
# 400 is a bad request (e.g. malformed URI or missing request parameter)
|
||||
assert_response_raises ActiveResource::BadRequest, 400
|
||||
|
||||
# 401 is an unauthorized request
|
||||
assert_response_raises ActiveResource::UnauthorizedAccess, 401
|
||||
|
||||
# 403 is a forbidden requst (and authorizing will not help)
|
||||
assert_response_raises ActiveResource::ForbiddenAccess, 403
|
||||
|
||||
# 404 is a missing resource.
|
||||
assert_response_raises ActiveResource::ResourceNotFound, 404
|
||||
|
||||
# 405 is a missing not allowed error
|
||||
assert_response_raises ActiveResource::MethodNotAllowed, 405
|
||||
|
||||
# 409 is an optimistic locking error
|
||||
assert_response_raises ActiveResource::ResourceConflict, 409
|
||||
|
||||
# 410 is a removed resource
|
||||
assert_response_raises ActiveResource::ResourceGone, 410
|
||||
|
||||
# 422 is a validation error
|
||||
assert_response_raises ActiveResource::ResourceInvalid, 422
|
||||
|
||||
# 4xx are client errors.
|
||||
[402, 499].each do |code|
|
||||
assert_response_raises ActiveResource::ClientError, code
|
||||
end
|
||||
|
||||
# 5xx are server errors.
|
||||
[500, 599].each do |code|
|
||||
assert_response_raises ActiveResource::ServerError, code
|
||||
end
|
||||
|
||||
# Others are unknown.
|
||||
[199, 600].each do |code|
|
||||
assert_response_raises ActiveResource::ConnectionError, code
|
||||
end
|
||||
end
|
||||
|
||||
ResponseHeaderStub = Struct.new(:code, :message, 'Allow')
|
||||
def test_should_return_allowed_methods_for_method_no_allowed_exception
|
||||
begin
|
||||
handle_response ResponseHeaderStub.new(405, "HTTP Failed...", "GET, POST")
|
||||
rescue ActiveResource::MethodNotAllowed => e
|
||||
assert_equal "Failed with 405 HTTP Failed...", e.message
|
||||
assert_equal [:get, :post], e.allowed_methods
|
||||
end
|
||||
end
|
||||
|
||||
def test_initialize_raises_argument_error_on_missing_site
|
||||
assert_raise(ArgumentError) { ActiveResource::Connection.new(nil) }
|
||||
end
|
||||
|
||||
def test_site_accessor_accepts_uri_or_string_argument
|
||||
site = URI.parse("http://localhost")
|
||||
|
||||
assert_raise(URI::InvalidURIError) { @conn.site = nil }
|
||||
|
||||
assert_nothing_raised { @conn.site = "http://localhost" }
|
||||
assert_equal site, @conn.site
|
||||
|
||||
assert_nothing_raised { @conn.site = site }
|
||||
assert_equal site, @conn.site
|
||||
end
|
||||
|
||||
def test_proxy_accessor_accepts_uri_or_string_argument
|
||||
proxy = URI.parse("http://proxy_user:proxy_password@proxy.local:4242")
|
||||
|
||||
assert_nothing_raised { @conn.proxy = "http://proxy_user:proxy_password@proxy.local:4242" }
|
||||
assert_equal proxy, @conn.proxy
|
||||
|
||||
assert_nothing_raised { @conn.proxy = proxy }
|
||||
assert_equal proxy, @conn.proxy
|
||||
end
|
||||
|
||||
def test_timeout_accessor
|
||||
@conn.timeout = 5
|
||||
assert_equal 5, @conn.timeout
|
||||
end
|
||||
|
||||
def test_get
|
||||
matz = @conn.get("/people/1.xml")
|
||||
assert_equal "Matz", matz["name"]
|
||||
end
|
||||
|
||||
def test_head
|
||||
response = @conn.head("/people/1.xml")
|
||||
assert response.body.blank?
|
||||
assert_equal 200, response.code
|
||||
end
|
||||
|
||||
def test_get_with_header
|
||||
david = @conn.get("/people/2.xml", @header)
|
||||
assert_equal "David", david["name"]
|
||||
end
|
||||
|
||||
def test_get_collection
|
||||
people = @conn.get("/people.xml")
|
||||
assert_equal "Matz", people[0]["name"]
|
||||
assert_equal "David", people[1]["name"]
|
||||
end
|
||||
|
||||
def test_get_collection_single
|
||||
people = @conn.get("/people_single_elements.xml")
|
||||
assert_equal "Matz", people[0]["name"]
|
||||
end
|
||||
|
||||
def test_get_collection_empty
|
||||
people = @conn.get("/people_empty_elements.xml")
|
||||
assert_equal [], people
|
||||
end
|
||||
|
||||
def test_post
|
||||
response = @conn.post("/people.xml")
|
||||
assert_equal "/people/5.xml", response["Location"]
|
||||
end
|
||||
|
||||
def test_post_with_header
|
||||
response = @conn.post("/members.xml", @header)
|
||||
assert_equal "/people/6.xml", response["Location"]
|
||||
end
|
||||
|
||||
def test_put
|
||||
response = @conn.put("/people/1.xml")
|
||||
assert_equal 204, response.code
|
||||
end
|
||||
|
||||
def test_put_with_header
|
||||
response = @conn.put("/people/2.xml", @header)
|
||||
assert_equal 204, response.code
|
||||
end
|
||||
|
||||
def test_delete
|
||||
response = @conn.delete("/people/1.xml")
|
||||
assert_equal 200, response.code
|
||||
end
|
||||
|
||||
def test_delete_with_header
|
||||
response = @conn.delete("/people/2.xml", @header)
|
||||
assert_equal 200, response.code
|
||||
end
|
||||
|
||||
def test_timeout
|
||||
@http = mock('new Net::HTTP')
|
||||
@conn.expects(:http).returns(@http)
|
||||
@http.expects(:get).raises(Timeout::Error, 'execution expired')
|
||||
assert_raise(ActiveResource::TimeoutError) { @conn.get('/people_timeout.xml') }
|
||||
end
|
||||
|
||||
def test_setting_timeout
|
||||
http = Net::HTTP.new('')
|
||||
|
||||
[10, 20].each do |timeout|
|
||||
@conn.timeout = timeout
|
||||
@conn.send(:configure_http, http)
|
||||
assert_equal timeout, http.open_timeout
|
||||
assert_equal timeout, http.read_timeout
|
||||
end
|
||||
end
|
||||
|
||||
def test_accept_http_header
|
||||
@http = mock('new Net::HTTP')
|
||||
@conn.expects(:http).returns(@http)
|
||||
path = '/people/1.xml'
|
||||
@http.expects(:get).with(path, {'Accept' => 'application/xhtml+xml'}).returns(ActiveResource::Response.new(@matz, 200, {'Content-Type' => 'text/xhtml'}))
|
||||
assert_nothing_raised(Mocha::ExpectationError) { @conn.get(path, {'Accept' => 'application/xhtml+xml'}) }
|
||||
end
|
||||
|
||||
def test_ssl_options_get_applied_to_http
|
||||
http = Net::HTTP.new('')
|
||||
@conn.site="https://secure"
|
||||
@conn.ssl_options={:verify_mode => OpenSSL::SSL::VERIFY_PEER}
|
||||
@conn.timeout = 10 # prevent warning about uninitialized.
|
||||
@conn.send(:configure_http, http)
|
||||
|
||||
assert http.use_ssl?
|
||||
assert_equal http.verify_mode, OpenSSL::SSL::VERIFY_PEER
|
||||
end
|
||||
|
||||
def test_ssl_error
|
||||
http = Net::HTTP.new('')
|
||||
@conn.expects(:http).returns(http)
|
||||
http.expects(:get).raises(OpenSSL::SSL::SSLError, 'Expired certificate')
|
||||
assert_raise(ActiveResource::SSLError) { @conn.get('/people/1.xml') }
|
||||
end
|
||||
|
||||
protected
|
||||
def assert_response_raises(klass, code)
|
||||
assert_raise(klass, "Expected response code #{code} to raise #{klass}") do
|
||||
handle_response ResponseCodeStub.new(code)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_response(response)
|
||||
@conn.__send__(:handle_response, response)
|
||||
end
|
||||
end
|
||||
14
activeresource/test/fixtures/beast.rb
vendored
14
activeresource/test/fixtures/beast.rb
vendored
@@ -1,14 +0,0 @@
|
||||
class BeastResource < ActiveResource::Base
|
||||
self.site = 'http://beast.caboo.se'
|
||||
site.user = 'foo'
|
||||
site.password = 'bar'
|
||||
end
|
||||
|
||||
class Forum < BeastResource
|
||||
# taken from BeastResource
|
||||
# self.site = 'http://beast.caboo.se'
|
||||
end
|
||||
|
||||
class Topic < BeastResource
|
||||
self.site += '/forums/:forum_id'
|
||||
end
|
||||
3
activeresource/test/fixtures/customer.rb
vendored
3
activeresource/test/fixtures/customer.rb
vendored
@@ -1,3 +0,0 @@
|
||||
class Customer < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
3
activeresource/test/fixtures/person.rb
vendored
3
activeresource/test/fixtures/person.rb
vendored
@@ -1,3 +0,0 @@
|
||||
class Person < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000"
|
||||
end
|
||||
4
activeresource/test/fixtures/proxy.rb
vendored
4
activeresource/test/fixtures/proxy.rb
vendored
@@ -1,4 +0,0 @@
|
||||
class ProxyResource < ActiveResource::Base
|
||||
self.site = "http://localhost"
|
||||
self.proxy = "http://user:password@proxy.local:3000"
|
||||
end
|
||||
@@ -1,4 +0,0 @@
|
||||
class StreetAddress < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000/people/:person_id/"
|
||||
self.element_name = 'address'
|
||||
end
|
||||
@@ -1,112 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
require "fixtures/person"
|
||||
require "fixtures/street_address"
|
||||
|
||||
class FormatTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@matz = { :id => 1, :name => 'Matz' }
|
||||
@david = { :id => 2, :name => 'David' }
|
||||
|
||||
@programmers = [ @matz, @david ]
|
||||
end
|
||||
|
||||
def test_http_format_header_name
|
||||
header_name = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[:get]
|
||||
assert_equal 'Accept', header_name
|
||||
|
||||
headers_names = [ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[:put], ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[:post]]
|
||||
headers_names.each{ |name| assert_equal 'Content-Type', name }
|
||||
end
|
||||
|
||||
def test_formats_on_single_element
|
||||
for format in [ :json, :xml ]
|
||||
using_format(Person, format) do
|
||||
ActiveResource::HttpMock.respond_to.get "/people/1.#{format}", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode(@david)
|
||||
assert_equal @david[:name], Person.find(1).name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_formats_on_collection
|
||||
for format in [ :json, :xml ]
|
||||
using_format(Person, format) do
|
||||
ActiveResource::HttpMock.respond_to.get "/people.#{format}", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode(@programmers)
|
||||
remote_programmers = Person.find(:all)
|
||||
assert_equal 2, remote_programmers.size
|
||||
assert remote_programmers.select { |p| p.name == 'David' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_formats_on_custom_collection_method
|
||||
for format in [ :json, :xml ]
|
||||
using_format(Person, format) do
|
||||
ActiveResource::HttpMock.respond_to.get "/people/retrieve.#{format}?name=David", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode([@david])
|
||||
remote_programmers = Person.get(:retrieve, :name => 'David')
|
||||
assert_equal 1, remote_programmers.size
|
||||
assert_equal @david[:id], remote_programmers[0]['id']
|
||||
assert_equal @david[:name], remote_programmers[0]['name']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_formats_on_custom_element_method
|
||||
for format in [ :json, :xml ]
|
||||
using_format(Person, format) do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.get "/people/2.#{format}", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode(@david)
|
||||
mock.get "/people/2/shallow.#{format}", {'Accept' => ActiveResource::Formats[format].mime_type}, ActiveResource::Formats[format].encode(@david)
|
||||
end
|
||||
remote_programmer = Person.find(2).get(:shallow)
|
||||
assert_equal @david[:id], remote_programmer['id']
|
||||
assert_equal @david[:name], remote_programmer['name']
|
||||
end
|
||||
end
|
||||
|
||||
for format in [ :json, :xml ]
|
||||
ryan = ActiveResource::Formats[format].encode({ :name => 'Ryan' })
|
||||
using_format(Person, format) do
|
||||
remote_ryan = Person.new(:name => 'Ryan')
|
||||
ActiveResource::HttpMock.respond_to.post "/people.#{format}", {'Content-Type' => ActiveResource::Formats[format].mime_type}, ryan, 201, {'Location' => "/people/5.#{format}"}
|
||||
remote_ryan.save
|
||||
|
||||
remote_ryan = Person.new(:name => 'Ryan')
|
||||
ActiveResource::HttpMock.respond_to.post "/people/new/register.#{format}", {'Content-Type' => ActiveResource::Formats[format].mime_type}, ryan, 201, {'Location' => "/people/5.#{format}"}
|
||||
assert_equal ActiveResource::Response.new(ryan, 201, {'Location' => "/people/5.#{format}"}), remote_ryan.post(:register)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_setting_format_before_site
|
||||
resource = Class.new(ActiveResource::Base)
|
||||
resource.format = :json
|
||||
resource.site = 'http://37s.sunrise.i:3000'
|
||||
assert_equal ActiveResource::Formats[:json], resource.connection.format
|
||||
end
|
||||
|
||||
def test_serialization_of_nested_resource
|
||||
address = { :street => '12345 Street' }
|
||||
person = { :name=> 'Rus', :address => address}
|
||||
|
||||
[:json, :xml].each do |format|
|
||||
encoded_person = ActiveResource::Formats[format].encode(person)
|
||||
assert_match(/12345 Street/, encoded_person)
|
||||
remote_person = Person.new(person.update({:address => StreetAddress.new(address)}))
|
||||
assert_kind_of StreetAddress, remote_person.address
|
||||
using_format(Person, format) do
|
||||
ActiveResource::HttpMock.respond_to.post "/people.#{format}", {'Content-Type' => ActiveResource::Formats[format].mime_type}, encoded_person, 201, {'Location' => "/people/5.#{format}"}
|
||||
remote_person.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def using_format(klass, mime_type_reference)
|
||||
previous_format = klass.format
|
||||
klass.format = mime_type_reference
|
||||
|
||||
yield
|
||||
ensure
|
||||
klass.format = previous_format
|
||||
end
|
||||
end
|
||||
@@ -1,155 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
class HttpMockTest < ActiveSupport::TestCase
|
||||
def setup
|
||||
@http = ActiveResource::HttpMock.new("http://example.com")
|
||||
end
|
||||
|
||||
FORMAT_HEADER = { :get => 'Accept',
|
||||
:put => 'Content-Type',
|
||||
:post => 'Content-Type',
|
||||
:delete => 'Accept',
|
||||
:head => 'Accept'
|
||||
}
|
||||
|
||||
[:post, :put, :get, :delete, :head].each do |method|
|
||||
test "responds to simple #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/xml"}, "Response")
|
||||
end
|
||||
|
||||
assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body
|
||||
end
|
||||
|
||||
test "adds format header by default to #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {}, "Response")
|
||||
end
|
||||
|
||||
assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body
|
||||
end
|
||||
|
||||
test "respond only when headers match header by default to #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {"X-Header" => "X"}, "Response")
|
||||
end
|
||||
|
||||
assert_equal "Response", request(method, "/people/1", "X-Header" => "X").body
|
||||
assert_raise(ActiveResource::InvalidRequestError) { request(method, "/people/1") }
|
||||
end
|
||||
|
||||
test "does not overwrite format header to #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/json"}, "Response")
|
||||
end
|
||||
|
||||
assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/json").body
|
||||
end
|
||||
|
||||
test "ignores format header when there is only one response to same url in a #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {}, "Response")
|
||||
end
|
||||
|
||||
assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/json").body
|
||||
assert_equal "Response", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body
|
||||
end
|
||||
|
||||
test "responds correctly when format header is given to #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/xml"}, "XML")
|
||||
mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/json"}, "Json")
|
||||
end
|
||||
|
||||
assert_equal "XML", request(method, "/people/1", FORMAT_HEADER[method] => "application/xml").body
|
||||
assert_equal "Json", request(method, "/people/1", FORMAT_HEADER[method] => "application/json").body
|
||||
end
|
||||
|
||||
test "raises InvalidRequestError if no response found for the #{method} request" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(method, "/people/1", {FORMAT_HEADER[method] => "application/xml"}, "XML")
|
||||
end
|
||||
|
||||
assert_raise(::ActiveResource::InvalidRequestError) do
|
||||
request(method, "/people/1", FORMAT_HEADER[method] => "application/json")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
test "allows you to send in pairs directly to the respond_to method" do
|
||||
matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
|
||||
create_matz = ActiveResource::Request.new(:post, '/people.xml', matz, {})
|
||||
created_response = ActiveResource::Response.new("", 201, {"Location" => "/people/1.xml"})
|
||||
get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil)
|
||||
ok_response = ActiveResource::Response.new(matz, 200, {})
|
||||
|
||||
pairs = {create_matz => created_response, get_matz => ok_response}
|
||||
|
||||
ActiveResource::HttpMock.respond_to(pairs)
|
||||
assert_equal 2, ActiveResource::HttpMock.responses.length
|
||||
assert_equal "", ActiveResource::HttpMock.responses.assoc(create_matz)[1].body
|
||||
assert_equal matz, ActiveResource::HttpMock.responses.assoc(get_matz)[1].body
|
||||
end
|
||||
|
||||
test "resets all mocked responses on each call to respond_to with a block by default" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(:get, "/people/1", {}, "XML1")
|
||||
end
|
||||
assert_equal 1, ActiveResource::HttpMock.responses.length
|
||||
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(:get, "/people/2", {}, "XML2")
|
||||
end
|
||||
assert_equal 1, ActiveResource::HttpMock.responses.length
|
||||
end
|
||||
|
||||
test "resets all mocked responses on each call to respond_to by passing pairs by default" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(:get, "/people/1", {}, "XML1")
|
||||
end
|
||||
assert_equal 1, ActiveResource::HttpMock.responses.length
|
||||
|
||||
matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil)
|
||||
ok_response = ActiveResource::Response.new(matz, 200, {})
|
||||
ActiveResource::HttpMock.respond_to({get_matz => ok_response})
|
||||
|
||||
assert_equal 1, ActiveResource::HttpMock.responses.length
|
||||
end
|
||||
|
||||
test "allows you to add new responses to the existing responses by calling a block" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(:get, "/people/1", {}, "XML1")
|
||||
end
|
||||
assert_equal 1, ActiveResource::HttpMock.responses.length
|
||||
|
||||
ActiveResource::HttpMock.respond_to(false) do |mock|
|
||||
mock.send(:get, "/people/2", {}, "XML2")
|
||||
end
|
||||
assert_equal 2, ActiveResource::HttpMock.responses.length
|
||||
end
|
||||
|
||||
test "allows you to add new responses to the existing responses by passing pairs" do
|
||||
ActiveResource::HttpMock.respond_to do |mock|
|
||||
mock.send(:get, "/people/1", {}, "XML1")
|
||||
end
|
||||
assert_equal 1, ActiveResource::HttpMock.responses.length
|
||||
|
||||
matz = { :id => 1, :name => "Matz" }.to_xml(:root => "person")
|
||||
get_matz = ActiveResource::Request.new(:get, '/people/1.xml', nil)
|
||||
ok_response = ActiveResource::Response.new(matz, 200, {})
|
||||
ActiveResource::HttpMock.respond_to({get_matz => ok_response}, false)
|
||||
|
||||
assert_equal 2, ActiveResource::HttpMock.responses.length
|
||||
end
|
||||
|
||||
def request(method, path, headers = {}, body = nil)
|
||||
if [:put, :post].include? method
|
||||
@http.send(method, path, body, headers)
|
||||
else
|
||||
@http.send(method, path, headers)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
class SetterTrap < ActiveSupport::BasicObject
|
||||
class << self
|
||||
def rollback_sets(obj)
|
||||
trapped = new(obj)
|
||||
yield(trapped).tap { trapped.rollback_sets }
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(obj)
|
||||
@cache = {}
|
||||
@obj = obj
|
||||
end
|
||||
|
||||
def respond_to?(method)
|
||||
@obj.respond_to?(method)
|
||||
end
|
||||
|
||||
def method_missing(method, *args, &proc)
|
||||
@cache[method] ||= @obj.send($`) if method.to_s =~ /=$/
|
||||
@obj.send method, *args, &proc
|
||||
end
|
||||
|
||||
def rollback_sets
|
||||
@cache.each { |k, v| @obj.send k, v }
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,8 @@
|
||||
version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).chomp
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'activesupport'
|
||||
s.version = '2.3.18'
|
||||
s.version = version
|
||||
s.summary = 'Support and utility classes used by the Rails framework.'
|
||||
s.description = 'Utility library which carries commonly used classes and goodies from the Rails framework'
|
||||
|
||||
|
||||
30
activesupport/build_marshalled_tzinfo_data.rb
Normal file
30
activesupport/build_marshalled_tzinfo_data.rb
Normal 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}"
|
||||
134
activesupport/lib/active_support/concern.rb
Normal file
134
activesupport/lib/active_support/concern.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 => '&', # ampersand
|
||||
60 => '<', # left angle bracket
|
||||
62 => '>', # 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
|
||||
@@ -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{"}, '"') # " 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
|
||||
@@ -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
|
||||
@@ -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 & 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 <,
|
||||
# > and & 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,65 +0,0 @@
|
||||
# I18n translation metadata is useful when you want to access information
|
||||
# about how a translation was looked up, pluralized or interpolated in
|
||||
# your application.
|
||||
#
|
||||
# msg = I18n.t(:message, :default => 'Hi!', :scope => :foo)
|
||||
# msg.translation_metadata
|
||||
# # => { :key => :message, :scope => :foo, :default => 'Hi!' }
|
||||
#
|
||||
# If a :count option was passed to #translate it will be set to the metadata.
|
||||
# Likewise, if any interpolation variables were passed they will also be set.
|
||||
#
|
||||
# To enable translation metadata you can simply include the Metadata module
|
||||
# into the Simple backend class - or whatever other backend you are using:
|
||||
#
|
||||
# I18n::Backend::Simple.send(:include, I18n::Backend::Metadata)
|
||||
#
|
||||
module I18n
|
||||
module Backend
|
||||
module Metadata
|
||||
class << self
|
||||
def included(base)
|
||||
Object.class_eval do
|
||||
def translation_metadata
|
||||
@translation_metadata ||= {}
|
||||
end
|
||||
|
||||
def translation_metadata=(translation_metadata)
|
||||
@translation_metadata = translation_metadata
|
||||
end
|
||||
end unless Object.method_defined?(:translation_metadata)
|
||||
end
|
||||
end
|
||||
|
||||
def translate(locale, key, options = {})
|
||||
metadata = {
|
||||
:locale => locale,
|
||||
:key => key,
|
||||
:scope => options[:scope],
|
||||
:default => options[:default],
|
||||
:separator => options[:separator],
|
||||
:values => options.reject { |name, value| Base::RESERVED_KEYS.include?(name) }
|
||||
}
|
||||
with_metadata(metadata) { super }
|
||||
end
|
||||
|
||||
def interpolate(locale, entry, values = {})
|
||||
metadata = entry.translation_metadata.merge(:original => entry)
|
||||
with_metadata(metadata) { super }
|
||||
end
|
||||
|
||||
def pluralize(locale, entry, count)
|
||||
with_metadata(:count => count) { super }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def with_metadata(metadata, &block)
|
||||
result = yield
|
||||
result.translation_metadata = result.translation_metadata.merge(metadata) if result
|
||||
result
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,57 +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 specific pluralizations you can simply include the
|
||||
# Pluralization module to the Simple backend - or whatever other backend you
|
||||
# are using.
|
||||
#
|
||||
# I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
|
||||
#
|
||||
# You also need to make sure to provide pluralization algorithms to the
|
||||
# backend, i.e. include them to your I18n.load_path accordingly.
|
||||
module I18n
|
||||
module Backend
|
||||
module Pluralization
|
||||
# Overwrites the Base backend translate method so that it will check the
|
||||
# translation meta data space (:i18n) for a locale specific pluralization
|
||||
# rule and use it to pluralize the given entry. I.e. the library expects
|
||||
# pluralization rules to be stored at I18n.t(:'i18n.plural.rule')
|
||||
#
|
||||
# Pluralization rules are expected to respond to #call(entry, count) and
|
||||
# return a pluralization key. Valid keys depend on the translation data
|
||||
# hash (entry) but it is generally recommended to follow CLDR's style,
|
||||
# i.e., return one of the keys :zero, :one, :few, :many, :other.
|
||||
#
|
||||
# The :zero key is always picked directly when count equals 0 AND the
|
||||
# translation data has the key :zero. This way translators are free to
|
||||
# either pick a special :zero translation even for languages where the
|
||||
# pluralizer does not return a :zero key.
|
||||
def pluralize(locale, entry, count)
|
||||
return entry unless entry.is_a?(Hash) and count
|
||||
|
||||
pluralizer = pluralizer(locale)
|
||||
if pluralizer.respond_to?(:call)
|
||||
key = count == 0 && entry.has_key?(:zero) ? :zero : pluralizer.call(count)
|
||||
raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
|
||||
entry[key]
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def pluralizers
|
||||
@pluralizers ||= {}
|
||||
end
|
||||
|
||||
def pluralizer(locale)
|
||||
pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,87 +0,0 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
# A simple backend that reads translations from YAML files and stores them in
|
||||
# an in-memory hash. Relies on the Base backend.
|
||||
#
|
||||
# The implementation is provided by a Implementation module allowing to easily
|
||||
# extend Simple backend's behavior by including modules. E.g.:
|
||||
#
|
||||
# module I18n::Backend::Pluralization
|
||||
# def pluralize(*args)
|
||||
# # extended pluralization logic
|
||||
# super
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
|
||||
class Simple
|
||||
module Implementation
|
||||
include Base
|
||||
|
||||
def initialized?
|
||||
@initialized ||= false
|
||||
end
|
||||
|
||||
# Stores translations for the given locale in memory.
|
||||
# This uses a deep merge for the translations hash, so existing
|
||||
# translations will be overwritten by new ones only at the deepest
|
||||
# level of the hash.
|
||||
def store_translations(locale, data, options = {})
|
||||
locale = locale.to_sym
|
||||
translations[locale] ||= {}
|
||||
data = data.deep_symbolize_keys
|
||||
translations[locale].deep_merge!(data)
|
||||
end
|
||||
|
||||
# Get available locales from the translations hash
|
||||
def available_locales
|
||||
init_translations unless initialized?
|
||||
translations.inject([]) do |locales, (locale, data)|
|
||||
locales << locale unless (data.keys - [:i18n]).empty?
|
||||
locales
|
||||
end
|
||||
end
|
||||
|
||||
# Clean up translations hash and set initialized to false on reload!
|
||||
def reload!
|
||||
@initialized = false
|
||||
@translations = nil
|
||||
super
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def init_translations
|
||||
load_translations
|
||||
@initialized = true
|
||||
end
|
||||
|
||||
def translations
|
||||
@translations ||= {}
|
||||
end
|
||||
|
||||
# Looks up a translation from the translations hash. Returns nil if
|
||||
# eiher key is nil, or locale, scope or key do not exist as a key in the
|
||||
# nested translations hash. Splits keys or scopes containing dots
|
||||
# into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
|
||||
# <tt>%w(currency format)</tt>.
|
||||
def lookup(locale, key, scope = [], options = {})
|
||||
init_translations unless initialized?
|
||||
keys = I18n.normalize_keys(locale, key, scope, options[:separator])
|
||||
|
||||
keys.inject(translations) do |result, key|
|
||||
key = key.to_sym
|
||||
return nil unless result.is_a?(Hash) && result.has_key?(key)
|
||||
result = result[key]
|
||||
result = resolve(locale, key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include Implementation
|
||||
end
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user