activemodel 3.0.0.beta4 → 3.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data/CHANGELOG +1 -39
  2. data/MIT-LICENSE +1 -1
  3. data/README +16 -200
  4. data/lib/active_model.rb +19 -28
  5. data/lib/active_model/attribute_methods.rb +27 -142
  6. data/lib/active_model/conversion.rb +1 -37
  7. data/lib/active_model/dirty.rb +12 -51
  8. data/lib/active_model/errors.rb +22 -146
  9. data/lib/active_model/lint.rb +14 -48
  10. data/lib/active_model/locale/en.yml +23 -26
  11. data/lib/active_model/naming.rb +5 -41
  12. data/lib/active_model/observing.rb +16 -35
  13. data/lib/active_model/serialization.rb +0 -57
  14. data/lib/active_model/serializers/json.rb +8 -13
  15. data/lib/active_model/serializers/xml.rb +123 -63
  16. data/lib/active_model/state_machine.rb +70 -0
  17. data/lib/active_model/state_machine/event.rb +62 -0
  18. data/lib/active_model/state_machine/machine.rb +75 -0
  19. data/lib/active_model/state_machine/state.rb +47 -0
  20. data/lib/active_model/state_machine/state_transition.rb +40 -0
  21. data/lib/active_model/test_case.rb +2 -0
  22. data/lib/active_model/validations.rb +62 -125
  23. data/lib/active_model/validations/acceptance.rb +18 -23
  24. data/lib/active_model/validations/confirmation.rb +10 -14
  25. data/lib/active_model/validations/exclusion.rb +13 -15
  26. data/lib/active_model/validations/format.rb +24 -26
  27. data/lib/active_model/validations/inclusion.rb +13 -15
  28. data/lib/active_model/validations/length.rb +65 -61
  29. data/lib/active_model/validations/numericality.rb +58 -76
  30. data/lib/active_model/validations/presence.rb +8 -8
  31. data/lib/active_model/validations/with.rb +22 -90
  32. data/lib/active_model/validations_repair_helper.rb +35 -0
  33. data/lib/active_model/version.rb +2 -3
  34. metadata +19 -63
  35. data/lib/active_model/callbacks.rb +0 -134
  36. data/lib/active_model/railtie.rb +0 -2
  37. data/lib/active_model/translation.rb +0 -60
  38. data/lib/active_model/validations/validates.rb +0 -108
  39. data/lib/active_model/validator.rb +0 -183
@@ -1,44 +1,8 @@
1
1
  module ActiveModel
2
- # Handle default conversions: to_model, to_key and to_param.
3
- #
4
- # == Example
5
- #
6
- # Let's take for example this non persisted object.
7
- #
8
- # class ContactMessage
9
- # include ActiveModel::Conversion
10
- #
11
- # # ContactMessage are never persisted in the DB
12
- # def persisted?
13
- # false
14
- # end
15
- # end
16
- #
17
- # cm = ContactMessage.new
18
- # cm.to_model == self #=> true
19
- # cm.to_key #=> nil
20
- # cm.to_param #=> nil
21
- #
2
+ # Include ActiveModel::Conversion if your object "acts like an ActiveModel model".
22
3
  module Conversion
23
- # If your object is already designed to implement all of the Active Model you can use
24
- # the default to_model implementation, which simply returns self.
25
- #
26
- # If your model does not act like an Active Model object, then you should define
27
- # <tt>:to_model</tt> yourself returning a proxy object that wraps your object
28
- # with Active Model compliant methods.
29
4
  def to_model
30
5
  self
31
6
  end
32
-
33
- # Returns an Enumerable of all (primary) key attributes or nil if persisted? is fakse
34
- def to_key
35
- persisted? ? [id] : nil
36
- end
37
-
38
- # Returns a string representing the object's key suitable for use in URLs,
39
- # or nil if persisted? is false
40
- def to_param
41
- to_key ? to_key.join('-') : nil
42
- end
43
7
  end
44
8
  end
@@ -1,48 +1,5 @@
1
- require 'active_model/attribute_methods'
2
- require 'active_support/concern'
3
- require 'active_support/hash_with_indifferent_access'
4
- require 'active_support/core_ext/object/duplicable'
5
-
6
1
  module ActiveModel
