diff options
19 files changed, 1027 insertions, 95 deletions
diff --git a/activemodel/README b/activemodel/README index 7c9c754a8e..cf103b8d6d 100644 --- a/activemodel/README +++ b/activemodel/README @@ -12,32 +12,55 @@ Active Model provides a known set of interfaces that your objects can implement to then present a common interface to the Action Pack helpers. You can include functionality from the following modules: -* Adding callbacks to your class +* Adding attribute magic to your objects - class MyClass - extend ActiveModel::Callbacks - define_model_callbacks :create + Add prefixes and suffixes to defined attribute methods... + + class Person + include ActiveModel::AttributeMethods + + attribute_method_prefix 'clear_' + define_attribute_methods [:name, :age] + + attr_accessor :name, :age + + def clear_attribute(attr) + send("#{attr}=", nil) + end + end + + ...gives you clear_name, clear_age. + + {Learn more}[link:classes/ActiveModel/AttributeMethods.html] + +* Adding callbacks to your objects - def create - _run_create_callbacks do - # Your create action methods here - end - end - end - - ...gives you before_create, around_create and after_create class methods that - wrap your create method. + class Person + extend ActiveModel::Callbacks + define_model_callbacks :create + + def create + _run_create_callbacks do + # Your create action methods here + end + end + end + + ...gives you before_create, around_create and after_create class methods that + wrap your create method. {Learn more}[link:classes/ActiveModel/CallBacks.html] * For classes that already look like an Active Record object - class MyClass + class Person include ActiveModel::Conversion end ...returns the class itself when sent :to_model + {Learn more}[link:classes/ActiveModel/Conversion.html] + * Tracking changes in your object Provides all the value tracking features implemented by ActiveRecord... @@ -55,3 +78,141 @@ functionality from the following modules: {Learn more}[link:classes/ActiveModel/Dirty.html] +* Adding +errors+ support to your object + + Provides the error messages to allow your object to interact with Action Pack + helpers seamlessly... + + class Person + + def initialize + @errors = ActiveModel::Errors.new(self) + end + + attr_accessor :name + attr_reader :errors + + def validate! + errors.add(:name, "can not be nil") if name == nil + end + + def ErrorsPerson.human_attribute_name(attr, options = {}) + "Name" + end + + end + + ... gives you... + + person.errors.full_messages + # => ["Name Can not be nil"] + person.errors.full_messages + # => ["Name Can not be nil"] + + {Learn more}[link:classes/ActiveModel/Errors.html] + +* Testing the compliance of your object + + Use ActiveModel::Lint to test the compliance of your object to the + basic ActiveModel API... + + {Learn more}[link:classes/ActiveModel/Lint/Tests.html] + +* Providing a human face to your object + + ActiveModel::Naming provides your model with the model_name convention + and a human_name attribute... + + class NamedPerson + extend ActiveModel::Naming + end + + ...gives you... + + NamedPerson.model_name #=> "NamedPerson" + NamedPerson.model_name.human #=> "Named person" + + {Learn more}[link:classes/ActiveModel/Naming.html] + +* Adding observer support to your objects + + ActiveModel::Observers allows your object to implement the Observer + pattern in a Rails App and take advantage of all the standard observer + functions. + + {Learn more}[link:classes/ActiveModel/Observer.html] + +* Making your object serializable + + ActiveModel::Serialization provides a standard interface for your object + to provide to_json or to_xml serialization... + + s = SerialPerson.new + s.serializable_hash # => {"name"=>nil} + s.to_json # => "{\"name\":null}" + s.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... + + {Learn more}[link:classes/ActiveModel/Serialization.html] + + +* Turning your object into a finite State Machine + + ActiveModel::StateMachine provides a clean way to include all the methods + you need to transform your object into a finite State Machine... + + light = TrafficLight.new + light.current_state #=> :red + light.change_color! #=> true + light.current_state #=> :green + + {Learn more}[link:classes/ActiveModel/StateMachine.html] + +* Integrating with Rail's internationalization (i18n) handling through + ActiveModel::Translations... + + class Person + extend ActiveModel::Translation + end + + {Learn more}[link:classes/ActiveModel/Translation.html] + +* Providing a full Validation stack for your objects... + + class Person + include ActiveModel::Validations + + attr_accessor :first_name, :last_name + + validates_each :first_name, :last_name do |record, attr, value| + record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z + end + end + + person = Person.new(:first_name => 'zoolander') + person.valid? #=> false + + {Learn more}[link:classes/ActiveModel/Validations.html] + +* Make custom validators + + class Person + include ActiveModel::Validations + validates_with HasNameValidator + attr_accessor :name + end + + class HasNameValidator < ActiveModel::Validator + def validate(record) + record.errors[:name] = "must exist" if record.name.blank? + end + end + + p = ValidatorPerson.new + p.valid? #=> false + p.errors.full_messages #=> ["Name must exist"] + p.name = "Bob" + p.valid? #=> true + + {Learn more}[link:classes/ActiveModel/Validator.html] + +
\ No newline at end of file diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 977a101277..32ddf1d579 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -5,10 +5,51 @@ module ActiveModel class MissingAttributeError < NoMethodError end + # <tt>ActiveModel::AttributeMethods</tt> provides a way to add prefixes and suffixes + # to your methods as well as handling the creation of Active Record like class methods + # such as +table_name+. + # + # The requirements to implement ActiveModel::AttributeMethods are: + # + # * <tt>include ActiveModel::AttributeMethods</tt> in your object + # * Call each Attribute Method module method you want to add, such as + # attribute_method_suffix or attribute_method_prefix + # * Call <tt>define_attribute_methods</tt> after the other methods are + # called. + # * Define the various generic +_attribute+ methods that you have declared + # + # A minimal implementation could be: + # + # class Person + # + # include ActiveModel::AttributeMethods + # + # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' + # attribute_method_suffix '_contrived?' + # attribute_method_prefix 'clear_' + # define_attribute_methods ['name'] + # + # attr_accessor :name + # + # private + # + # def attribute_contrived?(attr) + # true + # end + # + # def clear_attribute(attr) + # send("#{attr}=", nil) + # end + # + # def reset_attribute_to_default!(attr) + # send("#{attr}=", "Default Name") + # end + # + # end + # module AttributeMethods extend ActiveSupport::Concern - # Declare and check for suffixed attribute methods. module ClassMethods # Defines an "attribute" method (like +inheritance_column+ or # +table_name+). A new (class) method will be created with the @@ -22,12 +63,27 @@ module ActiveModel # # Example: # - # class A < ActiveRecord::Base + # class Person + # + # include ActiveModel::AttributeMethods + # + # cattr_accessor :primary_key + # cattr_accessor :inheritance_column + # # define_attr_method :primary_key, "sysid" # define_attr_method( :inheritance_column ) do # original_inheritance_column + "_id" # end + # # end + # + # Provivdes you with: + # + # AttributePerson.primary_key + # # => "sysid" + # AttributePerson.inheritance_column = 'address' + # AttributePerson.inheritance_column + # # => 'address_id' def define_attr_method(name, value=nil, &block) sing = metaclass sing.send :alias_method, "original_#{name}", name @@ -54,19 +110,25 @@ module ActiveModel # # For example: # - # class Person < ActiveRecord::Base + # class Person + # + # include ActiveModel::AttributeMethods + # attr_accessor :name # attribute_method_prefix 'clear_' + # define_attribute_methods [:name] # # private - # def clear_attribute(attr) - # ... - # end + # + # def clear_attribute(attr) + # send("#{attr}=", nil) + # end # end # - # person = Person.find(1) - # person.name # => 'Gem' + # person = Person.new + # person.name = "Bob" + # person.name # => "Bob" # person.clear_name - # person.name # => '' + # person.name # => nil def attribute_method_prefix(*prefixes) attribute_method_matchers.concat(prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix }) undefine_attribute_methods @@ -86,18 +148,24 @@ module ActiveModel # # For example: # - # class Person < ActiveRecord::Base + # class Person + # + # include ActiveModel::AttributeMethods + # attr_accessor :name # attribute_method_suffix '_short?' + # define_attribute_methods [:name] # # private - # def attribute_short?(attr) - # ... - # end + # + # def attribute_short?(attr) + # send(attr).length < 5 + # end # end # - # person = Person.find(1) - # person.name # => 'Gem' - # person.name_short? # => true + # person = Person.new + # person.name = "Bob" + # person.name # => "Bob" + # person.name_short? # => true def attribute_method_suffix(*suffixes) attribute_method_matchers.concat(suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix }) undefine_attribute_methods @@ -118,16 +186,21 @@ module ActiveModel # # For example: # - # class Person < ActiveRecord::Base + # class Person + # + # include ActiveModel::AttributeMethods + # attr_accessor :name # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' + # define_attribute_methods [:name] # # private - # def reset_attribute_to_default!(attr) - # ... - # end + # + # def reset_attribute_to_default!(attr) + # ... + # end # end # - # person = Person.find(1) + # person = Person.new # person.name # => 'Gem' # person.reset_name_to_default! # person.name # => 'Gemma' @@ -146,6 +219,30 @@ module ActiveModel end end + # Declares a the attributes that should be prefixed and suffixed by + # ActiveModel::AttributeMethods. + # + # To use, pass in an array of attribute names (as strings or symbols), + # be sure to declare +define_attribute_methods+ after you define any + # prefix, suffix or affix methods, or they will not hook in. + # + # class Person + # + # include ActiveModel::AttributeMethods + # attr_accessor :name, :age, :address + # attribute_method_prefix 'clear_' + # + # # Call to define_attribute_methods must appear after the + # # attribute_method_prefix, attribute_method_suffix or + # # attribute_method_affix declares. + # define_attribute_methods [:name, :age, :address] + # + # private + # + # def clear_attribute(attr) + # ... + # end + # end def define_attribute_methods(attr_names) return if attribute_methods_generated? attr_names.each do |attr_name| @@ -168,6 +265,7 @@ module ActiveModel @attribute_methods_generated = true end + # Removes all the preiously dynamically defined methods from the class def undefine_attribute_methods generated_attribute_methods.module_eval do instance_methods.each { |m| undef_method(m) } @@ -183,6 +281,7 @@ module ActiveModel end end + # Returns true if the attribute methods defined have been generated. def attribute_methods_generated? @attribute_methods_generated ||= nil end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 2e5bcab070..76e6ad93a7 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -2,9 +2,68 @@ require 'active_support/core_ext/string/inflections' require 'active_support/ordered_hash' module ActiveModel + # Provides a modified +OrderedHash+ that you can include in your object + # for handling error messages and interacting with Action Pack helpers. + # + # A minimal implementation could be: + # + # class Person + # + # # Required dependency for ActiveModel::Errors + # extend ActiveModel::Naming + # + # def initialize + # @errors = ActiveModel::Errors.new(self) + # end + # + # attr_accessor :name + # attr_reader :errors + # + # def validate! + # errors.add(:name, "can not be nil") if name == nil + # end + # + # # The following methods are needed to be minimally implemented + # + # def read_attribute_for_validation(attr) + # send(attr) + # end + # + # def ErrorsPerson.human_attribute_name(attr, options = {}) + # attr + # end + # + # def ErrorsPerson.lookup_ancestors + # [self] + # end + # + # end + # + # The last three methods are required in your object for Errors to be + # able to generate error messages correctly and also handle multiple + # languages. Of course, if you extend your object with ActiveModel::Translations + # you will not need to implement the last two. Likewise, using + # ActiveModel::Validations will handle the validation related methods + # for you. + # + # The above allows you to do: + # + # p = Person.new + # p.validate! # => ["can not be nil"] + # p.errors.full_messages # => ["name can not be nil"] + # # etc.. class Errors < ActiveSupport::OrderedHash include DeprecatedErrorMethods + # Pass in the instance of the object that is using the errors object. + # + # class Person + # def initialize + # @errors = ActiveModel::Errors.new(self) + # end + # end + # + # def initialize(base) @base = base super() @@ -13,6 +72,10 @@ module ActiveModel alias_method :get, :[] alias_method :set, :[]= + # When passed a symbol or a name of a method, returns an array of errors for the method. + # + # p.errors[:name] #=> ["can not be nil"] + # p.errors['name'] #=> ["can not be nil"] def [](attribute) if errors = get(attribute.to_sym) errors @@ -21,28 +84,73 @@ module ActiveModel end end + # Adds to the supplied attribute the supplied error message. + # + # p.errors[:name] = "must be set" + # p.errors[:name] #=> ['must be set'] def []=(attribute, error) self[attribute.to_sym] << error end + # Iterates through each error key, value pair in the error messages hash. + # Yields the attribute and the error for that attribute. If the attribute + # has more than one error message, yields once for each error message. + # + # p.errors.add(:name, "can't be blank") + # p.errors.each do |attribute, errors_array| + # # Will yield :name and "can't be blank" + # end + # + # p.errors.add(:name, "must be specified") + # p.errors.each do |attribute, errors_array| + # # Will yield :name and "can't be blank" + # # then yield :name and "must be specified" + # end def each each_key do |attribute| self[attribute].each { |error| yield attribute, error } end end + # Returns the number of error messages. + # + # p.errors.add(:name, "can't be blank") + # p.errors.size #=> 1 + # p.errors.add(:name, "must be specified") + # p.errors.size #=> 2 def size values.flatten.size end + # Returns an array of error messages, with the attribute name included + # + # p.errors.add(:name, "can't be blank") + # p.errors.add(:name, "must be specified") + # p.errors.to_a #=> ["name can't be blank", "name must be specified"] def to_a full_messages end + # Returns the number of error messages. + # p.errors.add(:name, "can't be blank") + # p.errors.count #=> 1 + # p.errors.add(:name, "must be specified") + # p.errors.count #=> 2 def count to_a.size end + # Returns an xml formatted representation of the Errors hash. + # + # p.errors.add(:name, "can't be blank") + # p.errors.add(:name, "must be specified") + # p.errors.to_xml #=> Produces: + # + # # <?xml version=\"1.0\" encoding=\"UTF-8\"?> + # # <errors> + # # <error>name can't be blank</error> + # # <error>name must be specified</error> + # # </errors> def to_xml(options={}) require 'builder' unless defined? ::Builder options[:root] ||= "errors" @@ -59,6 +167,7 @@ module ActiveModel # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. # If no +messsage+ is supplied, :invalid is assumed. + # # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error). # If +message+ is a Proc, it will be called, allowing for things like Time.now to be used within an error def add(attribute, message = nil, options = {}) diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index 1330bf7042..e8a39130a6 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -13,8 +13,7 @@ module ActiveModel module Lint module Tests - # valid? - # ------ + # == Responds to <tt>valid?</tt> # # Returns a boolean that specifies whether the object is in a valid or invalid # state. @@ -23,8 +22,7 @@ module ActiveModel assert_boolean model.valid?, "valid?" end - # new_record? - # ----------- + # == Responds to <tt>new_record?</tt> # # Returns a boolean that specifies whether the object has been persisted yet. # This is used when calculating the URL for an object. If the object is @@ -41,8 +39,7 @@ module ActiveModel assert_boolean model.destroyed?, "destroyed?" end - # naming - # ------ + # == Naming # # Model.model_name must returns a string with some convenience methods as # :human and :partial_path. Check ActiveModel::Naming for more information. @@ -57,9 +54,8 @@ module ActiveModel assert_kind_of String, model_name.plural end - # errors - # ------ - # + # == Errors Testing + # # Returns an object that has :[] and :full_messages defined on it. See below # for more details. diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 4cd68a0c89..b9fb5fe0c8 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,6 +1,7 @@ require 'active_support/inflector' module ActiveModel + class Name < String attr_reader :singular, :plural, :element, :collection, :partial_path alias_method :cache_key, :collection @@ -35,6 +36,21 @@ module ActiveModel end end + # ActiveModel::Naming is a module that creates a +model_name+ method on your + # object. + # + # To implement, just extend ActiveModel::Naming in your object: + # + # class BookCover + # exten ActiveModel::Naming + # end + # + # BookCover.model_name #=> "BookCover" + # BookCover.model_name.human #=> "Book cover" + # + # Providing the functionality that ActiveModel::Naming provides in your object + # is required to pass the ActiveModel Lint test. So either extending the provided + # method below, or rolling your own is required.. module Naming # Returns an ActiveModel::Name object for module. It can be # used to retrieve all kinds of naming-related information. @@ -42,4 +58,5 @@ module ActiveModel @_model_name ||= ActiveModel::Name.new(self) end end + end diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index d9d1ab8967..ed6fb47c7e 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -24,8 +24,9 @@ module ActiveModel # # Same as above, just using explicit class references # ActiveRecord::Base.observers = Cacher, GarbageCollector # - # Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is - # called during startup, and before each development request. + # Note: Setting this does not instantiate the observers yet. + # +instantiate_observers+ is called during startup, and before + # each development request. def observers=(*values) @observers = values.flatten end @@ -102,10 +103,12 @@ module ActiveModel # # == Observing a class that can't be inferred # - # Observers will by default be mapped to the class with which they share a name. So CommentObserver will - # be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer - # differently than the class you're interested in observing, you can use the Observer.observe class method which takes - # either the concrete class (Product) or a symbol for that class (:product): + # Observers will by default be mapped to the class with which they share a + # name. So CommentObserver will be tied to observing Comment, ProductManagerObserver + # to ProductManager, and so on. If you want to name your observer differently than + # the class you're interested in observing, you can use the Observer.observe class + # method which takes either the concrete class (Product) or a symbol for that + # class (:product): # # class AuditObserver < ActiveModel::Observer # observe :account @@ -115,7 +118,8 @@ module ActiveModel # end # end # - # If the audit observer needs to watch more than one kind of object, this can be specified with multiple arguments: + # If the audit observer needs to watch more than one kind of object, this can be + # specified with multiple arguments: # # class AuditObserver < ActiveModel::Observer # observe :account, :balance @@ -125,7 +129,8 @@ module ActiveModel # end # end # - # The AuditObserver will now act on both updates to Account and Balance by treating them both as records. + # The AuditObserver will now act on both updates to Account and Balance by treating + # them both as records. # class Observer include Singleton @@ -144,7 +149,7 @@ module ActiveModel # # class AuditObserver < ActiveModel::Observer # def self.observed_classes - # [AccountObserver, BalanceObserver] + # [Account, Balance] # end # end def observed_classes diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 4c0073f687..28f95f0cdc 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -2,6 +2,64 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' module ActiveModel + + # Provides a basic serialization to a serializable_hash for your object. + # + # A minimal implementation could be: + # + # class Person + # + # include ActiveModel::Serialization + # + # attr_accessor :name + # + # def attributes + # @attributes ||= {'name' => 'nil'} + # end + # + # end + # + # Which would provide you with: + # + # person = Person.new + # person.serializable_hash # => {"name"=>nil} + # person.name = "Bob" + # person.serializable_hash # => {"name"=>"Bob"} + # + # You need to declare some sort of attributes hash which contains the attributes + # you want to serialize and their current value. + # + # Most of the time though, you will want to include the JSON or XML + # serializations. Both of these modules automatically include the + # ActiveModel::Serialization module, so there is no need to explicitly + # include it. + # + # So a minimal implementation including XML and JSON would be: + # + # class Person + # + # include ActiveModel::Serializers::JSON + # include ActiveModel::Serializers::Xml + # + # attr_accessor :name + # + # def attributes + # @attributes ||= {'name' => 'nil'} + # end + # + # end + # + # Which would provide you with: + # + # person = Person.new + # person.serializable_hash # => {"name"=>nil} + # person.to_json # => "{\"name\":null}" + # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... + # + # person.name = "Bob" + # person.serializable_hash # => {"name"=>"Bob"} + # person.to_json # => "{\"name\":\"Bob\"}" + # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... module Serialization def serializable_hash(options = nil) options ||= {} diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 86149f1e5f..a185204680 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -85,8 +85,8 @@ module ActiveModel @options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s } end - # To replicate the behavior in ActiveRecord#attributes, - # <tt>:except</tt> takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set + # To replicate the behavior in ActiveRecord#attributes, <tt>:except</tt> + # takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set # for a N level model but is set for the N+1 level models, # then because <tt>:except</tt> is set to a default value, the second # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if diff --git a/activemodel/lib/active_model/state_machine.rb b/activemodel/lib/active_model/state_machine.rb index 527794b34d..64b91c1659 100644 --- a/activemodel/lib/active_model/state_machine.rb +++ b/activemodel/lib/active_model/state_machine.rb @@ -1,4 +1,153 @@ module ActiveModel + + # ActiveModel::StateMachine provides methods that turn your object into a + # finite state machine, able to move from one state to another. + # + # A minimal implementation could be: + # + # class EmailMessage + # include ActiveModel::StateMachine + # + # state_machine do + # state :unread + # state :read + # end + # + # event :open_email do + # transitions :to => :read, :from => :unread + # end + # end + # + # === Examples + # + # class TrafficLight + # include ActiveModel::StateMachine + # + # attr_reader :runners_caught + # + # def initialize + # @runners_caught = 0 + # end + # + # state_machine do + # state :red + # state :green + # state :yellow + # state :blink + # + # event :change_color do + # transitions :to => :red, :from => [:yellow], + # :on_transition => :catch_runners + # transitions :to => :green, :from => [:red] + # transitions :to => :yellow, :from => [:green] + # end + # + # event :defect do + # transitions :to => :blink, :from => [:yellow, :red, :green] + # end + # + # event :repair do + # transitions :to => :red, :from => [:blink] + # end + # end + # + # def catch_runners + # @runners_caught += 1 + # end + # end + # + # light = TrafficLight.new + # light.current_state # => :red + # light.change_color! # => true + # light.current_state # => :green + # light.green? # => true + # light.change_color! # => true + # light.current_state # => :yellow + # light.red? # => false + # light.change_color! # => true + # light.runners_caught # => 1 + # + # * The initial state for TrafficLight is red which is the first state defined. + # + # TrafficLight.state_machine.initial_state # => :red + # + # * Call an event to transition a state machine, e.g. <tt>change_color!</tt>. + # You can call the event with or without the exclamation mark, however, the common Ruby + # idiom is to name methods that directly change the state of the receivier with + # an exclamation mark, so <tt>change_color!</tt> is preferred over <tt>change_color</tt>. + # + # light.current_state #=> :green + # light.change_color! #=> true + # light.current_state #=> :yellow + # + # * On a succesful transition to red (from yellow), the local +catch_runners+ + # method is executed + # + # light.current_state #=> :red + # light.change_color! #=> true + # light.runners_caught #=> 1 + # + # * The object acts differently depending on its current state, for instance, + # the change_color! method has a different action depending on the current + # color of the light + # + # light.change_color! #=> true + # light.current_state #=> :red + # light.change_color! #=> true + # light.current_state #=> :green + # + # * Get the possible events for a state + # + # TrafficLight.state_machine.events_for(:red) # => [:change_color, :defect] + # TrafficLight.state_machine.events_for(:blink) # => [:repair] + # + # The StateMachine also supports the following features : + # + # * Success callbacks on event transition + # + # event :sample, :success => :we_win do + # ... + # end + # + # * Enter and exit callbacks par state + # + # state :open, :enter => [:alert_twitter, :send_emails], :exit => :alert_twitter + # + # * Guards on transition + # + # event :close do + # # You may only close the store if the safe is locked!! + # transitions :to => :closed, :from => :open, :guard => :safe_locked? + # end + # + # * Setting the initial state + # + # state_machine :initial => :yellow do + # ... + # end + # + # * Named the state machine, to have more than one + # + # class Stated + # include ActiveModel::StateMachine + # + # strate_machine :name => :ontest do + # end + # + # state_machine do + # end + # end + # + # # Get the state of the <tt>:ontest</tt> state machine + # stat.current_state(:ontest) + # # Get the initial state + # Stated.state_machine(:ontest).initial_state + # + # * Changing the state + # + # stat.current_state(:default, :astate) # => :astate + # # But you must give the name of the state machine, here <tt>:default</tt> + # module StateMachine autoload :Event, 'active_model/state_machine/event' autoload :Machine, 'active_model/state_machine/machine' diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 2d2df269d0..2ab342ffac 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -1,6 +1,23 @@ require 'active_support/core_ext/hash/reverse_merge' module ActiveModel + + # ActiveModel::Translation provides integration between your object and + # the Rails internationalization (i18n) framework. + # + # A minimal implementation could be: + # + # class TranslatedPerson + # extend ActiveModel::Translation + # end + # + # TranslatedPerson.human_attribute_name('my_attribue') + # #=> "My attribute" + # + # This also provides the required class methods for hooking into the + # Rails internationalization API, including being able to define a + # class based i18n_scope and lookup_ancestors to find translations in + # parent classes. module Translation include ActiveModel::Naming @@ -18,8 +35,6 @@ module ActiveModel # Transforms attributes names into a more human format, such as "First name" instead of "first_name". # - # Example: - # # Person.human_attribute_name("first_name") # => "First name" # # Specify +options+ with additional translating options. diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 276472ea46..03733a9c89 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -3,6 +3,41 @@ require 'active_support/core_ext/hash/keys' require 'active_model/errors' module ActiveModel + + # Provides a full validation framework to your objects. + # + # A minimal implementation could be: + # + # class Person + # include ActiveModel::Validations + # + # attr_accessor :first_name, :last_name + # + # validates_each :first_name, :last_name do |record, attr, value| + # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z + # end + # end + # + # Which provides you with the full standard validation stack that you + # know from ActiveRecord. + # + # person = Person.new + # person.valid? + # #=> true + # person.invalid? + # #=> false + # person.first_name = 'zoolander' + # person.valid? + # #=> false + # person.invalid? + # #=> true + # person.errors + # #=> #<OrderedHash {:first_name=>["starts with z."]}> + # + # Note that ActiveModel::Validations automatically adds an +errors+ method + # to your instances initialized with a new ActiveModel::Errors object, so + # there is no need for you to add this manually. + # module Validations extend ActiveSupport::Concern include ActiveSupport::Callbacks @@ -18,8 +53,10 @@ module ActiveModel # class Person # include ActiveModel::Validations # + # attr_accessor :first_name, :last_name + # # validates_each :first_name, :last_name do |record, attr, value| - # record.errors.add attr, 'starts with z.' if value[0] == ?z + # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z # end # end # diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 382a4cc98d..ad9729de00 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -1,5 +1,6 @@ module ActiveModel #:nodoc: - # A simple base class that can be used along with ActiveModel::Validations::ClassMethods.validates_with + # A simple base class that can be used along with + # +ActiveModel::Validations::ClassMethods.validates_with+ # # class Person # include ActiveModel::Validations @@ -28,7 +29,7 @@ module ActiveModel #:nodoc: # end # # class MyValidator < ActiveModel::Validator - # def validate + # def validate(record) # record # => The person instance being validated # options # => Any non-standard options passed to validates_with # end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index df8aefea5a..c751e29908 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -1,8 +1,8 @@ class Module # Provides a delegate class method to easily expose contained objects' methods # as your own. Pass one or more methods (specified as symbols or strings) - # and the name of the target object as the final <tt>:to</tt> option (also a symbol - # or string). At least one method and the <tt>:to</tt> option are required. + # and the name of the target object via the <tt>:to</tt> option (also a symbol + # or string). At least one method and the <tt>:to</tt> option are required. # # Delegation is particularly useful with Active Record associations: # @@ -72,9 +72,9 @@ class Module # invoice.customer_name # => "John Doe" # invoice.customer_address # => "Vimmersvej 13" # - # If the object to which you delegate can be nil, you may want to use the - # :allow_nil option. In that case, it returns nil instead of raising a - # NoMethodError exception: + # If the delegate object is +nil+ an exception is raised, and that happens + # no matter whether +nil+ responds to the delegated method. You can get a + # +nil+ instead with the +:allow_nil+ option. # # class Foo # attr_accessor :bar @@ -124,7 +124,7 @@ class Module #{to}.__send__(#{method.inspect}, *args, &block) # client.__send__(:name, *args, &block) rescue NoMethodError # rescue NoMethodError if #{to}.nil? # if client.nil? - #{on_nil} + #{on_nil} # return # depends on :allow_nil else # else raise # raise end # end diff --git a/railties/guides/source/3_0_release_notes.textile b/railties/guides/source/3_0_release_notes.textile new file mode 100644 index 0000000000..43a27f6402 --- /dev/null +++ b/railties/guides/source/3_0_release_notes.textile @@ -0,0 +1,51 @@ +h2. Ruby on Rails 3.0 Release Notes + +Rails 3.0 is a landmark release as it delivers on the Merb/Rails merge promise made in December, 2008. Rails 3.0 provides many major upgrades to all of the major components of Rails, and now adds Active Model ORM abstraction as well as a consistent Plugin API giving developers full access to all the Rails internals that make Action Mailer, Action Controller, Action View, Active Record and Active Resource work. These release notes cover the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the "list of commits":http://github.com/rails/rails/commits/master in the main Rails repository on GitHub or review the +CHANGELOG+ files for the individual Rails components. + +endprologue. + +h3. Application Creation + + +h3. Application Architecture + + +h3. Documentation + + +h3. Ruby 1.9.1 Support + + +h3. Railties + + +h3. Action Controller + + +h3. Action Dispatch + + +h3. Action Pack + + +h3. Action View + + +h3. Active Model + + +h3. Active Record + + +h3. Active Resource + + +h3. Action Mailer + + +h3. Deprecated + + +h3. Credits + +Release notes compiled by "Mikel Lindsaar":http://lindsaar.net. This version of the Rails 3.0 release notes was compiled based on Rails version... diff --git a/railties/guides/source/action_controller_overview.textile b/railties/guides/source/action_controller_overview.textile index 46a28da8c4..bedca59c12 100644 --- a/railties/guides/source/action_controller_overview.textile +++ b/railties/guides/source/action_controller_overview.textile @@ -625,9 +625,9 @@ class ClientsController < ApplicationController # returns it. The user will get the PDF as a file download. def download_pdf client = Client.find(params[:id]) - send_data(generate_pdf, + send_data generate_pdf(client), :filename => "#{client.name}.pdf", - :type => "application/pdf") + :type => "application/pdf" end private diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index 3073c3a7a5..8dd1bb06c2 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -323,6 +323,92 @@ TIP: Since +with_options+ forwards calls to its receiver they can be nested. Eac NOTE: Defined in +active_support/core_ext/object/with_options.rb+. +h4. Modules and Classes + +h5. +remove_subclasses_of+ + +The method +remove_subclasses_of+ receives an arbitrary number of class objects and removes their subclasses. It is a wrapper of +Class#remove_class+ explained with more details in "Class Removal FIX THIS LINK":FIXME. + +h5. +subclasses_of+ + +The method +subclasses_of+ receives an arbitrary number of class objects and returns all their anonymous or reachable descendants as a single array: + +<ruby> +class C; end +subclasses_of(C) # => [] + +subclasses_of(Integer) # => [Bignum, Fixnum] + +module M + class A; end + class B1 < A; end + class B2 < A; end +end + +module N + class C < M::B1; end +end + +subclasses_of(M::A) # => [N::C, M::B2, M::B1] +</ruby> + +The order in which these classes are returned is unspecified. The returned collection may have duplicates: + +<ruby> +subclasses_of(Numeric, Integer) +# => [Bignum, Float, Fixnum, Integer, Date::Infinity, Rational, BigDecimal, Bignum, Fixnum] +</ruby> + +See also +Class#subclasses+ in "Extensions to +Class+ FIXME THIS LINK":FIXME. + +NOTE: Defined in +active_support/core_ext/object/extending.rb+. + +h5. +extended_by+ + +The method +extended_by+ returns an array with the modules that extend the receiver, if any: + +<ruby> +module M +end + +module N2 +end + +module N + include N2 +end + +class C + extend M + extend N +end + +C.extended_by # => [N, N2, M] +</ruby> + +NOTE: Defined in +active_support/core_ext/object/extending.rb+. + +h5. +extend_with_included_modules_from+ + +The method +extend_with_included_modules_from+ extends its receiver with the modules that extend its argument: + +<ruby> +module M +end + +class C + extend M +end + +class D +end + +D.extend_with_included_modules_from(C) +D.extended_by # => [M] +</ruby> + +NOTE: Defined in +active_support/core_ext/object/extending.rb+. + h4. Instance Variables Active Support provides several methods to ease access to instance variables. @@ -587,9 +673,19 @@ If for whatever reason an application loads the definition of a mailer class and NOTE: Defined in +active_support/core_ext/class/delegating_attributes.rb+. -h4. Subclasses +h4. Descendants + +h5. +descendents+ + +The +descendents+ method returns all the descendants of its receiver as an array of class objects. This method performs no filtering so non-reachable classes are included, if any. + +See aso +Object#subclasses_of+, explained in "Extensions to All Objects FIX THIS LINK":FIXME. -The +subclasses+ method returns the names of all subclasses of a given class as an array of strings. That comprises not only direct subclasses, but all descendants down the hierarchy: +NOTE: Defined in +active_support/core_ext/object/extending.rb+. + +h5. +subclasses+ + +The +subclasses+ method returns the names of all the anonymous or reachable descendants of its receiver as an array of strings: <ruby> class C; end @@ -610,9 +706,9 @@ end M::A.subclasses # => ["N::C", "M::B2", "M::B1"] </ruby> -The order in which these class names are returned is unspecified. +WARNING: +ActiveRecord::Base+ redefines +subclasses+, it returns class objects, reachable or not, and it is protected. -See also +Object#subclasses_of+ in "Extensions to All Objects FIX THIS LINK":FIXME. +See aso +Object#subclasses_of+, explained in "Extensions to All Objects FIX THIS LINK":FIXME. NOTE: Defined in +active_support/core_ext/class/removal.rb+. @@ -663,6 +759,31 @@ See also +Object#remove_subclasses_of+ in "Extensions to All Objects FIX THIS LI NOTE: Defined in +active_support/core_ext/class/removal.rb+. +h4. Reachable Classes + +By definition a non-anonymous class is reachable if its name constantized is defined, and the corresponding constant evaluates to +self+: + +<ruby> +class C; end +C.reachable? # => true + +phantom = Object.send(:remove_const, :C) + +# The class object is orphan now but it still has a name. +phantom.name # => "C" + +# Class name no longer available as a constant. +phantom.reachable? # => nil + +# Let's define a class named "C" again. +class C; end + +# Class name available as a constant, but different class object. +phantom.reachable? # => false +</ruby> + +NOTE: Defined in +active_support/core_ext/class/removal.rb+. + h3. Extensions to +String+ h4. +squish+ @@ -816,7 +937,15 @@ NOTE: Defined in +active_support/core_ext/integer/inflections.rb+. h3. Extensions to +Float+ -... +h4. +round+ + +The builtin method +Float#round+ rounds a float to the nearest integer. Active Support adds an optional parameter to let you specify a precision: + +<ruby> +Math::E.round(4) # => 2.7183 +</ruby> + +NOTE: Defined in +active_support/core_ext/float/rounding.rb+. h3. Extensions to +BigDecimal+ @@ -1799,20 +1928,11 @@ NOTE: Defined in +active_support/core_ext/name_error.rb+. h3. Extensions to +LoadError+ -Rails hijacks +LoadError.new+ to return a +MissingSourceFile+ exception: +Active Support adds +is_missing?+ to +LoadError+, and also assigns that class to the constant +MissingSourceFile+ for backwards compatibility. -<shell> -$ ruby -e 'require "nonexistent"' -...: no such file to load -- nonexistent (LoadError) -... -$ script/runner 'require "nonexistent"' -...: no such file to load -- nonexistent (MissingSourceFile) -... -</shell> +Given a path name +is_missing?+ tests whether the exception was raised due to that particular file (except perhaps for the ".rb" extension). -The class +MissingSourceFile+ is a subclass of +LoadError+, so any code that rescues +LoadError+ as usual still works as expected. Point is these exception objects respond to +is_missing?+, which given a path name tests whether the exception was raised due to that particular file (except perhaps for the ".rb" extension). - -For example, when an action of +PostsController+ is called Rails tries to load +posts_helper.rb+, but that file may not exist. That's fine, the helper module is not mandatory so Rails silences a load error. But it could be the case that the helper module does exist, but it in turn requires another library that is missing. In that case Rails must reraise the exception. The method +is_missing?+ provides a way to distinguish both cases: +For example, when an action of +PostsController+ is called Rails tries to load +posts_helper.rb+, but that file may not exist. That's fine, the helper module is not mandatory so Rails silences a load error. But it could be the case that the helper module does exist and in turn requires another library that is missing. In that case Rails must reraise the exception. The method +is_missing?+ provides a way to distinguish both cases: <ruby> def default_helper_module! @@ -1820,7 +1940,7 @@ def default_helper_module! module_path = module_name.underscore helper module_path rescue MissingSourceFile => e - raise e unless e.is_missing? "#{module_path}_helper" + raise e unless e.is_missing? "helpers/#{module_path}_helper" rescue NameError => e raise e unless e.missing_name? "#{module_name}Helper" end diff --git a/railties/guides/source/layouts_and_rendering.textile b/railties/guides/source/layouts_and_rendering.textile index 0cee413ac3..323f441218 100644 --- a/railties/guides/source/layouts_and_rendering.textile +++ b/railties/guides/source/layouts_and_rendering.textile @@ -180,7 +180,7 @@ render :text => "OK" TIP: Rendering pure text is most useful when you're responding to AJAX or web service requests that are expecting something other than proper HTML. -NOTE: By default, if you use the +:text+ option, the file is rendered without using the current layout. If you want Rails to put the text into the current layout, you need to add the +:layout => true+ option +NOTE: By default, if you use the +:text+ option the text is rendered without using the current layout. If you want Rails to put the text into the current layout, you need to add the +:layout => true+ option. h5. Rendering JSON diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index e3297148e5..611688cef1 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -1,7 +1,122 @@ module Rails + # Railtie is the core of the Rails Framework and provides all the hooks and + # methods you need to link your plugin into Rails. + # + # What Railtie does is make every component of Rails a "plugin" and creates + # an API that exposes all the powers that the builtin components need + # to any plugin author. + # + # In fact, every major component of Rails (Action Mailer, Action Controller, + # Action View, Active Record and Active Resource) are all now just plain + # old plugins, so anything they can do, your plugin can do. + # + # Developing a plugin for Rails does not _require_ any implementation of + # Railtie, there is no fixed rule, but as a guideline, if your plugin works + # by just being required before Rails boots, then there is no need for you + # to hook into Railtie, but if you need to interact with the Rails framework + # during boot, or after boot, then Railtie is what you need to do that + # interaction. + # + # For example, the following would need you to implement Railtie in your + # plugin: + # + # * creating initializers (including route insertion) + # * modifying the render path (think HAML et al) + # * adding Rails config.* keys to the environment + # * setting up a subscriber to the Rails +ActiveSupport::Notifications+ + # * adding global Rake tasks into rails + # * setting up a default configuration for the Application + # + # Railtie gives you a central place to connect into the Rails framework. If you + # find yourself writing plugin code that is having to monkey patch parts of the + # Rails framework to achieve something, there is probably a better, more elegant + # way to do it through Railtie, if there isn't, then you have found a lacking + # feature of Railtie, please lodge a ticket. + # + # Implementing Railtie in your plugin is by creating a class Railtie in your + # application that has your plugin name and making sure that this gets loaded + # durng boot time of the Rails stack. + # + # You can do this however you wish, but three straight forward ways are: + # + # == For gems or plugins that are not used outside of Rails + # + # * Create a Railtie subclass within your lib/my_plugin.rb file: + # + # # lib/my_plugin.rb + # module MyPlugin + # class Railtie < Rails::Railtie + # end + # end + # + # * Pass in your plugin name + # + # # lib/my_plugin.rb + # module MyPlugin + # class Railtie < Rails::Railtie + # plugin_name :my_plugin + # end + # end + # + # == For gems that could be used without Rails + # + # * Create a file (say, lib/my_gem/railtie.rb) which contains class Railtie inheriting from + # Rails::Railtie and is namespaced to your gem: + # + # # lib/my_gem/railtie.rb + # module MyGem + # class Railtie < Rails::Railtie + # end + # end + # + # * Require your own gem as well as rails in this file: + # + # # lib/my_gem/railtie.rb + # require 'my_gem' + # require 'rails' + # + # module MyGem + # class Railtie < Rails::Railtie + # end + # end + # + # * Give your gem a unique name: + # + # # lib/my_gem/railtie.rb + # require 'my_gem' + # require 'rails' + # + # module MyGem + # class Railtie < Rails::Railtie + # plugin_name :my_gem + # end + # end + # + # * Make sure your Gem loads the railtie.rb file if Rails is loaded first, an easy + # way to check is by checking for the Rails constant which will exist if Rails + # has started: + # + # # lib/my_gem.rb + # module MyGem + # require 'lib/railtie' if defined?(Rails) + # end + # + # * Or instead of doing the require automatically, you can ask your users to require + # it for you in their Gemfile: + # + # # #{USER_RAILS_ROOT}/Gemfile + # gem "my_gem", :require_as => ["my_gem", "my_gem/railtie"] + # class Railtie include Initializable + # Pass in the name of your plugin. This is passed in as an underscored symbol. + # + # module MyPlugin + # class Railtie < Rails::Railtie + # plugin_name :my_plugin + # end + # end def self.plugin_name(plugin_name = nil) @plugin_name ||= name.demodulize.underscore @plugin_name = plugin_name if plugin_name diff --git a/railties/lib/rails/subscriber.rb b/railties/lib/rails/subscriber.rb index 8c62f562d9..6638ff28c1 100644 --- a/railties/lib/rails/subscriber.rb +++ b/railties/lib/rails/subscriber.rb @@ -3,10 +3,10 @@ require 'active_support/notifications' module Rails # Rails::Subscriber is an object set to consume ActiveSupport::Notifications - # on initialization with solely purpose of logging. The subscriber dispatches - # notifications to a regirested object based on its given namespace. + # on initialization with the sole purpose of logging. The subscriber dispatches + # notifications to a registered object based on it's given namespace. # - # An example would be ActiveRecord subscriber responsible for logging queries: + # An example would be an Active Record subscriber responsible for logging queries: # # module ActiveRecord # class Railtie @@ -18,16 +18,16 @@ module Rails # end # end # - # It's finally registed as: + # Which would be registed as: # # Rails::Subscriber.add :active_record, ActiveRecord::Railtie::Subscriber.new # - # So whenever a "active_record.sql" notification arrive to Rails::Subscriber, + # So whenever an +active_record.sql+ notification arrives to Rails::Subscriber, # it will properly dispatch the event (ActiveSupport::Notifications::Event) to # the sql method. # - # This is useful because it avoids spanning several subscribers just for logging - # purposes(which slows down the main thread). Besides of providing a centralized + # This avoids spanning several subscribers just for logging purposes + # (which slows down the main thread). It also provides a centralized # facility on top of Rails.logger. # # Subscriber also has some helpers to deal with logging and automatically flushes @@ -97,7 +97,6 @@ module Rails # option is set to true, it also adds bold to the string. This is based # on Highline implementation and it automatically appends CLEAR to the end # of the returned String. - # def color(text, color, bold=false) return text unless colorize_logging color = self.class.const_get(color.to_s.upcase) if color.is_a?(Symbol) |