diff options
Diffstat (limited to 'activemodel/lib/active_model')
-rw-r--r-- | activemodel/lib/active_model/attribute_methods.rb | 141 | ||||
-rw-r--r-- | activemodel/lib/active_model/errors.rb | 109 | ||||
-rw-r--r-- | activemodel/lib/active_model/lint.rb | 14 | ||||
-rw-r--r-- | activemodel/lib/active_model/naming.rb | 17 | ||||
-rw-r--r-- | activemodel/lib/active_model/observing.rb | 23 | ||||
-rw-r--r-- | activemodel/lib/active_model/serialization.rb | 58 | ||||
-rw-r--r-- | activemodel/lib/active_model/serializers/xml.rb | 4 | ||||
-rw-r--r-- | activemodel/lib/active_model/translation.rb | 19 | ||||
-rw-r--r-- | activemodel/lib/active_model/validations.rb | 39 | ||||
-rw-r--r-- | activemodel/lib/active_model/validator.rb | 5 |
10 files changed, 383 insertions, 46 deletions
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 ff11ddc605..f1d74db5f3 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/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 |