7
- # <tt>ActiveModel::Dirty</tt> provides a way to track changes in your
8
- # object in the same way as ActiveRecord does.
9
- #
10
- # The requirements to implement ActiveModel::Dirty are:
11
- #
12
- # * <tt>include ActiveModel::Dirty</tt> in your object
13
- # * Call <tt>define_attribute_methods</tt> passing each method you want to track
14
- # * Call <tt>attr_name_will_change!</tt> before each change to the tracked attribute
15
- #
16
- # If you wish to also track previous changes on save or update, you need to add
17
- #
18
- # @previously_changed = changes
19
- #
20
- # inside of your save or update method.
21
- #
22
- # A minimal implementation could be:
23
- #
24
- # class Person
25
- #
26
- # include ActiveModel::Dirty
27
- #
28
- # define_attribute_methods [:name]
29
- #
30
- # def name
31
- # @name
32
- # end
33
- #
34
- # def name=(val)
35
- # name_will_change!
36
- # @name = val
37
- # end
38
- #
39
- # def save
40
- # @previously_changed = changes
41
- # end
42
- #
43
- # end
44
- #
45
- # == Examples:
2
+ # Track unsaved attribute changes.
46
3
  #
47
4
  # A newly instantiated object is unchanged:
48
5
  # person = Person.find_by_name('Uncle Bob')
@@ -112,7 +69,7 @@ module ActiveModel
112
69
  # person.name = 'bob'
113
70
  # person.changes # => { 'name' => ['bill', 'bob'] }
114
71
  def changes
115
- changed.inject(HashWithIndifferentAccess.new){ |h, attr| h[attr] = attribute_change(attr); h }
72
+ changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
116
73
  end
117
74
 
118
75
  # Map of attributes that were changed when the model was saved.
@@ -121,15 +78,19 @@ module ActiveModel
121
78
  # person.save
122
79
  # person.previous_changes # => {'name' => ['bob, 'robert']}
123
80
  def previous_changes
124
- @previously_changed
125
- end
126
-
127
- # Map of change <tt>attr => original value</tt>.
128
- def changed_attributes
129
- @changed_attributes ||= {}
81
+ previously_changed_attributes
130
82
  end
131
83
 
132
84
  private
85
+ # Map of change <tt>attr => original value</tt>.
86
+ def changed_attributes
87
+ @changed_attributes ||= {}
88
+ end
89
+
90
+ # Map of fields that were changed when the model was saved
91
+ def previously_changed_attributes
92
+ @previously_changed || {}
93
+ end
133
94
 
134
95
  # Handle <tt>*_changed?</tt> for +method_missing+.
135
96
  def attribute_changed?(attr)
@@ -1,71 +1,10 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- require 'active_support/core_ext/array/wrap'
4
1
  require 'active_support/core_ext/string/inflections'
5
- require 'active_support/core_ext/object/blank'
6
2
  require 'active_support/ordered_hash'
7
3
 
8
4
  module ActiveModel
9
- # Provides a modified +OrderedHash+ that you can include in your object
10
- # for handling error messages and interacting with Action Pack helpers.
11
- #
12
- # A minimal implementation could be:
13
- #
14
- # class Person
15
- #
16
- # # Required dependency for ActiveModel::Errors
17
- # extend ActiveModel::Naming
18
- #
19
- # def initialize
20
- # @errors = ActiveModel::Errors.new(self)
21
- # end
22
- #
23
- # attr_accessor :name
24
- # attr_reader :errors
25
- #
26
- # def validate!
27
- # errors.add(:name, "can not be nil") if name == nil
28
- # end
29
- #
30
- # # The following methods are needed to be minimally implemented
31
- #
32
- # def read_attribute_for_validation(attr)
33
- # send(attr)
34
- # end
35
- #
36
- # def ErrorsPerson.human_attribute_name(attr, options = {})
37
- # attr
38
- # end
39
- #
40
- # def ErrorsPerson.lookup_ancestors
41
- # [self]
42
- # end
43
- #
44
- # end
45
- #
46
- # The last three methods are required in your object for Errors to be
47
- # able to generate error messages correctly and also handle multiple
48
- # languages. Of course, if you extend your object with ActiveModel::Translations
49
- # you will not need to implement the last two. Likewise, using
50
- # ActiveModel::Validations will handle the validation related methods
51
- # for you.
52
- #
53
- # The above allows you to do:
54
- #
55
- # p = Person.new
56
- # p.validate! # => ["can not be nil"]
57
- # p.errors.full_messages # => ["name can not be nil"]
58
- # # etc..
59
5
  class Errors < ActiveSupport::OrderedHash
