diff options
143 files changed, 1257 insertions, 245 deletions
diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 90503edc20..0c669e2e91 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -7,6 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Email composition, delivery, and receiving framework (part of Rails).' s.description = 'Email on Rails. Compose, deliver, receive, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments.' s.required_ruby_version = '>= 1.9.3' + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 589a67dc02..ae26d6f9e5 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -7,6 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Web-flow and rendering framework putting the VC in MVC (part of Rails).' s.description = 'Web apps on Rails. Simple, battle-tested conventions for building and testing MVC web applications. Works with any Rack-compatible server.' s.required_ruby_version = '>= 1.9.3' + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 97a9eec144..9c3960961b 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -51,7 +51,7 @@ module AbstractController # to specify particular actions as hidden. # # ==== Returns - # * <tt>array</tt> - An array of method names that should not be considered actions. + # * <tt>Array</tt> - An array of method names that should not be considered actions. def hidden_actions [] end @@ -63,7 +63,7 @@ module AbstractController # itself. Finally, #hidden_actions are removed. # # ==== Returns - # * <tt>set</tt> - A set of all methods that should be considered actions. + # * <tt>Set</tt> - A set of all methods that should be considered actions. def action_methods @action_methods ||= begin # All public instance methods of this class, including ancestors @@ -92,11 +92,12 @@ module AbstractController # controller_path. # # ==== Returns - # * <tt>string</tt> + # * <tt>String</tt> def controller_path @controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous? end + # Refresh the cached action_methods when a new action_method is added. def method_added(name) super clear_action_methods! @@ -130,6 +131,7 @@ module AbstractController self.class.controller_path end + # Delegates to the class' #action_methods def action_methods self.class.action_methods end @@ -139,8 +141,14 @@ module AbstractController # # Notice that <tt>action_methods.include?("foo")</tt> may return # false and <tt>available_action?("foo")</tt> returns true because - # available action consider actions that are also available + # this method considers actions that are also available # through other means, for example, implicit render ones. + # + # ==== Parameters + # * <tt>action_name</tt> - The name of an action to be tested + # + # ==== Returns + # * <tt>TrueClass</tt>, <tt>FalseClass</tt> def available_action?(action_name) method_for_action(action_name).present? end diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 4ad7071820..d8bcc28613 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -87,6 +87,14 @@ module ActionDispatch alias :key? :has_key? alias :include? :has_key? + def keys + @delegate.keys + end + + def values + @delegate.values + end + def []=(key, value) load_for_write! @delegate[key.to_s] = value diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 67a208263b..e43e897783 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1318,7 +1318,7 @@ module ActionDispatch def draw(name) path = @draw_paths.find do |_path| - _path.join("#{name}.rb").file? + File.exists? "#{_path}/#{name}.rb" end unless path @@ -1328,8 +1328,8 @@ module ActionDispatch raise ArgumentError, msg end - route_path = path.join("#{name}.rb") - instance_eval(route_path.read, route_path.to_s) + route_path = "#{path}/#{name}.rb" + instance_eval(File.read(route_path), route_path.to_s) end # match 'path' => 'controller#action' diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index ad8885b708..ac150882b1 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -342,7 +342,7 @@ module ActionView # Example: # # <%= form_for(@post) do |f| %> - # <% f.fields_for(:comments, :include_id => false) do |cf| %> + # <%= f.fields_for(:comments, :include_id => false) do |cf| %> # ... # <% end %> # <% end %> diff --git a/actionpack/lib/action_view/helpers/tags/date_field.rb b/actionpack/lib/action_view/helpers/tags/date_field.rb index 0e79609d52..64c29dea3d 100644 --- a/actionpack/lib/action_view/helpers/tags/date_field.rb +++ b/actionpack/lib/action_view/helpers/tags/date_field.rb @@ -1,13 +1,12 @@ module ActionView module Helpers module Tags - class DateField < TextField #:nodoc: - def render - options = @options.stringify_keys - options["value"] = @options.fetch("value") { value(object).try(:to_date) } - @options = options - super - end + class DateField < DatetimeField #:nodoc: + private + + def format_date(value) + value.try(:strftime, "%Y-%m-%d") + end end end end diff --git a/actionpack/lib/action_view/helpers/tags/datetime_field.rb b/actionpack/lib/action_view/helpers/tags/datetime_field.rb index 11d58744fd..e407146e96 100644 --- a/actionpack/lib/action_view/helpers/tags/datetime_field.rb +++ b/actionpack/lib/action_view/helpers/tags/datetime_field.rb @@ -4,16 +4,16 @@ module ActionView class DatetimeField < TextField #:nodoc: def render options = @options.stringify_keys - options["value"] = @options.fetch("value") { format_global_date_time_string(value(object)) } - options["min"] = format_global_date_time_string(options["min"]) - options["max"] = format_global_date_time_string(options["max"]) + options["value"] = @options.fetch("value") { format_date(value(object)) } + options["min"] = format_date(options["min"]) + options["max"] = format_date(options["max"]) @options = options super end private - def format_global_date_time_string(value) + def format_date(value) value.try(:strftime, "%Y-%m-%dT%T.%L%z") end end diff --git a/actionpack/lib/action_view/helpers/tags/datetime_local_field.rb b/actionpack/lib/action_view/helpers/tags/datetime_local_field.rb index 7593a3c733..6668d6d718 100644 --- a/actionpack/lib/action_view/helpers/tags/datetime_local_field.rb +++ b/actionpack/lib/action_view/helpers/tags/datetime_local_field.rb @@ -1,20 +1,16 @@ module ActionView module Helpers module Tags - class DatetimeLocalField < TextField #:nodoc: - def render - options = @options.stringify_keys - options["type"] = "datetime-local" - options["value"] = @options.fetch("value") { format_local_date_time_string(value(object)) } - options["min"] = format_local_date_time_string(options["min"]) - options["max"] = format_local_date_time_string(options["max"]) - @options = options - super + class DatetimeLocalField < DatetimeField #:nodoc: + class << self + def field_type + @field_type ||= "datetime-local" + end end private - def format_local_date_time_string(value) + def format_date(value) value.try(:strftime, "%Y-%m-%dT%T") end end diff --git a/actionpack/lib/action_view/helpers/tags/month_field.rb b/actionpack/lib/action_view/helpers/tags/month_field.rb index 56bd85a90b..3d3c32d847 100644 --- a/actionpack/lib/action_view/helpers/tags/month_field.rb +++ b/actionpack/lib/action_view/helpers/tags/month_field.rb @@ -1,19 +1,10 @@ module ActionView module Helpers module Tags - class MonthField < TextField #:nodoc: - def render - options = @options.stringify_keys - options["value"] = @options.fetch("value") { format_month_string(value(object)) } - options["min"] = format_month_string(options["min"]) - options["max"] = format_month_string(options["max"]) - @options = options - super - end - + class MonthField < DatetimeField #:nodoc: private - def format_month_string(value) + def format_date(value) value.try(:strftime, "%Y-%m") end end diff --git a/actionpack/lib/action_view/helpers/tags/time_field.rb b/actionpack/lib/action_view/helpers/tags/time_field.rb index 271dc00c54..a3941860c9 100644 --- a/actionpack/lib/action_view/helpers/tags/time_field.rb +++ b/actionpack/lib/action_view/helpers/tags/time_field.rb @@ -1,13 +1,12 @@ module ActionView module Helpers module Tags - class TimeField < TextField #:nodoc: - def render - options = @options.stringify_keys - options["value"] = @options.fetch("value") { value(object).try(:strftime, "%T.%L") } - @options = options - super - end + class TimeField < DatetimeField #:nodoc: + private + + def format_date(value) + value.try(:strftime, "%T.%L") + end end end end diff --git a/actionpack/lib/action_view/helpers/tags/week_field.rb b/actionpack/lib/action_view/helpers/tags/week_field.rb index a1265d9928..1e13939a0a 100644 --- a/actionpack/lib/action_view/helpers/tags/week_field.rb +++ b/actionpack/lib/action_view/helpers/tags/week_field.rb @@ -1,19 +1,10 @@ module ActionView module Helpers module Tags - class WeekField < TextField #:nodoc: - def render - options = @options.stringify_keys - options["value"] = @options.fetch("value") { format_week_string(value(object)) } - options["min"] = format_week_string(options["min"]) - options["max"] = format_week_string(options["max"]) - @options = options - super - end - + class WeekField < DatetimeField #:nodoc: private - def format_week_string(value) + def format_date(value) value.try(:strftime, "%Y-W%W") end end diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index 4d24456ba6..80d5a13171 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -36,6 +36,22 @@ module ActionDispatch assert_equal s, Session.find(env) end + def test_keys + env = {} + s = Session.create(store, env, {}) + s['rails'] = 'ftw' + s['adequate'] = 'awesome' + assert_equal %w[rails adequate], s.keys + end + + def test_values + env = {} + s = Session.create(store, env, {}) + s['rails'] = 'ftw' + s['adequate'] = 'awesome' + assert_equal %w[ftw awesome], s.values + end + private def store Class.new { diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 00d09282ca..fa4cb301eb 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -2331,7 +2331,7 @@ class TestDrawExternalFile < ActionDispatch::IntegrationTest end end - DRAW_PATH = Pathname.new(File.expand_path('../../fixtures/routes', __FILE__)) + DRAW_PATH = File.expand_path('../../fixtures/routes', __FILE__) DefaultScopeRoutes = ActionDispatch::Routing::RouteSet.new.tap do |app| app.draw_paths << DRAW_PATH diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 97c8025db3..c9b39ed18f 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -644,6 +644,15 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal(expected, date_field("post", "written_on")) end + def test_date_field_with_extra_attrs + expected = %{<input id="post_written_on" step="2" max="2010-08-15" min="2000-06-15" name="post[written_on]" type="date" value="2004-06-15" />} + @post.written_on = DateTime.new(2004, 6, 15) + min_value = DateTime.new(2000, 6, 15) + max_value = DateTime.new(2010, 8, 15) + step = 2 + assert_dom_equal(expected, date_field("post", "written_on", :min => min_value, :max => max_value, :step => step)) + end + def test_date_field_with_timewithzone_value previous_time_zone, Time.zone = Time.zone, 'UTC' expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />} @@ -670,6 +679,15 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal(expected, time_field("post", "written_on")) end + def test_time_field_with_extra_attrs + expected = %{<input id="post_written_on" step="60" max="10:25:00.000" min="20:45:30.000" name="post[written_on]" type="time" value="01:02:03.000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = DateTime.new(2000, 6, 15, 20, 45, 30) + max_value = DateTime.new(2010, 8, 15, 10, 25, 00) + step = 60 + assert_dom_equal(expected, time_field("post", "written_on", :min => min_value, :max => max_value, :step => step)) + end + def test_time_field_with_timewithzone_value previous_time_zone, Time.zone = Time.zone, 'UTC' expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="01:02:03.000" />} diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index f2d004fb0a..66f324a1a1 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -8,6 +8,7 @@ Gem::Specification.new do |s| s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, observers, serialization, internationalization, and testing.' s.required_ruby_version = '>= 1.9.3' + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 99918fdb96..846d0d7f86 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -196,7 +196,7 @@ module ActiveModel attribute_method_matchers.each do |matcher| matcher_new = matcher.method_name(new_name).to_s matcher_old = matcher.method_name(old_name).to_s - define_optimized_call self, matcher_new, matcher_old + define_proxy_call false, self, matcher_new, matcher_old end end @@ -238,7 +238,7 @@ module ActiveModel if respond_to?(generate_method, true) send(generate_method, attr_name) else - define_optimized_call generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s + define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s end end end @@ -293,7 +293,7 @@ module ActiveModel # Define a method `name` in `mod` that dispatches to `send` # using the given `extra` args. This fallbacks `define_method` # and `send` if the given names cannot be compiled. - def define_optimized_call(mod, name, send, *extra) #:nodoc: + def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc: if name =~ NAME_COMPILABLE_REGEXP defn = "def #{name}(*args)" else @@ -303,7 +303,7 @@ module ActiveModel extra = (extra.map(&:inspect) << "*args").join(", ") if send =~ CALL_COMPILABLE_REGEXP - target = "#{send}(#{extra})" + target = "#{"self." unless include_private}#{send}(#{extra})" else target = "send(:'#{send}', #{extra})" end diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index b78f1ff3f3..2b3e9ce134 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -172,7 +172,7 @@ module ActiveModel # <id type="integer">1</id> # <name>David</name> # <age type="integer">16</age> - # <created-at type="datetime">2011-01-30T22:29:23Z</created-at> + # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at> # </user> # # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index a9db29ee21..e2f2cecc09 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -76,6 +76,19 @@ private end end +class ModelWithRubyKeywordNamedAttributes + include ActiveModel::AttributeMethods + + def attributes + { :begin => 'value of begin', :end => 'value of end' } + end + +private + def attribute(name) + attributes[name.to_sym] + end +end + class ModelWithoutAttributesMethod include ActiveModel::AttributeMethods end @@ -155,6 +168,15 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar end + test '#alias_attribute works with attributes named as a ruby keyword' do + ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end]) + ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin) + ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end) + + assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from + assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to + end + test '#undefine_attribute_methods removes attribute methods' do ModelWithAttributes.define_attribute_methods(:foo) ModelWithAttributes.undefine_attribute_methods diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index 5fa227e0e0..7eb48abc3c 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -140,7 +140,7 @@ class XmlSerializationTest < ActiveModel::TestCase end test "should serialize datetime" do - assert_match %r{<created-at type=\"datetime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml + assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml end test "should serialize boolean" do diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index e8e5f4adfe..dca7f13fd2 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -8,6 +8,7 @@ Gem::Specification.new do |s| s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.' s.required_ruby_version = '>= 1.9.3' + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index f0b549e5ad..3ae7030caa 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -10,9 +10,9 @@ module ActiveRecord # Active Record implements aggregation through a macro-like class method called +composed_of+ # for representing attributes as value objects. It expresses relationships like "Account [is] # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call - # to the macro adds a description of how the value objects are created from the attributes of - # the entity object (when the entity is initialized either as a new object or from finding an - # existing object) and how it can be turned back into attributes (when the entity is saved to + # to the macro adds a description of how the value objects are created from the attributes of + # the entity object (when the entity is initialized either as a new object or from finding an + # existing object) and how it can be turned back into attributes (when the entity is saved to # the database). # # class Customer < ActiveRecord::Base diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index fa316a8c9d..100fb38dec 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -37,9 +37,107 @@ module ActiveRecord delegate :target, :load_target, :loaded?, :to => :@association ## + # :method: select + # + # :call-seq: + # select(select = nil) + # select(&block) + # + # Works in two ways. + # + # *First:* Specify a subset of fields to be selected from the result set. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.select(:name) + # # => [ + # # #<Pet id: nil, name: "Fancy-Fancy">, + # # #<Pet id: nil, name: "Spook">, + # # #<Pet id: nil, name: "Choo-Choo"> + # # ] + # + # person.pets.select([:id, :name]) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy">, + # # #<Pet id: 2, name: "Spook">, + # # #<Pet id: 3, name: "Choo-Choo"> + # # ] + # + # Be careful because this also means you’re initializing a model + # object with only the fields that you’ve selected. If you attempt + # to access a field that is not in the initialized record you’ll + # receive: + # + # person.pets.select(:name).first.person_id + # # => ActiveModel::MissingAttributeError: missing attribute: person_id + # + # *Second:* You can pass a block so it can be used just like Array#select. + # This build an array of objects from the database for the scope, + # converting them into an array and iterating through them using + # Array#select. + # + # person.pets.select { |pet| pet.name =~ /oo/ } + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.select(:name) { |pet| pet.name =~ /oo/ } + # # => [ + # # #<Pet id: 2, name: "Spook">, + # # #<Pet id: 3, name: "Choo-Choo"> + # # ] + + ## + # :method: find + # + # :call-seq: + # find(*args, &block) + # + # Finds an object in the collection responding to the +id+. Uses the same + # rules as +ActiveRecord::Base.find+. Returns +ActiveRecord::RecordNotFound++ + # error if the object can not be found. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4 + # + # person.pets.find(2) { |pet| pet.name.downcase! } + # # => #<Pet id: 2, name: "fancy-fancy", person_id: 1> + # + # person.pets.find(2, 3) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + + ## # :method: first + # + # :call-seq: + # first(limit = nil) + # # Returns the first record, or the first +n+ records, from the collection. - # If the collection is empty, the first form returns nil, and the second + # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. # # class Person < ActiveRecord::Base @@ -67,8 +165,12 @@ module ActiveRecord ## # :method: last + # + # :call-seq: + # last(limit = nil) + # # Returns the last record, or the last +n+ records, from the collection. - # If the collection is empty, the first form returns nil, and the second + # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. # # class Person < ActiveRecord::Base @@ -95,7 +197,95 @@ module ActiveRecord # another_person_without.pets.last(3) # => [] ## + # :method: build + # + # :call-seq: + # build(attributes = {}, options = {}, &block) + # + # Returns a new object of the collection type that has been instantiated + # with +attributes+ and linked to this object, but have not yet been saved. + # You can pass an array of attributes hashes, this will return an array + # with the new objects. + # + # class Person + # has_many :pets + # end + # + # person.pets.build + # # => #<Pet id: nil, name: nil, person_id: 1> + # + # person.pets.build(name: 'Fancy-Fancy') + # # => #<Pet id: nil, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}]) + # # => [ + # # #<Pet id: nil, name: "Spook", person_id: 1>, + # # #<Pet id: nil, name: "Choo-Choo", person_id: 1>, + # # #<Pet id: nil, name: "Brain", person_id: 1> + # # ] + # + # person.pets.size # => 5 # size of the collection + # person.pets.count # => 0 # count from database + + ## + # :method: create + # + # :call-seq: + # create(attributes = {}, options = {}, &block) + # + # Returns a new object of the collection type that has been instantiated with + # attributes, linked to this object and that has already been saved (if it + # passes the validations). + # + # class Person + # has_many :pets + # end + # + # person.pets.create(name: 'Fancy-Fancy') + # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.create([{name: 'Spook'}, {name: 'Choo-Choo'}]) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 3 + # person.pets.count # => 3 + # + # person.pets.find(1, 2, 3) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + + ## + # :method: create! + # + # :call-seq: + # create!(attributes = {}, options = {}, &block) + # + # Like +create+, except that if the record is invalid, raises an exception. + # + # class Person + # has_many :pets + # end + # + # class Pet + # attr_accessible :name + # validates :name, presence: true + # end + # + # person.pets.create!(name: nil) + # # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank + + ## # :method: concat + # + # :call-seq: + # concat(*records) + # # Add one or more records to the collection by setting their foreign keys # to the association's primary key. Since << flattens its argument list and # inserts each record, +push+ and +concat+ behave identically. Returns +self+ @@ -123,6 +313,10 @@ module ActiveRecord ## # :method: replace + # + # :call-seq: + # replace(other_array) + # # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. # @@ -147,23 +341,249 @@ module ActiveRecord # # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String ## + # :method: delete_all + # + # Deletes all the records from the collection. For +has_many+ asssociations, + # the deletion is done according to the strategy specified by the <tt>:dependent</tt> + # option. Returns an array with the deleted records. + # + # If no <tt>:dependent</tt> option is given, then it will follow the + # default strategy. The default strategy is <tt>:nullify</tt>. This + # sets the foreign keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, + # the default strategy is +delete_all+. + # + # class Person < ActiveRecord::Base + # has_many :pets # dependent: :nullify option by default + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1, 2, 3) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>, + # # #<Pet id: 2, name: "Spook", person_id: nil>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: nil> + # # ] + # + # If it is set to <tt>:destroy</tt> all the objects from the collection + # are removed by calling their +destroy+ method. See +destroy+ for more + # information. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :destroy + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1, 2, 3) + # # => ActiveRecord::RecordNotFound + # + # If it is set to <tt>:delete_all</tt>, all the objects are deleted + # *without* calling their +destroy+ method. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :delete_all + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1, 2, 3) + # # => ActiveRecord::RecordNotFound + + ## # :method: destroy_all - # Destroy all the records from this association. + # + # Deletes the records of the collection directly from the database. + # This will _always_ remove the records ignoring the +:dependent+ + # option. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] # # person.pets.destroy_all # # person.pets.size # => 0 # person.pets # => [] + # + # Pet.find(1) # => Couldn't find Pet with id=1 + + ## + # :method: destroy + # + # :call-seq: + # destroy(*records) + # + # Destroy the +records+ supplied and remove them from the collection. + # This method will _always_ remove record from the database ignoring + # the +:dependent+ option. Returns an array with the removed records. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy(Pet.find(1)) + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy(Pet.find(2), Pet.find(3)) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3) + # + # You can pass +Fixnum+ or +String+ values, it finds the records + # responding to the +id+ and then deletes them from the database. + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.destroy("4") + # # => #<Pet id: 4, name: "Benny", person_id: 1> + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.destroy(5, 6) + # # => [ + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6) + + ## + # :method: size + # + # Returns the size of the collection. If the collection hasn't been loaded, + # it executes a <tt>SELECT COUNT(*)</tt> query. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # # executes something like SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" = 1 + # + # person.pets # This will execute a SELECT * FROM query + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 3 + # # Because the collection is already loaded, this will behave like + # # collection.size and no SQL count query is executed. + + ## + # :method: length + # + # Returns the size of the collection calling +size+ on the target. + # If the collection has been already loaded, +length+ and +size+ are + # equivalent. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.length # => 3 + # # executes something like SELECT "pets".* FROM "pets" WHERE "pets"."person_id" = 1 + # + # # Because the collection is loaded, you can + # # call the collection with no additional queries: + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] ## # :method: empty? - # Returns true if the collection is empty. + # + # Returns +true+ if the collection is empty. # # class Person < ActiveRecord::Base # has_many :pets @@ -179,7 +599,12 @@ module ActiveRecord ## # :method: any? - # Returns true if the collection is not empty. + # + # :call-seq: + # any? + # any?{|item| block} + # + # Returns +true+ if the collection is not empty. # # class Person < ActiveRecord::Base # has_many :pets @@ -211,8 +636,13 @@ module ActiveRecord ## # :method: many? + # + # :call-seq: + # many? + # many?{|item| block} + # # Returns true if the collection has more than one record. - # Equivalent to +collection.size > 1+. + # Equivalent to <tt>collection.size > 1</tt>. # # class Person < ActiveRecord::Base # has_many :pets @@ -248,7 +678,11 @@ module ActiveRecord ## # :method: include? - # Returns true if the given object is present in the collection. + # + # :call-seq: + # include?(record) + # + # Returns +true+ if the given object is present in the collection. # # class Person < ActiveRecord::Base # has_many :pets @@ -331,37 +765,32 @@ module ActiveRecord end alias_method :push, :<< - # Removes every object from the collection. This does not destroy - # the objects, it sets their foreign keys to +NULL+. Returns +self+ - # so methods can be chained. + # Equivalent to +delete_all+. The difference is that returns +self+, instead + # of an array with the deleted objects, so methods can be chained. See + # +delete_all+ for more information. + def clear + delete_all + self + end + + # Reloads the collection from the database. Returns +self+. + # Equivalent to <tt>collection(true)</tt>. # # class Person < ActiveRecord::Base # has_many :pets # end # - # person.pets # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] - # person.pets.clear # => [] - # person.pets.size # => 0 + # person.pets # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] # - # Pet.find(1) # => #<Pet id: 1, name: "Snoop", group: "dogs", person_id: nil> + # person.pets # uses the pets cache + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] # - # If they are associated with +dependent: :destroy+ option, it deletes - # them directly from the database. + # person.pets.reload # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] # - # class Person < ActiveRecord::Base - # has_many :pets, dependent: :destroy - # end - # - # person.pets # => [#<Pet id: 2, name: "Gorby", group: "cats", person_id: 2>] - # person.pets.clear # => [] - # person.pets.size # => 0 - # - # Pet.find(2) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=2 - def clear - delete_all - self - end - + # person.pets(true) # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload proxy_association.reload self diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index fafed94ff2..54705e4950 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -12,7 +12,7 @@ module ActiveRecord # and all of its books via a single query: # # SELECT * FROM authors - # LEFT OUTER JOIN books ON authors.id = books.id + # LEFT OUTER JOIN books ON authors.id = books.author_id # WHERE authors.name = 'Ken Akamatsu' # # However, this could result in many rows that contain redundant data. After diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index c6699737b4..c259e46073 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -55,12 +55,20 @@ module ActiveRecord # # == Options # - # There are two connection-pooling-related options that you can add to + # There are several connection-pooling-related options that you can add to # your database connection configuration: # # * +pool+: number indicating size of connection pool (default 5) - # * +wait_timeout+: number of seconds to block and wait for a connection + # * +checkout_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). + # * +reaping_frequency+: frequency in seconds to periodically run the + # Reaper, which attempts to find and close dead connections, which can + # occur if a programmer forgets to close a connection at the end of a + # thread or a thread dies unexpectedly. (Default nil, which means don't + # run the Reaper). + # * +dead_connection_timeout+: number of seconds from last checkout + # after which the Reaper will consider a connection reapable. (default + # 5 seconds). class ConnectionPool # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. # A reaper instantiated with a nil frequency will never reap the @@ -89,7 +97,7 @@ module ActiveRecord include MonitorMixin - attr_accessor :automatic_reconnect, :timeout + attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout attr_reader :spec, :connections, :size, :reaper class Latch # :nodoc: @@ -121,7 +129,8 @@ module ActiveRecord # The cache of reserved connections mapped to threads @reserved_connections = {} - @timeout = spec.config[:wait_timeout] || 5 + @checkout_timeout = spec.config[:checkout_timeout] || 5 + @dead_connection_timeout = spec.config[:dead_connection_timeout] @reaper = Reaper.new self, spec.config[:reaping_frequency] @reaper.run @@ -241,7 +250,7 @@ module ActiveRecord return checkout_and_verify(conn) if conn end - Timeout.timeout(@timeout, PoolFullError) { @latch.await } + Timeout.timeout(@checkout_timeout, PoolFullError) { @latch.await } end end @@ -279,7 +288,7 @@ module ActiveRecord # or a thread dies unexpectedly. def reap synchronize do - stale = Time.now - @timeout + stale = Time.now - @dead_connection_timeout connections.dup.each do |conn| remove conn if conn.in_use? && stale > conn.last_use && !conn.active? end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 350ccce03d..8fc172f6e8 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -268,7 +268,7 @@ module ActiveRecord # increase timeout so mysql server doesn't disconnect us wait_timeout = @config[:wait_timeout] - wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) + wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) variable_assignments << "@@wait_timeout = #{wait_timeout}" execute("SET #{variable_assignments.join(', ')}", :skip_logging) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 15c3d7be36..7dcea375e1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -18,7 +18,7 @@ module ActiveRecord # Forward any unused config params to PGconn.connect. [:statement_limit, :encoding, :min_messages, :schema_search_path, - :schema_order, :adapter, :pool, :wait_timeout, :template, + :schema_order, :adapter, :pool, :checkout_timeout, :template, :reaping_frequency, :insert_returning].each do |key| conn_params.delete key end @@ -964,22 +964,22 @@ module ActiveRecord binds = [[nil, table]] binds << [nil, schema] if schema - exec_query(<<-SQL, 'SCHEMA', binds).rows.first[0].to_i > 0 + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 SELECT COUNT(*) FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind in ('v','r') - AND c.relname = $1 - AND n.nspname = #{schema ? '$2' : 'ANY (current_schemas(false))'} + AND c.relname = '#{table.gsub(/(^"|"$)/,'')}' + AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} SQL end # Returns true if schema exists. def schema_exists?(name) - exec_query(<<-SQL, 'SCHEMA', [[nil, name]]).rows.first[0].to_i > 0 + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 SELECT COUNT(*) FROM pg_namespace - WHERE nspname = $1 + WHERE nspname = '#{name}' SQL end @@ -1110,8 +1110,8 @@ module ActiveRecord end def serial_sequence(table, column) - result = exec_query(<<-eosql, 'SCHEMA', [[nil, table], [nil, column]]) - SELECT pg_get_serial_sequence($1, $2) + result = exec_query(<<-eosql, 'SCHEMA') + SELECT pg_get_serial_sequence('#{table}', '#{column}') eosql result.rows.first.first end @@ -1188,13 +1188,13 @@ module ActiveRecord # Returns just a table's primary key def primary_key(table) - row = exec_query(<<-end_sql, 'SCHEMA', [[nil, table]]).rows.first + row = exec_query(<<-end_sql, 'SCHEMA').rows.first SELECT DISTINCT(attr.attname) FROM pg_attribute attr INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] WHERE cons.contype = 'p' - AND dep.refobjid = $1::regclass + AND dep.refobjid = '#{table}'::regclass end_sql row && row.first @@ -1274,7 +1274,7 @@ module ActiveRecord end when 'integer' return 'integer' unless limit - + case limit when 1, 2; 'smallint' when 3, 4; 'integer' @@ -1471,7 +1471,7 @@ module ActiveRecord end def last_insert_id_result(sequence_name) #:nodoc: - exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) + exec_query("SELECT currval('#{sequence_name}')", 'SQL') end # Executes a SELECT query and returns the results, performing any data type diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index f26e18b1e0..d8d4834d22 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -406,10 +406,11 @@ db_namespace = namespace :db do set_psql_env(abcs[Rails.env]) search_path = abcs[Rails.env]['schema_search_path'] unless search_path.blank? - search_path = search_path.split(",").map{|search_path_part| "--schema=#{search_path_part.strip}" }.join(" ") + search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") end - `pg_dump -i -s -x -O -f #{filename} #{search_path} #{abcs[Rails.env]['database']}` + `pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(abcs[Rails.env]['database'])}` raise 'Error dumping database' if $?.exitstatus == 1 + File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } when /sqlite/ dbfile = abcs[Rails.env]['database'] `sqlite3 #{dbfile} .schema > #{filename}` diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 2e60521638..b833af64fe 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -18,8 +18,8 @@ module ActiveRecord #:nodoc: # <id type="integer">1</id> # <approved type="boolean">false</approved> # <replies-count type="integer">0</replies-count> - # <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="datetime">2003-07-16T09:28:00+1200</written-on> + # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> + # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> # <content>Have a nice day</content> # <author-email-address>david@loudthinking.com</author-email-address> # <parent-id></parent-id> diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 8dc9f761c2..bba7815d73 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -124,7 +124,7 @@ module ActiveRecord @pool.checkout @pool.checkout @pool.checkout - @pool.timeout = 0 + @pool.dead_connection_timeout = 0 connections = @pool.connections.dup @@ -137,7 +137,7 @@ module ActiveRecord @pool.checkout @pool.checkout @pool.checkout - @pool.timeout = 0 + @pool.dead_connection_timeout = 0 connections = @pool.connections.dup connections.each do |conn| diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index fba3006ebe..0a6354f5cc 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -17,7 +17,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase end def checkout_connections - ActiveRecord::Model.establish_connection(@connection.merge({:pool => 2, :wait_timeout => 0.3})) + ActiveRecord::Model.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.3})) @connections = [] @timed_out = 0 @@ -34,7 +34,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase # Will deadlock due to lack of Monitor timeouts in 1.9 def checkout_checkin_connections(pool_size, threads) - ActiveRecord::Model.establish_connection(@connection.merge({:pool => pool_size, :wait_timeout => 0.5})) + ActiveRecord::Model.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5})) @connection_count = 0 @timed_out = 0 threads.times do diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index 576ab60090..e53a27d5dd 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -64,7 +64,7 @@ module ActiveRecord spec.config[:reaping_frequency] = 0.0001 pool = ConnectionPool.new spec - pool.timeout = 0 + pool.dead_connection_timeout = 0 conn = pool.checkout count = pool.connections.length diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 88751a72f9..12373333b0 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -92,7 +92,7 @@ class DefaultXmlSerializationTest < ActiveRecord::TestCase end def test_should_serialize_datetime - assert_match %r{<created-at type=\"datetime\">2006-08-01T00:00:00Z</created-at>}, @xml + assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @xml end def test_should_serialize_boolean @@ -109,7 +109,7 @@ class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)" toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) - assert_match %r{<updated-at type=\"datetime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml + assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml ensure Time.zone = timezone end @@ -118,7 +118,7 @@ class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)" toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload - assert_match %r{<updated-at type=\"datetime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml + assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml ensure Time.zone = timezone end @@ -152,7 +152,7 @@ class NilXmlSerializationTest < ActiveRecord::TestCase assert %r{<created-at (.*)></created-at>}.match(@xml) attributes = $1 assert_match %r{nil="true"}, attributes - assert_match %r{type="datetime"}, attributes + assert_match %r{type="dateTime"}, attributes end def test_should_serialize_boolean @@ -188,7 +188,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert_equal "integer" , xml.elements["//replies-count"].attributes['type'] assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text - assert_equal "datetime" , xml.elements["//written-on"].attributes['type'] + assert_equal "dateTime" , xml.elements["//written-on"].attributes['type'] assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text @@ -198,7 +198,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase if current_adapter?(:SybaseAdapter) assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text - assert_equal "datetime" , xml.elements["//last-read"].attributes['type'] + assert_equal "dateTime" , xml.elements["//last-read"].attributes['type'] else # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) assert_equal "2004-04-15", xml.elements["//last-read"].text @@ -211,7 +211,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert_equal "boolean" , xml.elements["//approved"].attributes['type'] assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text - assert_equal "datetime" , xml.elements["//bonus-time"].attributes['type'] + assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type'] end end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index c1fef1b9d9..62b8a789c7 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,9 +1,17 @@ ## Rails 4.0.0 (unreleased) ## +* Add `Hash#transform_keys`, `Hash#transform_keys!`, `Hash#deep_transform_keys`, and `Hash#deep_transform_keys!`. *Mark McSpadden* + +* Changed xml type `datetime` to `dateTime` (with upper case letter `T`). *Angelo Capilleri* + * Add `:instance_accessor` option for `class_attribute`. *Alexey Vakhov* * `constantize` now looks in the ancestor chain. *Marc-Andre Lafortune & Andrew White* +* Adds `Hash#deep_stringify_keys` and `Hash#deep_stringify_keys!` to convert all keys from a +Hash+ instance into strings *Lucas Húngaro* + +* Adds `Hash#deep_symbolize_keys` and `Hash#deep_symbolize_keys!` to convert all keys from a +Hash+ instance into symbols *Lucas Húngaro* + * `Object#try` can't call private methods. *Vasiliy Ermolovich* * `AS::Callbacks#run_callbacks` remove `key` argument. *Francesco Rodriguez* diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 2c874e932e..30221f2401 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -8,6 +8,7 @@ Gem::Specification.new do |s| s.description = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Rich support for multibyte strings, internationalization, time zones, and testing.' s.required_ruby_version = '>= 1.9.3' + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 02d5a7080f..03efe6a19a 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -65,11 +65,15 @@ class Range #:nodoc: # Optimize range sum to use arithmetic progression if a block is not given and # we have a range of numeric values. def sum(identity = 0) - if block_given? || !(first.instance_of?(Integer) && last.instance_of?(Integer)) + if block_given? || !(first.is_a?(Integer) && last.is_a?(Integer)) super else actual_last = exclude_end? ? (last - 1) : last - (actual_last - first + 1) * (actual_last + first) / 2 + if actual_last >= first + (actual_last - first + 1) * (actual_last + first) / 2 + else + identity + end end end end diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 469dc41f2d..43ba05a256 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -57,8 +57,8 @@ class Hash # "TrueClass" => "boolean", # "FalseClass" => "boolean", # "Date" => "date", - # "DateTime" => "datetime", - # "Time" => "datetime" + # "DateTime" => "dateTime", + # "Time" => "dateTime" # } # # By default the root node is "hash", but that's configurable via the <tt>:root</tt> option. diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index be4d611ce7..e5e77bcef4 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -1,46 +1,53 @@ class Hash - # Return a new hash with all keys converted to strings. + # Return a new hash with all keys converted using the block operation. # - # { :name => 'Rob', :years => '28' }.stringify_keys - # #=> { "name" => "Rob", "years" => "28" } - def stringify_keys + # { :name => 'Rob', :years => '28' }.transform_keys{ |key| key.to_s.upcase } + # # => { "NAME" => "Rob", "YEARS" => "28" } + def transform_keys result = {} keys.each do |key| - result[key.to_s] = self[key] + result[yield(key)] = self[key] end result end - # Destructively convert all keys to strings. Same as - # +stringify_keys+, but modifies +self+. - def stringify_keys! + # Destructively convert all keys using the block operations. + # Same as transform_keys but modifies +self+ + def transform_keys! keys.each do |key| - self[key.to_s] = delete(key) + self[yield(key)] = delete(key) end self end + # Return a new hash with all keys converted to strings. + # + # { :name => 'Rob', :years => '28' }.stringify_keys + # #=> { "name" => "Rob", "years" => "28" } + def stringify_keys + transform_keys{ |key| key.to_s } + end + + # Destructively convert all keys to strings. Same as + # +stringify_keys+, but modifies +self+. + def stringify_keys! + transform_keys!{ |key| key.to_s } + end + # Return a new hash with all keys converted to symbols, as long as # they respond to +to_sym+. # # { 'name' => 'Rob', 'years' => '28' }.symbolize_keys # #=> { :name => "Rob", :years => "28" } def symbolize_keys - result = {} - keys.each do |key| - result[(key.to_sym rescue key)] = self[key] - end - result + transform_keys{ |key| key.to_sym rescue key } end alias_method :to_options, :symbolize_keys # Destructively convert all keys to symbols, as long as they respond # to +to_sym+. Same as +symbolize_keys+, but modifies +self+. def symbolize_keys! - keys.each do |key| - self[(key.to_sym rescue key)] = delete(key) - end - self + transform_keys!{ |key| key.to_sym rescue key } end alias_method :to_options!, :symbolize_keys! @@ -57,4 +64,57 @@ class Hash raise ArgumentError.new("Unknown key: #{k}") unless valid_keys.include?(k) end end + + # Return a new hash with all keys converted by the block operation. + # This includes the keys from the root hash and from all + # nested hashes. + # + # { :person => { :name => 'Rob', :years => '28' } }.deep_transform_keys{ |key| key.to_s.upcase } + # # => { "PERSON" => { "NAME" => "Rob", "YEARS" => "28" } } + def deep_transform_keys(&block) + result = {} + each do |key, value| + result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value + end + result + end + + # Destructively convert all keys by using the block operation. + # This includes the keys from the root hash and from all + # nested hashes. + def deep_transform_keys!(&block) + keys.each do |key| + value = delete(key) + self[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys!(&block) : value + end + self + end + + # Return a new hash with all keys converted to strings. + # This includes the keys from the root hash and from all + # nested hashes. + def deep_stringify_keys + deep_transform_keys{ |key| key.to_s } + end + + # Destructively convert all keys to strings. + # This includes the keys from the root hash and from all + # nested hashes. + def deep_stringify_keys! + deep_transform_keys!{ |key| key.to_s } + end + + # Return a new hash with all keys converted to symbols, as long as + # they respond to +to_sym+. This includes the keys from the root hash + # and from all nested hashes. + def deep_symbolize_keys + deep_transform_keys{ |key| key.to_sym rescue key } + end + + # Destructively convert all keys to symbols, as long as they respond + # to +to_sym+. This includes the keys from the root hash and from all + # nested hashes. + def deep_symbolize_keys! + deep_transform_keys!{ |key| key.to_sym rescue key } + end end diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb index 883f5f556c..f55fbc282e 100644 --- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb +++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/duplicable' + class Object # Returns a deep copy of object if it's duplicable. If it's # not duplicable, returns +self+. diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 91459f3e5b..6e1c0da991 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -141,9 +141,13 @@ module ActiveSupport end def stringify_keys!; self end + def deep_stringify_keys!; self end def stringify_keys; dup end + def deep_stringify_keys; dup end undef :symbolize_keys! + undef :deep_symbolize_keys! def symbolize_keys; to_hash.symbolize_keys end + def deep_symbolize_keys; to_hash.deep_symbolize_keys end def to_options!; self end # Convert to a Hash with String keys. diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index 677e9910bb..88e18f6fff 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -39,8 +39,8 @@ module ActiveSupport "TrueClass" => "boolean", "FalseClass" => "boolean", "Date" => "date", - "DateTime" => "datetime", - "Time" => "datetime", + "DateTime" => "dateTime", + "Time" => "dateTime", "Array" => "array", "Hash" => "hash" } unless defined?(TYPE_NAMES) @@ -48,7 +48,7 @@ module ActiveSupport FORMATTING = { "symbol" => Proc.new { |symbol| symbol.to_s }, "date" => Proc.new { |date| date.to_s(:db) }, - "datetime" => Proc.new { |time| time.xmlschema }, + "dateTime" => Proc.new { |time| time.xmlschema }, "binary" => Proc.new { |binary| ::Base64.encode64(binary) }, "yaml" => Proc.new { |yaml| yaml.to_yaml } } unless defined?(FORMATTING) @@ -111,6 +111,7 @@ module ActiveSupport type_name ||= TYPE_NAMES[value.class.name] type_name ||= value.class.name if value && !value.respond_to?(:to_str) type_name = type_name.to_s if type_name + type_name = "dateTime" if type_name == "datetime" key = rename_key(key.to_s, options) @@ -145,7 +146,7 @@ module ActiveSupport "#{left}#{middle.tr('_ ', '--')}#{right}" end - # TODO: Add support for other encodings + # TODO: Add support for other encodings def _parse_binary(bin, entity) #:nodoc: case entity['encoding'] when 'base64' diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 0bf48dd378..0a1abac767 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -84,6 +84,11 @@ class EnumerableTests < ActiveSupport::TestCase assert_equal 10, (1..4.5).sum assert_equal 6, (1...4).sum assert_equal 'abc', ('a'..'c').sum + assert_equal 50_000_005_000_000, (0..10_000_000).sum + assert_equal 0, (10..0).sum + assert_equal 5, (10..0).sum(5) + assert_equal 10, (10..10).sum + assert_equal 42, (10...10).sum(42) end def test_index_by diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 8239054117..5d422ce5ad 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -4,6 +4,7 @@ require 'bigdecimal' require 'active_support/core_ext/string/access' require 'active_support/ordered_hash' require 'active_support/core_ext/object/conversions' +require 'active_support/core_ext/object/deep_dup' require 'active_support/inflections' class HashExtTest < ActiveSupport::TestCase @@ -24,56 +25,207 @@ class HashExtTest < ActiveSupport::TestCase def setup @strings = { 'a' => 1, 'b' => 2 } + @nested_strings = { 'a' => { 'b' => { 'c' => 3 } } } @symbols = { :a => 1, :b => 2 } + @nested_symbols = { :a => { :b => { :c => 3 } } } @mixed = { :a => 1, 'b' => 2 } + @nested_mixed = { 'a' => { :b => { 'c' => 3 } } } @fixnums = { 0 => 1, 1 => 2 } + @nested_fixnums = { 0 => { 1 => { 2 => 3} } } @illegal_symbols = { [] => 3 } + @nested_illegal_symbols = { [] => { [] => 3} } + @upcase_strings = { 'A' => 1, 'B' => 2 } + @nested_upcase_strings = { 'A' => { 'B' => { 'C' => 3 } } } end def test_methods h = {} + assert_respond_to h, :transform_keys + assert_respond_to h, :transform_keys! + assert_respond_to h, :deep_transform_keys + assert_respond_to h, :deep_transform_keys! assert_respond_to h, :symbolize_keys assert_respond_to h, :symbolize_keys! + assert_respond_to h, :deep_symbolize_keys + assert_respond_to h, :deep_symbolize_keys! assert_respond_to h, :stringify_keys assert_respond_to h, :stringify_keys! + assert_respond_to h, :deep_stringify_keys + assert_respond_to h, :deep_stringify_keys! assert_respond_to h, :to_options assert_respond_to h, :to_options! end + def test_transform_keys + assert_equal @upcase_strings, @strings.transform_keys{ |key| key.to_s.upcase } + assert_equal @upcase_strings, @symbols.transform_keys{ |key| key.to_s.upcase } + assert_equal @upcase_strings, @mixed.transform_keys{ |key| key.to_s.upcase } + end + + def test_transform_keys_not_mutates + transformed_hash = @mixed.dup + transformed_hash.transform_keys{ |key| key.to_s.upcase } + assert_equal @mixed, transformed_hash + end + + def test_deep_transform_keys + assert_equal @nested_upcase_strings, @nested_symbols.deep_transform_keys{ |key| key.to_s.upcase } + assert_equal @nested_upcase_strings, @nested_strings.deep_transform_keys{ |key| key.to_s.upcase } + assert_equal @nested_upcase_strings, @nested_mixed.deep_transform_keys{ |key| key.to_s.upcase } + end + + def test_deep_transform_keys_not_mutates + transformed_hash = @nested_mixed.deep_dup + transformed_hash.deep_transform_keys{ |key| key.to_s.upcase } + assert_equal @nested_mixed, transformed_hash + end + + def test_transform_keys! + assert_equal @upcase_strings, @symbols.dup.transform_keys!{ |key| key.to_s.upcase } + assert_equal @upcase_strings, @strings.dup.transform_keys!{ |key| key.to_s.upcase } + assert_equal @upcase_strings, @mixed.dup.transform_keys!{ |key| key.to_s.upcase } + end + + def test_transform_keys_with_bang_mutates + transformed_hash = @mixed.dup + transformed_hash.transform_keys!{ |key| key.to_s.upcase } + assert_equal @upcase_strings, transformed_hash + assert_equal @mixed, { :a => 1, "b" => 2 } + end + + def test_deep_transform_keys! + assert_equal @nested_upcase_strings, @nested_symbols.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } + assert_equal @nested_upcase_strings, @nested_strings.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } + assert_equal @nested_upcase_strings, @nested_mixed.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } + end + + def test_deep_transform_keys_with_bang_mutates + transformed_hash = @nested_mixed.deep_dup + transformed_hash.deep_transform_keys!{ |key| key.to_s.upcase } + assert_equal @nested_upcase_strings, transformed_hash + assert_equal @nested_mixed, { 'a' => { :b => { 'c' => 3 } } } + end + def test_symbolize_keys assert_equal @symbols, @symbols.symbolize_keys assert_equal @symbols, @strings.symbolize_keys assert_equal @symbols, @mixed.symbolize_keys end + def test_symbolize_keys_not_mutates + transformed_hash = @mixed.dup + transformed_hash.symbolize_keys + assert_equal @mixed, transformed_hash + end + + def test_deep_symbolize_keys + assert_equal @nested_symbols, @nested_symbols.deep_symbolize_keys + assert_equal @nested_symbols, @nested_strings.deep_symbolize_keys + assert_equal @nested_symbols, @nested_mixed.deep_symbolize_keys + end + + def test_deep_symbolize_keys_not_mutates + transformed_hash = @nested_mixed.deep_dup + transformed_hash.deep_symbolize_keys + assert_equal @nested_mixed, transformed_hash + end + def test_symbolize_keys! assert_equal @symbols, @symbols.dup.symbolize_keys! assert_equal @symbols, @strings.dup.symbolize_keys! assert_equal @symbols, @mixed.dup.symbolize_keys! end + def test_symbolize_keys_with_bang_mutates + transformed_hash = @mixed.dup + transformed_hash.deep_symbolize_keys! + assert_equal @symbols, transformed_hash + assert_equal @mixed, { :a => 1, "b" => 2 } + end + + def test_deep_symbolize_keys! + assert_equal @nested_symbols, @nested_symbols.deep_dup.deep_symbolize_keys! + assert_equal @nested_symbols, @nested_strings.deep_dup.deep_symbolize_keys! + assert_equal @nested_symbols, @nested_mixed.deep_dup.deep_symbolize_keys! + end + + def test_deep_symbolize_keys_with_bang_mutates + transformed_hash = @nested_mixed.deep_dup + transformed_hash.deep_symbolize_keys! + assert_equal @nested_symbols, transformed_hash + assert_equal @nested_mixed, { 'a' => { :b => { 'c' => 3 } } } + end + def test_symbolize_keys_preserves_keys_that_cant_be_symbolized assert_equal @illegal_symbols, @illegal_symbols.symbolize_keys assert_equal @illegal_symbols, @illegal_symbols.dup.symbolize_keys! end + def test_deep_symbolize_keys_preserves_keys_that_cant_be_symbolized + assert_equal @nested_illegal_symbols, @nested_illegal_symbols.deep_symbolize_keys + assert_equal @nested_illegal_symbols, @nested_illegal_symbols.deep_dup.deep_symbolize_keys! + end + def test_symbolize_keys_preserves_fixnum_keys assert_equal @fixnums, @fixnums.symbolize_keys assert_equal @fixnums, @fixnums.dup.symbolize_keys! end + def test_deep_symbolize_keys_preserves_fixnum_keys + assert_equal @nested_fixnums, @nested_fixnums.deep_symbolize_keys + assert_equal @nested_fixnums, @nested_fixnums.deep_dup.deep_symbolize_keys! + end + def test_stringify_keys assert_equal @strings, @symbols.stringify_keys assert_equal @strings, @strings.stringify_keys assert_equal @strings, @mixed.stringify_keys end + def test_stringify_keys_not_mutates + transformed_hash = @mixed.dup + transformed_hash.stringify_keys + assert_equal @mixed, transformed_hash + end + + def test_deep_stringify_keys + assert_equal @nested_strings, @nested_symbols.deep_stringify_keys + assert_equal @nested_strings, @nested_strings.deep_stringify_keys + assert_equal @nested_strings, @nested_mixed.deep_stringify_keys + end + + def test_deep_stringify_keys_not_mutates + transformed_hash = @nested_mixed.deep_dup + transformed_hash.deep_stringify_keys + assert_equal @nested_mixed, transformed_hash + end + def test_stringify_keys! assert_equal @strings, @symbols.dup.stringify_keys! assert_equal @strings, @strings.dup.stringify_keys! assert_equal @strings, @mixed.dup.stringify_keys! end + def test_stringify_keys_with_bang_mutates + transformed_hash = @mixed.dup + transformed_hash.stringify_keys! + assert_equal @strings, transformed_hash + assert_equal @mixed, { :a => 1, "b" => 2 } + end + + def test_deep_stringify_keys! + assert_equal @nested_strings, @nested_symbols.deep_dup.deep_stringify_keys! + assert_equal @nested_strings, @nested_strings.deep_dup.deep_stringify_keys! + assert_equal @nested_strings, @nested_mixed.deep_dup.deep_stringify_keys! + end + + def test_deep_stringify_keys_with_bang_mutates + transformed_hash = @nested_mixed.deep_dup + transformed_hash.deep_stringify_keys! + assert_equal @nested_strings, transformed_hash + assert_equal @nested_mixed, { 'a' => { :b => { 'c' => 3 } } } + end + def test_symbolize_keys_for_hash_with_indifferent_access assert_instance_of Hash, @symbols.with_indifferent_access.symbolize_keys assert_equal @symbols, @symbols.with_indifferent_access.symbolize_keys @@ -81,22 +233,46 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @symbols, @mixed.with_indifferent_access.symbolize_keys end + def test_deep_symbolize_keys_for_hash_with_indifferent_access + assert_instance_of Hash, @nested_symbols.with_indifferent_access.deep_symbolize_keys + assert_equal @nested_symbols, @nested_symbols.with_indifferent_access.deep_symbolize_keys + assert_equal @nested_symbols, @nested_strings.with_indifferent_access.deep_symbolize_keys + assert_equal @nested_symbols, @nested_mixed.with_indifferent_access.deep_symbolize_keys + end + + def test_symbolize_keys_bang_for_hash_with_indifferent_access assert_raise(NoMethodError) { @symbols.with_indifferent_access.dup.symbolize_keys! } assert_raise(NoMethodError) { @strings.with_indifferent_access.dup.symbolize_keys! } assert_raise(NoMethodError) { @mixed.with_indifferent_access.dup.symbolize_keys! } end + def test_deep_symbolize_keys_bang_for_hash_with_indifferent_access + assert_raise(NoMethodError) { @nested_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! } + assert_raise(NoMethodError) { @nested_strings.with_indifferent_access.deep_dup.deep_symbolize_keys! } + assert_raise(NoMethodError) { @nested_mixed.with_indifferent_access.deep_dup.deep_symbolize_keys! } + end + def test_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access assert_equal @illegal_symbols, @illegal_symbols.with_indifferent_access.symbolize_keys assert_raise(NoMethodError) { @illegal_symbols.with_indifferent_access.dup.symbolize_keys! } end + def test_deep_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access + assert_equal @nested_illegal_symbols, @nested_illegal_symbols.with_indifferent_access.deep_symbolize_keys + assert_raise(NoMethodError) { @nested_illegal_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! } + end + def test_symbolize_keys_preserves_fixnum_keys_for_hash_with_indifferent_access assert_equal @fixnums, @fixnums.with_indifferent_access.symbolize_keys assert_raise(NoMethodError) { @fixnums.with_indifferent_access.dup.symbolize_keys! } end + def test_deep_symbolize_keys_preserves_fixnum_keys_for_hash_with_indifferent_access + assert_equal @nested_fixnums, @nested_fixnums.with_indifferent_access.deep_symbolize_keys + assert_raise(NoMethodError) { @nested_fixnums.with_indifferent_access.deep_dup.deep_symbolize_keys! } + end + def test_stringify_keys_for_hash_with_indifferent_access assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.stringify_keys assert_equal @strings, @symbols.with_indifferent_access.stringify_keys @@ -104,6 +280,13 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @strings, @mixed.with_indifferent_access.stringify_keys end + def test_deep_stringify_keys_for_hash_with_indifferent_access + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.deep_stringify_keys + assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_stringify_keys + assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_stringify_keys + assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_stringify_keys + end + def test_stringify_keys_bang_for_hash_with_indifferent_access assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.dup.stringify_keys! assert_equal @strings, @symbols.with_indifferent_access.dup.stringify_keys! @@ -111,6 +294,13 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @strings, @mixed.with_indifferent_access.dup.stringify_keys! end + def test_deep_stringify_keys_bang_for_hash_with_indifferent_access + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.dup.deep_stringify_keys! + assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_dup.deep_stringify_keys! + assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_dup.deep_stringify_keys! + assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_dup.deep_stringify_keys! + end + def test_nested_under_indifferent_access foo = { "foo" => SubclassingHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access assert_kind_of ActiveSupport::HashWithIndifferentAccess, foo["foo"] @@ -297,6 +487,17 @@ class HashExtTest < ActiveSupport::TestCase assert_equal 1, h[:first] end + def test_deep_stringify_and_deep_symbolize_keys_on_indifferent_preserves_hash + h = HashWithIndifferentAccess.new + h[:first] = 1 + h = h.deep_stringify_keys + assert_equal 1, h['first'] + h = HashWithIndifferentAccess.new + h['first'] = 1 + h = h.deep_symbolize_keys + assert_equal 1, h[:first] + end + def test_to_options_on_indifferent_preserves_hash h = HashWithIndifferentAccess.new h['first'] = 1 @@ -672,8 +873,8 @@ class HashToXmlTest < ActiveSupport::TestCase :created_at => Time.utc(1999,2,2), :local_created_at => Time.utc(1999,2,2).in_time_zone('Eastern Time (US & Canada)') }.to_xml(@xml_options) - assert_match %r{<created-at type=\"datetime\">1999-02-02T00:00:00Z</created-at>}, xml - assert_match %r{<local-created-at type=\"datetime\">1999-02-01T19:00:00-05:00</local-created-at>}, xml + assert_match %r{<created-at type=\"dateTime\">1999-02-02T00:00:00Z</created-at>}, xml + assert_match %r{<local-created-at type=\"dateTime\">1999-02-01T19:00:00-05:00</local-created-at>}, xml end def test_multiple_records_from_xml_with_attributes_other_than_type_ignores_them_without_exploding diff --git a/guides/assets/images/belongs_to.png b/guides/assets/images/belongs_to.png Binary files differindex 44243edbca..43c963ffa8 100644 --- a/guides/assets/images/belongs_to.png +++ b/guides/assets/images/belongs_to.png diff --git a/guides/assets/images/book_icon.gif b/guides/assets/images/book_icon.gif Binary files differindex c81d5db520..efc5e06880 100644 --- a/guides/assets/images/book_icon.gif +++ b/guides/assets/images/book_icon.gif diff --git a/guides/assets/images/challenge.png b/guides/assets/images/challenge.png Binary files differindex d163748640..30be3d7028 100644 --- a/guides/assets/images/challenge.png +++ b/guides/assets/images/challenge.png diff --git a/guides/assets/images/chapters_icon.gif b/guides/assets/images/chapters_icon.gif Binary files differindex 06fb415f4a..a61c28c02d 100644 --- a/guides/assets/images/chapters_icon.gif +++ b/guides/assets/images/chapters_icon.gif diff --git a/guides/assets/images/check_bullet.gif b/guides/assets/images/check_bullet.gif Binary files differindex 1fcfeba250..bd54ef64c9 100644 --- a/guides/assets/images/check_bullet.gif +++ b/guides/assets/images/check_bullet.gif diff --git a/guides/assets/images/credits_pic_blank.gif b/guides/assets/images/credits_pic_blank.gif Binary files differindex f6f654fc65..a6b335d0c9 100644 --- a/guides/assets/images/credits_pic_blank.gif +++ b/guides/assets/images/credits_pic_blank.gif diff --git a/guides/assets/images/csrf.png b/guides/assets/images/csrf.png Binary files differindex ab73baafe8..a8123d47c3 100644 --- a/guides/assets/images/csrf.png +++ b/guides/assets/images/csrf.png diff --git a/guides/assets/images/customized_error_messages.png b/guides/assets/images/customized_error_messages.png Binary files differindex fa676991e3..fcf47b4be0 100644 --- a/guides/assets/images/customized_error_messages.png +++ b/guides/assets/images/customized_error_messages.png diff --git a/guides/assets/images/edge_badge.png b/guides/assets/images/edge_badge.png Binary files differindex cddd46c4b8..a35dc9f8ee 100644 --- a/guides/assets/images/edge_badge.png +++ b/guides/assets/images/edge_badge.png diff --git a/guides/assets/images/error_messages.png b/guides/assets/images/error_messages.png Binary files differindex 428892194a..1189e486d4 100644 --- a/guides/assets/images/error_messages.png +++ b/guides/assets/images/error_messages.png diff --git a/guides/assets/images/getting_started/confirm_dialog.png b/guides/assets/images/getting_started/confirm_dialog.png Binary files differindex a26c09ef2d..1a13eddd91 100644 --- a/guides/assets/images/getting_started/confirm_dialog.png +++ b/guides/assets/images/getting_started/confirm_dialog.png diff --git a/guides/assets/images/getting_started/form_with_errors.png b/guides/assets/images/getting_started/form_with_errors.png Binary files differindex badefe6ea6..6910e1647e 100644 --- a/guides/assets/images/getting_started/form_with_errors.png +++ b/guides/assets/images/getting_started/form_with_errors.png diff --git a/guides/assets/images/getting_started/index_action_with_edit_link.png b/guides/assets/images/getting_started/index_action_with_edit_link.png Binary files differindex 6e58a13756..bf23cba231 100644 --- a/guides/assets/images/getting_started/index_action_with_edit_link.png +++ b/guides/assets/images/getting_started/index_action_with_edit_link.png diff --git a/guides/assets/images/getting_started/new_post.png b/guides/assets/images/getting_started/new_post.png Binary files differindex dc9459032a..b573cb164c 100644 --- a/guides/assets/images/getting_started/new_post.png +++ b/guides/assets/images/getting_started/new_post.png diff --git a/guides/assets/images/getting_started/post_with_comments.png b/guides/assets/images/getting_started/post_with_comments.png Binary files differindex bd9b2e10f5..e13095ff8f 100644 --- a/guides/assets/images/getting_started/post_with_comments.png +++ b/guides/assets/images/getting_started/post_with_comments.png diff --git a/guides/assets/images/getting_started/routing_error_no_controller.png b/guides/assets/images/getting_started/routing_error_no_controller.png Binary files differindex 92a39efd78..407ea2ea06 100644 --- a/guides/assets/images/getting_started/routing_error_no_controller.png +++ b/guides/assets/images/getting_started/routing_error_no_controller.png diff --git a/guides/assets/images/getting_started/routing_error_no_route_matches.png b/guides/assets/images/getting_started/routing_error_no_route_matches.png Binary files differindex bc768a94a2..d461807c5d 100644 --- a/guides/assets/images/getting_started/routing_error_no_route_matches.png +++ b/guides/assets/images/getting_started/routing_error_no_route_matches.png diff --git a/guides/assets/images/getting_started/show_action_for_posts.png b/guides/assets/images/getting_started/show_action_for_posts.png Binary files differindex 5c8c4d8e5e..9467df6a07 100644 --- a/guides/assets/images/getting_started/show_action_for_posts.png +++ b/guides/assets/images/getting_started/show_action_for_posts.png diff --git a/guides/assets/images/getting_started/template_is_missing_posts_new.png b/guides/assets/images/getting_started/template_is_missing_posts_new.png Binary files differindex 9f070d59db..6860aaeca7 100644 --- a/guides/assets/images/getting_started/template_is_missing_posts_new.png +++ b/guides/assets/images/getting_started/template_is_missing_posts_new.png diff --git a/guides/assets/images/getting_started/undefined_method_post_path.png b/guides/assets/images/getting_started/undefined_method_post_path.png Binary files differindex f568bf315c..c29cb2f54f 100644 --- a/guides/assets/images/getting_started/undefined_method_post_path.png +++ b/guides/assets/images/getting_started/undefined_method_post_path.png diff --git a/guides/assets/images/getting_started/unknown_action_create_for_posts.png b/guides/assets/images/getting_started/unknown_action_create_for_posts.png Binary files differindex 03d92dfb7d..1eca14b988 100644 --- a/guides/assets/images/getting_started/unknown_action_create_for_posts.png +++ b/guides/assets/images/getting_started/unknown_action_create_for_posts.png diff --git a/guides/assets/images/getting_started/unknown_action_new_for_posts.png b/guides/assets/images/getting_started/unknown_action_new_for_posts.png Binary files differindex b63883d922..fd72586573 100644 --- a/guides/assets/images/getting_started/unknown_action_new_for_posts.png +++ b/guides/assets/images/getting_started/unknown_action_new_for_posts.png diff --git a/guides/assets/images/grey_bullet.gif b/guides/assets/images/grey_bullet.gif Binary files differindex e75e8e93a1..3c08b1571c 100644 --- a/guides/assets/images/grey_bullet.gif +++ b/guides/assets/images/grey_bullet.gif diff --git a/guides/assets/images/habtm.png b/guides/assets/images/habtm.png Binary files differindex fea78b0b5c..b062bc73fe 100644 --- a/guides/assets/images/habtm.png +++ b/guides/assets/images/habtm.png diff --git a/guides/assets/images/has_many.png b/guides/assets/images/has_many.png Binary files differindex 6cff58460d..e7589e3b75 100644 --- a/guides/assets/images/has_many.png +++ b/guides/assets/images/has_many.png diff --git a/guides/assets/images/has_many_through.png b/guides/assets/images/has_many_through.png Binary files differindex 85d7599925..858c898dc1 100644 --- a/guides/assets/images/has_many_through.png +++ b/guides/assets/images/has_many_through.png diff --git a/guides/assets/images/has_one.png b/guides/assets/images/has_one.png Binary files differindex a70ddaaa86..93faa05b07 100644 --- a/guides/assets/images/has_one.png +++ b/guides/assets/images/has_one.png diff --git a/guides/assets/images/has_one_through.png b/guides/assets/images/has_one_through.png Binary files differindex 89a7617a30..07dac1a27d 100644 --- a/guides/assets/images/has_one_through.png +++ b/guides/assets/images/has_one_through.png diff --git a/guides/assets/images/header_backdrop.png b/guides/assets/images/header_backdrop.png Binary files differindex ff2982175e..72b030478f 100644 --- a/guides/assets/images/header_backdrop.png +++ b/guides/assets/images/header_backdrop.png diff --git a/guides/assets/images/i18n/demo_html_safe.png b/guides/assets/images/i18n/demo_html_safe.png Binary files differindex f881f60dac..9afa8ebec1 100644 --- a/guides/assets/images/i18n/demo_html_safe.png +++ b/guides/assets/images/i18n/demo_html_safe.png diff --git a/guides/assets/images/i18n/demo_localized_pirate.png b/guides/assets/images/i18n/demo_localized_pirate.png Binary files differindex 9134709573..bf8d0b558c 100644 --- a/guides/assets/images/i18n/demo_localized_pirate.png +++ b/guides/assets/images/i18n/demo_localized_pirate.png diff --git a/guides/assets/images/i18n/demo_translated_en.png b/guides/assets/images/i18n/demo_translated_en.png Binary files differindex ecdd878d38..e887bfa306 100644 --- a/guides/assets/images/i18n/demo_translated_en.png +++ b/guides/assets/images/i18n/demo_translated_en.png diff --git a/guides/assets/images/i18n/demo_translated_pirate.png b/guides/assets/images/i18n/demo_translated_pirate.png Binary files differindex 41c580923a..aa5618a865 100644 --- a/guides/assets/images/i18n/demo_translated_pirate.png +++ b/guides/assets/images/i18n/demo_translated_pirate.png diff --git a/guides/assets/images/i18n/demo_translation_missing.png b/guides/assets/images/i18n/demo_translation_missing.png Binary files differindex af9e2d0427..867aa7c42d 100644 --- a/guides/assets/images/i18n/demo_translation_missing.png +++ b/guides/assets/images/i18n/demo_translation_missing.png diff --git a/guides/assets/images/i18n/demo_untranslated.png b/guides/assets/images/i18n/demo_untranslated.png Binary files differindex 3603f43463..2ea6404822 100644 --- a/guides/assets/images/i18n/demo_untranslated.png +++ b/guides/assets/images/i18n/demo_untranslated.png diff --git a/guides/assets/images/icons/callouts/1.png b/guides/assets/images/icons/callouts/1.png Binary files differindex 7d473430b7..c5d02adcf4 100644 --- a/guides/assets/images/icons/callouts/1.png +++ b/guides/assets/images/icons/callouts/1.png diff --git a/guides/assets/images/icons/callouts/10.png b/guides/assets/images/icons/callouts/10.png Binary files differindex 997bbc8246..fe89f9ef83 100644 --- a/guides/assets/images/icons/callouts/10.png +++ b/guides/assets/images/icons/callouts/10.png diff --git a/guides/assets/images/icons/callouts/11.png b/guides/assets/images/icons/callouts/11.png Binary files differindex ce47dac3f5..9244a1ac4b 100644 --- a/guides/assets/images/icons/callouts/11.png +++ b/guides/assets/images/icons/callouts/11.png diff --git a/guides/assets/images/icons/callouts/12.png b/guides/assets/images/icons/callouts/12.png Binary files differindex 31daf4e2f2..ae56459f4c 100644 --- a/guides/assets/images/icons/callouts/12.png +++ b/guides/assets/images/icons/callouts/12.png diff --git a/guides/assets/images/icons/callouts/13.png b/guides/assets/images/icons/callouts/13.png Binary files differindex 14021a89c2..1181f9f892 100644 --- a/guides/assets/images/icons/callouts/13.png +++ b/guides/assets/images/icons/callouts/13.png diff --git a/guides/assets/images/icons/callouts/14.png b/guides/assets/images/icons/callouts/14.png Binary files differindex 64014b75fe..4274e6580a 100644 --- a/guides/assets/images/icons/callouts/14.png +++ b/guides/assets/images/icons/callouts/14.png diff --git a/guides/assets/images/icons/callouts/15.png b/guides/assets/images/icons/callouts/15.png Binary files differindex 0d65765fcf..39304de94f 100644 --- a/guides/assets/images/icons/callouts/15.png +++ b/guides/assets/images/icons/callouts/15.png diff --git a/guides/assets/images/icons/callouts/2.png b/guides/assets/images/icons/callouts/2.png Binary files differindex 5d09341b2f..8c57970ba9 100644 --- a/guides/assets/images/icons/callouts/2.png +++ b/guides/assets/images/icons/callouts/2.png diff --git a/guides/assets/images/icons/callouts/3.png b/guides/assets/images/icons/callouts/3.png Binary files differindex ef7b700471..57a33d15b4 100644 --- a/guides/assets/images/icons/callouts/3.png +++ b/guides/assets/images/icons/callouts/3.png diff --git a/guides/assets/images/icons/callouts/4.png b/guides/assets/images/icons/callouts/4.png Binary files differindex adb8364eb5..f061ab02b8 100644 --- a/guides/assets/images/icons/callouts/4.png +++ b/guides/assets/images/icons/callouts/4.png diff --git a/guides/assets/images/icons/callouts/5.png b/guides/assets/images/icons/callouts/5.png Binary files differindex 4d7eb46002..b4de02da11 100644 --- a/guides/assets/images/icons/callouts/5.png +++ b/guides/assets/images/icons/callouts/5.png diff --git a/guides/assets/images/icons/callouts/6.png b/guides/assets/images/icons/callouts/6.png Binary files differindex 0ba694af6c..0e055eec1e 100644 --- a/guides/assets/images/icons/callouts/6.png +++ b/guides/assets/images/icons/callouts/6.png diff --git a/guides/assets/images/icons/callouts/7.png b/guides/assets/images/icons/callouts/7.png Binary files differindex 472e96f8ac..5ead87d040 100644 --- a/guides/assets/images/icons/callouts/7.png +++ b/guides/assets/images/icons/callouts/7.png diff --git a/guides/assets/images/icons/callouts/8.png b/guides/assets/images/icons/callouts/8.png Binary files differindex 5e60973c21..cb99545eb6 100644 --- a/guides/assets/images/icons/callouts/8.png +++ b/guides/assets/images/icons/callouts/8.png diff --git a/guides/assets/images/icons/callouts/9.png b/guides/assets/images/icons/callouts/9.png Binary files differindex a0676d26cc..0ac03602f6 100644 --- a/guides/assets/images/icons/callouts/9.png +++ b/guides/assets/images/icons/callouts/9.png diff --git a/guides/assets/images/icons/caution.png b/guides/assets/images/icons/caution.png Binary files differindex cb9d5ea0df..031e19c776 100644 --- a/guides/assets/images/icons/caution.png +++ b/guides/assets/images/icons/caution.png diff --git a/guides/assets/images/icons/example.png b/guides/assets/images/icons/example.png Binary files differindex bba1c0010d..1b0e482059 100644 --- a/guides/assets/images/icons/example.png +++ b/guides/assets/images/icons/example.png diff --git a/guides/assets/images/icons/home.png b/guides/assets/images/icons/home.png Binary files differindex 37a5231bac..24149d6e78 100644 --- a/guides/assets/images/icons/home.png +++ b/guides/assets/images/icons/home.png diff --git a/guides/assets/images/icons/important.png b/guides/assets/images/icons/important.png Binary files differindex 1096c23295..dafcf0f59e 100644 --- a/guides/assets/images/icons/important.png +++ b/guides/assets/images/icons/important.png diff --git a/guides/assets/images/icons/next.png b/guides/assets/images/icons/next.png Binary files differindex 64e126bdda..355b329f5a 100644 --- a/guides/assets/images/icons/next.png +++ b/guides/assets/images/icons/next.png diff --git a/guides/assets/images/icons/note.png b/guides/assets/images/icons/note.png Binary files differindex 841820f7c4..08d35a6f5c 100644 --- a/guides/assets/images/icons/note.png +++ b/guides/assets/images/icons/note.png diff --git a/guides/assets/images/icons/prev.png b/guides/assets/images/icons/prev.png Binary files differindex 3e8f12fe24..ea564c865e 100644 --- a/guides/assets/images/icons/prev.png +++ b/guides/assets/images/icons/prev.png diff --git a/guides/assets/images/icons/tip.png b/guides/assets/images/icons/tip.png Binary files differindex a3a029d898..d834e6d1bb 100644 --- a/guides/assets/images/icons/tip.png +++ b/guides/assets/images/icons/tip.png diff --git a/guides/assets/images/icons/up.png b/guides/assets/images/icons/up.png Binary files differindex 2db1ce62fa..379f0045af 100644 --- a/guides/assets/images/icons/up.png +++ b/guides/assets/images/icons/up.png diff --git a/guides/assets/images/icons/warning.png b/guides/assets/images/icons/warning.png Binary files differindex 0b0c419df2..72a8a5d873 100644 --- a/guides/assets/images/icons/warning.png +++ b/guides/assets/images/icons/warning.png diff --git a/guides/assets/images/nav_arrow.gif b/guides/assets/images/nav_arrow.gif Binary files differindex c4f57658d7..ff081819ad 100644 --- a/guides/assets/images/nav_arrow.gif +++ b/guides/assets/images/nav_arrow.gif diff --git a/guides/assets/images/oscardelben.jpg b/guides/assets/images/oscardelben.jpg Binary files differnew file mode 100644 index 0000000000..9f3f67c2c7 --- /dev/null +++ b/guides/assets/images/oscardelben.jpg diff --git a/guides/assets/images/polymorphic.png b/guides/assets/images/polymorphic.png Binary files differindex ff2fd9f76d..a3cbc4502a 100644 --- a/guides/assets/images/polymorphic.png +++ b/guides/assets/images/polymorphic.png diff --git a/guides/assets/images/rails_guides_logo.gif b/guides/assets/images/rails_guides_logo.gif Binary files differindex a24683a34e..9b0ad5af28 100644 --- a/guides/assets/images/rails_guides_logo.gif +++ b/guides/assets/images/rails_guides_logo.gif diff --git a/guides/assets/images/rails_welcome.png b/guides/assets/images/rails_welcome.png Binary files differindex f2aa210d19..8ad2d351de 100644 --- a/guides/assets/images/rails_welcome.png +++ b/guides/assets/images/rails_welcome.png diff --git a/guides/assets/images/session_fixation.png b/guides/assets/images/session_fixation.png Binary files differindex 6b084508db..ac3ab01614 100644 --- a/guides/assets/images/session_fixation.png +++ b/guides/assets/images/session_fixation.png diff --git a/guides/assets/images/tab_grey.gif b/guides/assets/images/tab_grey.gif Binary files differindex e9680b7136..995adb76cf 100644 --- a/guides/assets/images/tab_grey.gif +++ b/guides/assets/images/tab_grey.gif diff --git a/guides/assets/images/tab_info.gif b/guides/assets/images/tab_info.gif Binary files differindex 458fea9a61..e9dd164f18 100644 --- a/guides/assets/images/tab_info.gif +++ b/guides/assets/images/tab_info.gif diff --git a/guides/assets/images/tab_note.gif b/guides/assets/images/tab_note.gif Binary files differindex 1d5c171ed6..f9b546c6f8 100644 --- a/guides/assets/images/tab_note.gif +++ b/guides/assets/images/tab_note.gif diff --git a/guides/assets/images/tab_red.gif b/guides/assets/images/tab_red.gif Binary files differindex daf140b5a8..0613093ddc 100644 --- a/guides/assets/images/tab_red.gif +++ b/guides/assets/images/tab_red.gif diff --git a/guides/assets/images/tab_yellow.gif b/guides/assets/images/tab_yellow.gif Binary files differindex dc961c99dd..39a3c2dc6a 100644 --- a/guides/assets/images/tab_yellow.gif +++ b/guides/assets/images/tab_yellow.gif diff --git a/guides/assets/images/tab_yellow.png b/guides/assets/images/tab_yellow.png Binary files differindex cceea6581f..3ab1c56c4d 100644 --- a/guides/assets/images/tab_yellow.png +++ b/guides/assets/images/tab_yellow.png diff --git a/guides/assets/images/validation_error_messages.png b/guides/assets/images/validation_error_messages.png Binary files differindex 622d35da5d..30e4ca4a3d 100644 --- a/guides/assets/images/validation_error_messages.png +++ b/guides/assets/images/validation_error_messages.png diff --git a/guides/source/active_support_core_extensions.textile b/guides/source/active_support_core_extensions.textile index 80faffa49c..587f65529e 100644 --- a/guides/source/active_support_core_extensions.textile +++ b/guides/source/active_support_core_extensions.textile @@ -2549,6 +2549,45 @@ There's also the bang variant +except!+ that removes keys in the very receiver. NOTE: Defined in +active_support/core_ext/hash/except.rb+. +h5. +transform_keys+ and +transform_keys!+ + +The method +transform_keys+ accepts a block and returns a hash that has applied the block operations to each of the keys in the receiver: + +<ruby> +{nil => nil, 1 => 1, :a => :a}.transform_keys{ |key| key.to_s.upcase } +# => {"" => nil, "A" => :a, "1" => 1} +</ruby> + +The result in case of collision is undefined: + +<ruby> +{"a" => 1, :a => 2}.transform_keys{ |key| key.to_s.upcase } +# => {"A" => 2}, in my test, can't rely on this result though +</ruby> + +This method may be useful for example to build specialized conversions. For instance +stringify_keys+ and +symbolize_keys+ use +transform_keys+ to perform their key conversions: + +<ruby> +def stringify_keys + transform_keys{ |key| key.to_s } +end +... +def symbolize_keys + transform_keys{ |key| key.to_sym rescue key } +end +</ruby> + +There's also the bang variant +transform_keys!+ that applies the block operations to keys in the very receiver. + +Besides that, one can use +deep_transform_keys+ and +deep_transform_keys!+ to perform the block operation on all the keys in the given hash and all the hashes nested into it. An example of the result is: + +<ruby> +{nil => nil, 1 => 1, :nested => {:a => 3, 5 => 5}}.deep_transform_keys{ |key| key.to_s.upcase } +# => {""=>nil, "1"=>1, "NESTED"=>{"A"=>3, "5"=>5}} +</ruby> + +NOTE: Defined in +active_support/core_ext/hash/keys.rb+. + h5. +stringify_keys+ and +stringify_keys!+ The method +stringify_keys+ returns a hash that has a stringified version of the keys in the receiver. It does so by sending +to_s+ to them: @@ -2579,6 +2618,13 @@ The second line can safely access the "type" key, and let the user to pass eithe There's also the bang variant +stringify_keys!+ that stringifies keys in the very receiver. +Besides that, one can use +deep_stringify_keys+ and +deep_stringify_keys!+ to stringify all the keys in the given hash and all the hashes nested into it. An example of the result is: + +<ruby> +{nil => nil, 1 => 1, :nested => {:a => 3, 5 => 5}}.deep_stringify_keys +# => {""=>nil, "1"=>1, "nested"=>{"a"=>3, "5"=>5}} +</ruby> + NOTE: Defined in +active_support/core_ext/hash/keys.rb+. h5. +symbolize_keys+ and +symbolize_keys!+ @@ -2613,6 +2659,13 @@ The second line can safely access the +:params+ key, and let the user to pass ei There's also the bang variant +symbolize_keys!+ that symbolizes keys in the very receiver. +Besides that, one can use +deep_symbolize_keys+ and +deep_symbolize_keys!+ to symbolize all the keys in the given hash and all the hashes nested into it. An example of the result is: + +<ruby> +{nil => nil, 1 => 1, "nested" => {"a" => 3, 5 => 5}}.deep_symbolize_keys +# => {nil=>nil, 1=>1, :nested=>{:a=>3, 5=>5}} +</ruby> + NOTE: Defined in +active_support/core_ext/hash/keys.rb+. h5. +to_options+ and +to_options!+ diff --git a/guides/source/configuring.textile b/guides/source/configuring.textile index f114075cae..af46538bf5 100644 --- a/guides/source/configuring.textile +++ b/guides/source/configuring.textile @@ -585,7 +585,7 @@ After loading the framework and any gems in your application, Rails turns to loa NOTE: You can use subfolders to organize your initializers if you like, because Rails will look into the whole file hierarchy from the initializers folder on down. -TIP: If you have any ordering dependency in your initializers, you can control the load order by naming. For example, +01_critical.rb+ will be loaded before +02_normal.rb+. +TIP: If you have any ordering dependency in your initializers, you can control the load order through naming. Initializer files are loaded in alphabetical order by their path. For example, +01_critical.rb+ will be loaded before +02_normal.rb+. h3. Initialization events diff --git a/guides/source/contributing_to_ruby_on_rails.textile b/guides/source/contributing_to_ruby_on_rails.textile index a2254a550c..72cdea885f 100644 --- a/guides/source/contributing_to_ruby_on_rails.textile +++ b/guides/source/contributing_to_ruby_on_rails.textile @@ -430,6 +430,35 @@ h4. Iterate as Necessary It’s entirely possible that the feedback you get will suggest changes. Don’t get discouraged: the whole point of contributing to an active open source project is to tap into community knowledge. If people are encouraging you to tweak your code, then it’s worth making the tweaks and resubmitting. If the feedback is that your code doesn’t belong in the core, you might still think about releasing it as a gem. +h4. Backporting + +Changes that are merged into master are intended for the next major release of Rails. Sometimes, it might be beneficial for your changes to propagate back to the maintenance releases for older stable branches. Generally, security fixes and bug fixes are good candidates for a backport, while new features and patches that introduce a change in behavior will not be accepted. When in doubt, it is best to consult a rails team member before backporting your changes to avoid wasted effort. + +For simple fixes, the easiest way to backport your change is to "extract a diff from your changes in master and apply them to the target branch":http://ariejan.net/2009/10/26/how-to-create-and-apply-a-patch-with-git. + +First make sure your changes are the only difference between your current branch and master: + +<shell> +$ git log master..HEAD +</shell> + +Then extract the diff: + +<shell> +$ git format-patch master --stdout > ~/my_changes.patch +</shell> + +Switch over to the target branch and apply your changes: + +<shell> +$ git checkout -b my_backport_branch 3-2-stable +$ git apply ~/my_changes.patch +</shell> + +This works well for simple changes. However, if your changes are complicated or if the code in master has deviated significantly from your target branch, it might require more work on your part. The difficulty of a backport varies greatly from case to case, and sometimes it is simply not worth the effort. + +Once you have resolved all conflicts and made sure all the tests are passing, push your changes and open a separate pull request for your backport. It is also worth noting that older branches might have a different set of build targets than master. When possible, it is best to first test your backport locally against the ruby versions listed in +.travis.yml+ before submitting your pull request. + And then ... think about your next contribution! h3. Rails Contributors diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb index da6bd6acdf..04deec6a11 100644 --- a/guides/source/credits.html.erb +++ b/guides/source/credits.html.erb @@ -31,6 +31,10 @@ Ruby on Rails Guides: Credits Ryan Bigg works as a consultant at <a href="http://rubyx.com">RubyX</a> and has been working with Rails since 2006. He's co-authoring a book called <a href="http://manning.com/katz">Rails 3 in Action</a> and he's written many gems which can be seen on <a href="http://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>. <% end %> +<%= author('Oscar Del Ben', 'oscardelben', 'oscardelben.jpg') do %> +Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wildfire</a>. He's a regular open source contributor (<a href="https://github.com/oscardelben">Github account</a>) and tweets regularly at <a href="https://twitter.com/oscardelben">@oscardelben</a>. + <% end %> + <%= author('Frederick Cheung', 'fcheung') do %> Frederick Cheung is Chief Wizard at Texperts where he has been using Rails since 2006. He is based in Cambridge (UK) and when not consuming fine ales he blogs at <a href="http://www.spacevatican.org">spacevatican.org</a>. <% end %> diff --git a/guides/source/debugging_rails_applications.textile b/guides/source/debugging_rails_applications.textile index 45fa4ada78..0802a2db26 100644 --- a/guides/source/debugging_rails_applications.textile +++ b/guides/source/debugging_rails_applications.textile @@ -698,7 +698,7 @@ There are some Rails plugins to help you to find errors and debug your applicati h3. References -* "ruby-debug Homepage":http://www.datanoise.com/ruby-debug +* "ruby-debug Homepage":http://bashdb.sourceforge.net/ruby-debug/home-page.html * "debugger Homepage":http://github.com/cldwalker/debugger * "Article: Debugging a Rails application with ruby-debug":http://www.sitepoint.com/article/debug-rails-app-ruby-debug/ * "ruby-debug Basics screencast":http://brian.maybeyoureinsane.net/blog/2007/05/07/ruby-debug-basics-screencast/ diff --git a/guides/source/getting_started.textile b/guides/source/getting_started.textile index e25dac22da..1799d55a7a 100644 --- a/guides/source/getting_started.textile +++ b/guides/source/getting_started.textile @@ -13,8 +13,6 @@ endprologue. WARNING. This Guide is based on Rails 3.2. Some of the code shown here will not work in earlier versions of Rails. -WARNING: The Edge version of this guide is currently being re-worked. Please excuse us while we re-arrange the place. - h3. Guide Assumptions This guide is designed for beginners who want to get started with a Rails @@ -179,7 +177,28 @@ To create a new controller, you will need to run the "controller" generator and $ rails generate controller welcome index </shell> -Rails will create several files for you. Most important of these are of course the controller, located at +app/controllers/welcome_controller.rb+ and the view, located at +app/views/welcome/index.html.erb+. +Rails will create several files and a route for you. + +<shell> +create app/controllers/welcome_controller.rb + route get "welcome/index" +invoke erb +create app/views/welcome +create app/views/welcome/index.html.erb +invoke test_unit +create test/functional/welcome_controller_test.rb +invoke helper +create app/helpers/welcome_helper.rb +invoke test_unit +create test/unit/helpers/welcome_helper_test.rb +invoke assets +invoke coffee +create app/assets/javascripts/welcome.js.coffee +invoke scss +create app/assets/stylesheets/welcome.css.scss +</shell> + +Most important of these are of course the controller, located at +app/controllers/welcome_controller.rb+ and the view, located at +app/views/welcome/index.html.erb+. Open the +app/views/welcome/index.html.erb+ file in your text editor and edit it to contain a single line of code: @@ -197,18 +216,27 @@ You need to do this because Rails will serve any static file in the +public+ dir Next, you have to tell Rails where your actual home page is located. -Open the file +config/routes.rb+ in your editor. This is your application's _routing file_ which holds entries in a special DSL (domain-specific language) that tells Rails how to connect incoming requests to controllers and actions. This file contains many sample routes on commented lines, and one of them actually shows you how to connect the root of your site to a specific controller and action. Find the line beginning with +root :to+ and uncomment it. It should look something like the following: +Open the file +config/routes.rb+ in your editor. <ruby> Blog::Application.routes.draw do - - #... + get "welcome/index" + + # The priority is based upon order of creation: + # first created -> highest priority. + # ... # You can have the root of your site routed with "root" # just remember to delete public/index.html. - root :to => "welcome#index" + # root :to => "welcome#index" +</ruby> + +This is your application's _routing file_ which holds entries in a special DSL (domain-specific language) that tells Rails how to connect incoming requests to controllers and actions. This file contains many sample routes on commented lines, and one of them actually shows you how to connect the root of your site to a specific controller and action. Find the line beginning with +root :to+ and uncomment it. It should look something like the following: + +<ruby> +root :to => "welcome#index" </ruby> -The +root :to => "welcome#index"+ tells Rails to map requests to the root of the application to the welcome controller's index action. This was created earlier when you ran the controller generator (+rails generate controller welcome index+). +The +root :to => "welcome#index"+ tells Rails to map requests to the root of the application to the welcome controller's index action and +get "welcome/index"+ tells Rails to map requests to "http://localhost:3000/welcome/index":http://localhost:3000/welcome/index to the welcome controller's index action. This was created earlier when you ran the controller generator (+rails generate controller welcome index+). If you navigate to "http://localhost:3000":http://localhost:3000 in your browser, you'll see the +Hello, Rails!+ message you put into +app/views/welcome/index.html.erb+, indicating that this new route is indeed going to +WelcomeController+'s +index+ action and is rendering the view correctly. diff --git a/guides/source/initialization.textile b/guides/source/initialization.textile index 155a439e64..12b2eb7458 100644 --- a/guides/source/initialization.textile +++ b/guides/source/initialization.textile @@ -1,13 +1,15 @@ h2. The Rails Initialization Process -This guide explains the internals of the initialization process in Rails as of Rails 3.1. It is an extremely in-depth guide and recommended for advanced Rails developers. +This guide explains the internals of the initialization process in Rails +as of Rails 4. It is an extremely in-depth guide and recommended for advanced Rails developers. * Using +rails server+ * Using Passenger endprologue. -This guide goes through every single file, class and method call that is required to boot up the Ruby on Rails stack for a default Rails 3.1 application, explaining each part in detail along the way. For this guide, we will be focusing on how the two most common methods (+rails server+ and Passenger) boot a Rails application. +This guide goes through every single file, class and method call that is +required to boot up the Ruby on Rails stack for a default Rails 4 application, explaining each part in detail along the way. For this guide, we will be focusing on how the two most common methods (+rails server+ and Passenger) boot a Rails application. NOTE: Paths in this guide are relative to Rails or a Rails application unless otherwise specified. @@ -22,16 +24,15 @@ The actual +rails+ command is kept in _bin/rails_: <ruby> #!/usr/bin/env ruby -begin - require "rails/cli" -rescue LoadError - railties_path = File.expand_path('../../railties/lib', __FILE__) +if File.exists?(File.join(File.expand_path('../../..', __FILE__), '.git')) + railties_path = File.expand_path('../../lib', __FILE__) $:.unshift(railties_path) - require "rails/cli" end +require "rails/cli" </ruby> -This file will attempt to load +rails/cli+. If it cannot find it then +railties/lib+ is added to the load path (+$:+) before retrying. +This file will first attempt to push the +railties/lib+ directory if +present, and then require +rails/cli+. h4. +railties/lib/rails/cli.rb+ @@ -46,7 +47,7 @@ require 'rails/script_rails_loader' Rails::ScriptRailsLoader.exec_script_rails! require 'rails/ruby_version_check' -Signal.trap("INT") { puts; exit } +Signal.trap("INT") { puts; exit(1) } if ARGV.first == 'plugin' ARGV.shift @@ -56,7 +57,7 @@ else end </ruby> -The +rbconfig+ file from the Ruby standard library provides us with the +RbConfig+ class which contains detailed information about the Ruby environment, including how Ruby was compiled. We can see this in use in +railties/lib/rails/script_rails_loader+. +The +rbconfig+ file from the Ruby standard library provides us with the +RbConfig+ class which contains detailed information about the Ruby environment, including how Ruby was compiled. We can see thisin use in +railties/lib/rails/script_rails_loader+. <ruby> require 'pathname' @@ -120,6 +121,9 @@ exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application? This is effectively the same as running +ruby script/rails [arguments]+, where +[arguments]+ at this point in time is simply "server". +TIP: If you execute +script/rails+ directly from your Rails app you will +avoid executing the code that we just described. + h4. +script/rails+ This file is as follows: @@ -134,23 +138,23 @@ The +APP_PATH+ constant will be used later in +rails/commands+. The +config/boot h4. +config/boot.rb+ -+config/boot.rb+ contains this: ++config/boot.rb+ contains: <ruby> # Set up gems listed in the Gemfile. -gemfile = File.expand_path('../../Gemfile', __FILE__) -begin - ENV['BUNDLE_GEMFILE'] = gemfile - require 'bundler' - Bundler.setup -rescue Bundler::GemNotFound => e - STDERR.puts e.message - STDERR.puts "Try running `bundle install`." - exit! -end if File.exist?(gemfile) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + +require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) </ruby> -In a standard Rails application, there's a +Gemfile+ which declares all dependencies of the application. +config/boot.rb+ sets +ENV["BUNDLE_GEMFILE"]+ to the location of this file, then requires Bundler and calls +Bundler.setup+ which adds the dependencies of the application (including all the Rails parts) to the load path, making them available for the application to load. The gems that a Rails 3.1 application depends on are as follows: +In a standard Rails application, there's a +Gemfile+ which declares all +dependencies of the application. +config/boot.rb+ sets ++ENV['BUNDLE_GEMFILE']+ to the location of this file. If the Gemfile +exists, +bundler/setup+ is then required. + +The gems that a Rails 4 application depends on are as follows: + +TODO: change these when the Rails 4 release is near. * abstract (1.0.0) * actionmailer (3.1.0.beta) @@ -183,6 +187,8 @@ h4. +rails/commands.rb+ Once +config/boot.rb+ has finished, the next file that is required is +rails/commands+ which will execute a command based on the arguments passed in. In this case, the +ARGV+ array simply contains +server+ which is extracted into the +command+ variable using these lines: <ruby> +ARGV << '--help' if ARGV.empty? + aliases = { "g" => "generate", "c" => "console", @@ -195,6 +201,9 @@ command = ARGV.shift command = aliases[command] || command </ruby> +TIP: As you can see, an empty ARGV list will make Rails show the help +snippet. + If we used <tt>s</tt> rather than +server+, Rails will use the +aliases+ defined in the file and match them to their respective commands. With the +server+ command, Rails will run this code: <ruby> @@ -361,8 +370,9 @@ This method is defined like this: <ruby> def start + url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}" - puts "=> Rails #{Rails.version} application starting in #{Rails.env} on http://#{options[:Host]}:#{options[:Port]}" + puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}" puts "=> Call with -d to detach" unless options[:daemonize] trap(:INT) { exit } puts "=> Ctrl-C to shutdown server" unless options[:daemonize] @@ -372,6 +382,15 @@ def start FileUtils.mkdir_p(Rails.root.join('tmp', dir_to_make)) end + unless options[:daemonize] + wrapped_app # touch the app so the logger is set up + + console = ActiveSupport::Logger.new($stdout) + console.formatter = Rails.logger.formatter + + Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) + end + super ensure # The '-h' option calls exit before @options is set. @@ -380,10 +399,18 @@ ensure end </ruby> -This is where the first output of the Rails initialization happens. This method creates a trap for +INT+ signals, so if you +CTRL+C+ the server, it will exit the process. As we can see from the code here, it will create the +tmp/cache+, +tmp/pids+, +tmp/sessions+ and +tmp/sockets+ directories if they don't already exist prior to calling +super+. The +super+ method will call +Rack::Server.start+ which begins its definition like this: +This is where the first output of the Rails initialization happens. This +method creates a trap for +INT+ signals, so if you +CTRL-C+ the server, +it will exit the process. As we can see from the code here, it will +create the +tmp/cache+, +tmp/pids+, +tmp/sessions+ and +tmp/sockets+ +directories. It then calls +wrapped_app+ which is responsible for +creating the Rack app, before creating and assignig an +instance of +ActiveSupport::Logger+. + +The +super+ method will call +Rack::Server.start+ which begins its definition like this: <ruby> -def start +def start &blk if options[:warn] $-w = true end @@ -403,22 +430,37 @@ def start pp wrapped_app pp app end -end -</ruby> -In a Rails application, these options are not set at all and therefore aren't used at all. The first line of code that's executed in this method is a call to this method: + check_pid! if options[:pid] -<ruby> -wrapped_app + # Touch the wrapped app, so that the config.ru is loaded before + # daemonization (i.e. before chdir, etc). + wrapped_app + + daemonize_app if options[:daemonize] + + write_pid if options[:pid] + + trap(:INT) do + if server.respond_to?(:shutdown) + server.shutdown + else + exit + end + end + + server.run wrapped_app, options, &blk +end </ruby> -This method calls another method: +The interesting part for a Rails app is the last line, +server.run+. Here we encounter the +wrapped_app+ method again, which this time +we're going to explore more. <ruby> @wrapped_app ||= build_app app </ruby> -Then the +app+ method here is defined like so: +The +app+ method here is defined like so: <ruby> def app @@ -440,7 +482,7 @@ The +options[:config]+ value defaults to +config.ru+ which contains this: # This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) -run YourApp::Application +run <%= app_const %> </ruby> diff --git a/rails.gemspec b/rails.gemspec index 8314036ad1..52f92f46c6 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -9,6 +9,7 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 1.9.3' s.required_rubygems_version = ">= 1.8.11" + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index ccccc178c5..787bafea04 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,5 +1,7 @@ ## Rails 4.0.0 (unreleased) ## +* Add `/rails/info/routes` path, displays same information as `rake routes` *Richard Schneeman & Andrew White* + * Improved `rake routes` output for redirects *Łukasz Strzałkowski & Andrew White* * Load all environments available in `config.paths["config/environments"]`. *Piotr Sarnacki* diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 5c52235318..d44465e547 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -97,7 +97,7 @@ module Rails # Rails application, you will need to add lib to $LOAD_PATH on your own in case # you need to load files in lib/ during the application configuration as well. def add_lib_to_load_path! #:nodoc: - path = config.root.join('lib').to_s + path = File.join config.root, 'lib' $LOAD_PATH.unshift(path) if File.exists?(path) end diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 84f2601f28..60aa40b92f 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -23,6 +23,8 @@ module Rails if Rails.env.development? app.routes.append do get '/rails/info/properties' => "rails/info#properties" + get '/rails/info/routes' => "rails/info#routes" + get '/rails/info' => "rails/info#index" end end end diff --git a/railties/lib/rails/application/route_inspector.rb b/railties/lib/rails/application/route_inspector.rb index b23fb3e920..942c4f4789 100644 --- a/railties/lib/rails/application/route_inspector.rb +++ b/railties/lib/rails/application/route_inspector.rb @@ -51,7 +51,7 @@ module Rails end def internal? - path =~ %r{/rails/info/properties|^#{Rails.application.config.assets.prefix}} + path =~ %r{/rails/info.*|^#{Rails.application.config.assets.prefix}} end def engine? diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index 4c4caad69f..e68d2e05c5 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -69,7 +69,7 @@ module Rails #Create required tmp directories if not found %w(cache pids sessions sockets).each do |dir_to_make| - FileUtils.mkdir_p(Rails.root.join('tmp', dir_to_make)) + FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) end unless options[:daemonize] diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index b327dd1e4a..4c7199a2e2 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -416,9 +416,9 @@ module Rails # Finds engine with given path def find(path) - expanded_path = File.expand_path path.to_s + expanded_path = File.expand_path path Rails::Engine::Railties.engines.find { |engine| - File.expand_path(engine.root.to_s) == expanded_path + File.expand_path(engine.root) == expanded_path } end end @@ -652,7 +652,7 @@ module Rails root = File.exist?("#{root_path}/#{flag}") ? root_path : default raise "Could not find root path for #{self}" unless root - File.realpath root + Pathname.new File.realpath root end def default_middleware_stack diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 92f40b9e31..c41acc7841 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -5,8 +5,7 @@ module Rails module Generators module Actions - # Adds an entry into Gemfile for the supplied gem. If env - # is specified, add the gem to the given environment. + # Adds an entry into Gemfile for the supplied gem. # # gem "rspec", :group => :test # gem "technoweenie-restful-authentication", :lib => "restful-authentication", :source => "http://gems.github.com/" diff --git a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb index 7088367462..ab0e440bc4 100644 --- a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb +++ b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb @@ -232,6 +232,18 @@ task :default => :test public_task :apply_rails_template, :run_bundle + def name + @name ||= begin + # same as ActiveSupport::Inflector#underscore except not replacing '-' + underscored = original_name.dup + underscored.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') + underscored.gsub!(/([a-z\d])([A-Z])/,'\1_\2') + underscored.downcase! + + underscored + end + end + protected def app_templates_dir @@ -268,18 +280,6 @@ task :default => :test @original_name ||= File.basename(destination_root) end - def name - @name ||= begin - # same as ActiveSupport::Inflector#underscore except not replacing '-' - underscored = original_name.dup - underscored.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') - underscored.gsub!(/([a-z\d])([A-Z])/,'\1_\2') - underscored.downcase! - - underscored - end - end - def camelized @camelized ||= name.gsub(/\W/, '_').squeeze('_').camelize end diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index 6b4bdb2921..5081074395 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -1,15 +1,33 @@ +require 'rails/application/route_inspector' + class Rails::InfoController < ActionController::Base + self.view_paths = File.join(File.dirname(__FILE__), 'templates') + layout 'application' + + before_filter :require_local! + + def index + redirect_to '/rails/info/routes' + end + def properties - if consider_all_requests_local? || request.local? - render :inline => Rails::Info.to_html - else - render :text => '<p>For security purposes, this information is only available to local requests.</p>', :status => :forbidden - end + @info = Rails::Info.to_html + end + + def routes + inspector = Rails::Application::RouteInspector.new + @info = inspector.format(_routes.routes).join("\n") end protected - def consider_all_requests_local? - Rails.application.config.consider_all_requests_local + def require_local! + unless local_request? + render :text => '<p>For security purposes, this information is only available to local requests.</p>', :status => :forbidden + end + end + + def local_request? + Rails.application.config.consider_all_requests_local || request.local? end end diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index b787d91821..6cd9c7bc95 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -1,5 +1,3 @@ -require "pathname" - module Rails module Paths # This object is an extended hash that behaves as root of the <tt>Rails::Paths</tt> system. @@ -186,7 +184,7 @@ module Rails raise "You need to set a path root" unless @root.path map do |p| - Pathname.new(@root.path).join(p) + File.join @root.path, p end end diff --git a/railties/lib/rails/tasks/engine.rake b/railties/lib/rails/tasks/engine.rake index eea8abe7d2..70370be3f5 100644 --- a/railties/lib/rails/tasks/engine.rake +++ b/railties/lib/rails/tasks/engine.rake @@ -60,7 +60,7 @@ namespace :db do end def find_engine_path(path) - return if path == "/" + return File.expand_path(Dir.pwd) if path == "/" if Rails::Engine.find(path) path diff --git a/railties/lib/rails/templates/layouts/application.html.erb b/railties/lib/rails/templates/layouts/application.html.erb new file mode 100644 index 0000000000..53276d3e7c --- /dev/null +++ b/railties/lib/rails/templates/layouts/application.html.erb @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8" /> + <title>Routes</title> + <style> + body { background-color: #fff; color: #333; } + + body, p, ol, ul, td { + font-family: helvetica, verdana, arial, sans-serif; + font-size: 13px; + line-height: 18px; + } + + pre { + background-color: #eee; + padding: 10px; + font-size: 11px; + white-space: pre-wrap; + } + + a { color: #000; } + a:visited { color: #666; } + a:hover { color: #fff; background-color:#000; } + </style> +</head> +<body> +<h2>Your App: <%= link_to 'properties', '/rails/info/properties' %> | <%= link_to 'routes', '/rails/info/routes' %></h2> +<%= yield %> + +</body> +</html> diff --git a/railties/lib/rails/templates/rails/info/properties.html.erb b/railties/lib/rails/templates/rails/info/properties.html.erb new file mode 100644 index 0000000000..d47cbab202 --- /dev/null +++ b/railties/lib/rails/templates/rails/info/properties.html.erb @@ -0,0 +1 @@ +<%= @info.html_safe %>
\ No newline at end of file diff --git a/railties/lib/rails/templates/rails/info/routes.html.erb b/railties/lib/rails/templates/rails/info/routes.html.erb new file mode 100644 index 0000000000..890f6f5b03 --- /dev/null +++ b/railties/lib/rails/templates/rails/info/routes.html.erb @@ -0,0 +1,9 @@ +<h2> + Routes +</h2> + +<p> + Routes match in priority from top to bottom +</p> + +<p><pre><%= @info %></pre></p>
\ No newline at end of file diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 7067253279..2a39826c8d 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -7,6 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Tools for creating, working with, and running Rails applications.' s.description = 'Rails internals: application bootup, plugins, generators, and rake tasks.' s.required_ruby_version = '>= 1.9.3' + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index 977a5fc7e8..d1373ba202 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -15,12 +15,24 @@ module ApplicationTests teardown_app end + test "rails/info/routes in development" do + app("development") + get "/rails/info/routes" + assert_equal 200, last_response.status + end + test "rails/info/properties in development" do app("development") get "/rails/info/properties" assert_equal 200, last_response.status end + test "rails/info/routes in production" do + app("production") + get "/rails/info/routes" + assert_equal 404, last_response.status + end + test "rails/info/properties in production" do app("production") get "/rails/info/properties" diff --git a/railties/test/generators/assets_generator_test.rb b/railties/test/generators/assets_generator_test.rb index d6338bd3da..a2b94f2e50 100644 --- a/railties/test/generators/assets_generator_test.rb +++ b/railties/test/generators/assets_generator_test.rb @@ -1,7 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/assets/assets_generator' -# FIXME: Silence the 'Could not find task "using_coffee?"' message in tests due to the public stub class AssetsGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments %w(posts) diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index aa04cad033..5d6b6f9f72 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -29,7 +29,7 @@ class PathsTest < ActiveSupport::TestCase test "creating a root level path" do @root.add "app" assert_equal ["/foo/bar/app"], @root["app"].to_a - assert_equal [Pathname.new("/foo/bar/app")], @root["app"].paths + assert_equal ["/foo/bar/app"], @root["app"].paths end test "creating a root level path with options" do @@ -192,7 +192,7 @@ class PathsTest < ActiveSupport::TestCase @root["app"] = "/app" @root["app"].glob = "*.rb" assert_equal "*.rb", @root["app"].glob - assert_equal [Pathname.new("/app")], @root["app"].paths + assert_equal ["/foo/bar/app"], @root["app"].paths end test "it should be possible to override a path's default glob without assignment" do diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index f7a30a16d2..cfb32b7d35 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -12,29 +12,28 @@ class InfoControllerTest < ActionController::TestCase def setup Rails.application.routes.draw do get '/rails/info/properties' => "rails/info#properties" + get '/rails/info/routes' => "rails/info#routes" end - @request.stubs(:local? => true) - @controller.stubs(:consider_all_requests_local? => false) + @controller.stubs(:local_request? => true) @routes = Rails.application.routes Rails::InfoController.send(:include, @routes.url_helpers) end test "info controller does not allow remote requests" do - @request.stubs(:local? => false) + @controller.stubs(:local_request? => false) get :properties assert_response :forbidden end test "info controller renders an error message when request was forbidden" do - @request.stubs(:local? => false) + @controller.stubs(:local_request? => false) get :properties assert_select 'p' end test "info controller allows requests when all requests are considered local" do - @request.stubs(:local? => false) - @controller.stubs(:consider_all_requests_local? => true) + @controller.stubs(:local_request? => true) get :properties assert_response :success end @@ -48,4 +47,10 @@ class InfoControllerTest < ActionController::TestCase get :properties assert_select 'table' end + + test "info controller renders with routes" do + get :routes + assert_select 'pre' + end + end |