60
6
  include DeprecatedErrorMethods
61
7
 
62
- # Pass in the instance of the object that is using the errors object.
63
- #
64
- # class Person
65
- # def initialize
66
- # @errors = ActiveModel::Errors.new(self)
67
- # end
68
- # end
69
8
  def initialize(base)
70
9
  @base = base
71
10
  super()
@@ -74,10 +13,6 @@ module ActiveModel
74
13
  alias_method :get, :[]
75
14
  alias_method :set, :[]=
76
15
 
77
- # When passed a symbol or a name of a method, returns an array of errors for the method.
78
- #
79
- # p.errors[:name] #=> ["can not be nil"]
80
- # p.errors['name'] #=> ["can not be nil"]
81
16
  def [](attribute)
82
17
  if errors = get(attribute.to_sym)
83
18
  errors
@@ -86,78 +21,28 @@ module ActiveModel
86
21
  end
87
22
  end
88
23
 
89
- # Adds to the supplied attribute the supplied error message.
90
- #
91
- # p.errors[:name] = "must be set"
92
- # p.errors[:name] #=> ['must be set']
93
24
  def []=(attribute, error)
94
25
  self[attribute.to_sym] << error
95
26
  end
96
27
 
97
- # Iterates through each error key, value pair in the error messages hash.
98
- # Yields the attribute and the error for that attribute. If the attribute
99
- # has more than one error message, yields once for each error message.
100
- #
101
- # p.errors.add(:name, "can't be blank")
102
- # p.errors.each do |attribute, errors_array|
103
- # # Will yield :name and "can't be blank"
104
- # end
105
- #
106
- # p.errors.add(:name, "must be specified")
107
- # p.errors.each do |attribute, errors_array|
108
- # # Will yield :name and "can't be blank"
109
- # # then yield :name and "must be specified"
110
- # end
111
28
  def each
112
29
  each_key do |attribute|
113
30
  self[attribute].each { |error| yield attribute, error }
114
31
  end
115
32
  end
116
33
 
117
- # Returns the number of error messages.
118
- #
119
- # p.errors.add(:name, "can't be blank")
120
- # p.errors.size #=> 1
121
- # p.errors.add(:name, "must be specified")
122
- # p.errors.size #=> 2
123
34
  def size
124
35
  values.flatten.size
125
36
  end
126
37
 
127
- # Returns an array of error messages, with the attribute name included
128
- #
129
- # p.errors.add(:name, "can't be blank")
130
- # p.errors.add(:name, "must be specified")
131
- # p.errors.to_a #=> ["name can't be blank", "name must be specified"]
132
38
  def to_a
133
39
  full_messages
134
40
  end
135
41
 
136
- # Returns the number of error messages.
137
- # p.errors.add(:name, "can't be blank")
138
- # p.errors.count #=> 1
139
- # p.errors.add(:name, "must be specified")
140
- # p.errors.count #=> 2
141
42
  def count
142
43
  to_a.size
143
44
  end
144
45
 
145
- # Returns true if there are any errors, false if not.
146
- def empty?
147
- all? { |k, v| v && v.empty? }
148
- end
149
-
150
- # Returns an xml formatted representation of the Errors hash.
151
- #
152
- # p.errors.add(:name, "can't be blank")
153
- # p.errors.add(:name, "must be specified")
154
- # p.errors.to_xml #=> Produces:
155
- #
156
- # # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
157
- # # <errors>
158
- # # <error>name can't be blank</error>
159
- # # <error>name must be specified</error>
160
- # # </errors>
161
46
  def to_xml(options={})
162
47
  require 'builder' unless defined? ::Builder
163
48
  options[:root] ||= "errors"
@@ -170,17 +55,14 @@ module ActiveModel
170
55
  end
171
56
  end
172
57
 
173
- # Adds +message+ to the error messages on +attribute+, which will be returned on a call to
174
- # <tt>on(attribute)</tt> for the same attribute. More than one error can be added to the same
175
- # +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
176
- # If no +message+ is supplied, <tt>:invalid</tt> is assumed.
177
- #
178
- # If +message+ is a symbol, it will be translated using the appropriate scope (see +translate_error+).
179
- # If +message+ is a proc, it will be called, allowing for things like <tt>Time.now</tt> to be used within an error.
58
+ # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
59
+ # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
60
+ # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
61
+ # If no +messsage+ is supplied, :invalid is assumed.
62
+ # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
180
63
  def add(attribute, message = nil, options = {})
181
64
  message ||= :invalid
182
65
  message = generate_message(attribute, message, options) if message.is_a?(Symbol)
183
- message = message.call if message.is_a?(Proc)
184
66
  self[attribute] << message
185
67
  end
186
68
 
@@ -211,7 +93,7 @@ module ActiveModel
211
93
  # company = Company.create(:address => '123 First St.')
212
94
  # company.errors.full_messages # =>
213
95
  # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
214
- def full_messages
96
+ def full_messages(options = {})
215
97
  full_messages = []
216
98
 
217
99
  each do |attribute, messages|
@@ -221,12 +103,10 @@ module ActiveModel
221
103
  if attribute == :base
222
104
  messages.each {|m| full_messages << m }
223
105
  else
224
- attr_name = attribute.to_s.gsub('.', '_').humanize
225
- attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
226
- options = { :default => "%{attribute} %{message}", :attribute => attr_name }
227
-
106
+ attr_name = attribute.to_s.humanize
107
+ prefix = attr_name + I18n.t('activemodel.errors.format.separator', :default => ' ')
228
108
  messages.each do |m|
229
- full_messages << I18n.t(:"errors.format", options.merge(:message => m))
109
+ full_messages << "#{prefix}#{m}"
230
110
  end
231
111
  end
232
112
  end
@@ -249,35 +129,31 @@ module ActiveModel
249
129
  # <li><tt>activemodel.errors.models.admin.blank</tt></li>
250
130
  # <li><tt>activemodel.errors.models.user.attributes.title.blank</tt></li>
251
131
  # <li><tt>activemodel.errors.models.user.blank</tt></li>
252
- # <li>any default you provided through the +options+ hash (in the activemodel.errors scope)</li>
253
132
  # <li><tt>activemodel.errors.messages.blank</tt></li>
254
- # <li><tt>errors.attributes.title.blank</tt></li>
255
- # <li><tt>errors.messages.blank</tt></li>
133
+ # <li>any default you provided through the +options+ hash (in the activemodel.errors scope)</li>
256
134
  # </ol>
257
135
  def generate_message(attribute, message = :invalid, options = {})
258
136
  message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)
259
137
 
260
- defaults = @base.class.lookup_ancestors.map do |klass|
261
- [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.attributes.#{attribute}.#{message}",
262
- :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.#{message}" ]
138
+ klass_ancestors = [@base.class]
139
+ klass_ancestors += @base.class.ancestors.reject {|x| x.is_a?(Module)}
140
+
141
+ defaults = klass_ancestors.map do |klass|
142
+ [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
143
+ :"models.#{klass.name.underscore}.#{message}" ]
263
144
  end
264
145
 
265
146
  defaults << options.delete(:default)
266
- defaults << :"#{@base.class.i18n_scope}.errors.messages.#{message}"
267
- defaults << :"errors.attributes.#{attribute}.#{message}"
268
- defaults << :"errors.messages.#{message}"
269
-
270
- defaults.compact!
271
- defaults.flatten!
147
+ defaults = defaults.compact.flatten << :"messages.#{message}"
272
148
 
273
149
  key = defaults.shift
274
150
  value = @base.send(:read_attribute_for_validation, attribute)
275
151
 
276
- options = {
277
- :default => defaults,
278
- :model => @base.class.model_name.human,
279
- :attribute => @base.class.human_attribute_name(attribute),
280
- :value => value
152
+ options = { :default => defaults,
153
+ :model => @base.class.name.humanize,
154
+ :attribute => attribute.to_s.humanize,
155
+ :value => value,
156
+ :scope => [:activemodel, :errors]
281
157
  }.merge(options)
282
158
 
283
159
  I18n.translate(key, options)
@@ -13,34 +13,8 @@
13
13
  module ActiveModel
14
14
  module Lint
15
15
  module Tests
16
-
17
- # == Responds to <tt>to_key</tt>
18
- #
19
- # Returns an Enumerable of all (primary) key attributes
20
- # or nil if model.persisted? is false
21
- def test_to_key
22
- assert model.respond_to?(:to_key), "The model should respond to to_key"
23
- def model.persisted?() false end
24
- assert model.to_key.nil?
25
- end
26
-
27
- # == Responds to <tt>to_param</tt>
28
- #
29
- # Returns a string representing the object's key suitable for use in URLs
30
- # or nil if model.persisted? is false.
31
- #
32
- # Implementers can decide to either raise an exception or provide a default
33
- # in case the record uses a composite primary key. There are no tests for this
34
- # behavior in lint because it doesn't make sense to force any of the possible
35
- # implementation strategies on the implementer. However, if the resource is
36
- # not persisted?, then to_param should always return nil.
37
- def test_to_param
38
- assert model.respond_to?(:to_param), "The model should respond to to_param"
39
- def model.persisted?() false end
40
- assert model.to_param.nil?
41
- end
42
-
43
- # == Responds to <tt>valid?</tt>
16
+ # valid?
17
+ # ------
44
18
  #
45
19
  # Returns a boolean that specifies whether the object is in a valid or invalid
46
20
  # state.
@@ -49,38 +23,30 @@ module ActiveModel
49
23
  assert_boolean model.valid?, "valid?"
50
24
  end
51
25
 
52
- # == Responds to <tt>persisted?</tt>
26
+ # new_record?
27
+ # -----------
53
28
  #
54
29
  # Returns a boolean that specifies whether the object has been persisted yet.
55
30
  # This is used when calculating the URL for an object. If the object is
56
31
  # not persisted, a form for that object, for instance, will be POSTed to the
57
32
  # collection. If it is persisted, a form for the object will put PUTed to the
58
33
  # URL for the object.
59
- def test_persisted?
60
- assert model.respond_to?(:persisted?), "The model should respond to persisted?"
61
- assert_boolean model.persisted?, "persisted?"
34
+ def test_new_record?
35
+ assert model.respond_to?(:new_record?), "The model should respond to new_record?"
36
+ assert_boolean model.new_record?, "new_record?"
62
37
  end
63
38
 
64
- # == Naming
65
- #
66
- # Model.model_name must returns a string with some convenience methods as
67
- # :human and :partial_path. Check ActiveModel::Naming for more information.
68
- #
69
- def test_model_naming
70
- assert model.class.respond_to?(:model_name), "The model should respond to model_name"
71
- model_name = model.class.model_name
72
- assert_kind_of String, model_name
73
- assert_kind_of String, model_name.human
74
- assert_kind_of String, model_name.partial_path
75
- assert_kind_of String, model_name.singular
76
- assert_kind_of String, model_name.plural
39
+ def test_destroyed?
40
+ assert model.respond_to?(:destroyed?), "The model should respond to destroyed?"
41
+ assert_boolean model.destroyed?, "destroyed?"
77
42
  end
78
43
 
79
- # == Errors Testing
80
- #
44
+ # errors
45
+ # ------
46
+ #
81
47
  # Returns an object that has :[] and :full_messages defined on it. See below
82
48
  # for more details.
83
- #
49
+
84
50
  # Returns an Array of Strings that are the errors for the attribute in
85
51
  # question. If localization is used, the Strings should be localized
86
52
  # for the current locale. If no error is present, this method should