activemodel 3.0.pre → 3.0.0.rc

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 (45) hide show
  1. data/CHANGELOG +44 -1
  2. data/MIT-LICENSE +1 -1
  3. data/README.rdoc +184 -0
  4. data/lib/active_model.rb +29 -19
  5. data/lib/active_model/attribute_methods.rb +167 -46
  6. data/lib/active_model/callbacks.rb +134 -0
  7. data/lib/active_model/conversion.rb +41 -1
  8. data/lib/active_model/deprecated_error_methods.rb +1 -1
  9. data/lib/active_model/dirty.rb +56 -12
  10. data/lib/active_model/errors.rb +205 -46
  11. data/lib/active_model/lint.rb +53 -17
  12. data/lib/active_model/locale/en.yml +26 -23
  13. data/lib/active_model/mass_assignment_security.rb +160 -0
  14. data/lib/active_model/mass_assignment_security/permission_set.rb +40 -0
  15. data/lib/active_model/mass_assignment_security/sanitizer.rb +23 -0
  16. data/lib/active_model/naming.rb +70 -5
  17. data/lib/active_model/observing.rb +40 -16
  18. data/lib/active_model/railtie.rb +2 -0
  19. data/lib/active_model/serialization.rb +59 -0
  20. data/lib/active_model/serializers/json.rb +17 -11
  21. data/lib/active_model/serializers/xml.rb +66 -123
  22. data/lib/active_model/test_case.rb +0 -2
  23. data/lib/active_model/translation.rb +64 -0
  24. data/lib/active_model/validations.rb +150 -68
  25. data/lib/active_model/validations/acceptance.rb +53 -33
  26. data/lib/active_model/validations/callbacks.rb +57 -0
  27. data/lib/active_model/validations/confirmation.rb +41 -23
  28. data/lib/active_model/validations/exclusion.rb +18 -13
  29. data/lib/active_model/validations/format.rb +28 -24
  30. data/lib/active_model/validations/inclusion.rb +18 -13
  31. data/lib/active_model/validations/length.rb +67 -65
  32. data/lib/active_model/validations/numericality.rb +83 -58
  33. data/lib/active_model/validations/presence.rb +10 -8
  34. data/lib/active_model/validations/validates.rb +110 -0
  35. data/lib/active_model/validations/with.rb +90 -23
  36. data/lib/active_model/validator.rb +186 -0
  37. data/lib/active_model/version.rb +3 -2
  38. metadata +79 -20
  39. data/README +0 -21
  40. data/lib/active_model/state_machine.rb +0 -70
  41. data/lib/active_model/state_machine/event.rb +0 -62
  42. data/lib/active_model/state_machine/machine.rb +0 -75
  43. data/lib/active_model/state_machine/state.rb +0 -47
  44. data/lib/active_model/state_machine/state_transition.rb +0 -40
  45. data/lib/active_model/validations_repair_helper.rb +0 -35
@@ -1,5 +1,7 @@
1
- # You can test whether an object is compliant with the ActiveModel API by
2
- # including ActiveModel::Lint::Tests in your TestCase. It will included
1
+ # == Active Model Lint Tests
2
+ #
3
+ # You can test whether an object is compliant with the Active Model API by
4
+ # including <tt>ActiveModel::Lint::Tests</tt> in your TestCase. It will include
3
5
  # tests that tell you whether your object is fully compliant, or if not,
4
6
  # which aspects of the API are not implemented.
5
7
  #
@@ -13,8 +15,34 @@
13
15
  module ActiveModel
14
16
  module Lint
15
17
  module Tests
16
- # valid?
17
- # ------
18
+
19
+ # == Responds to <tt>to_key</tt>
20
+ #
21
+ # Returns an Enumerable of all (primary) key attributes
22
+ # or nil if model.persisted? is false
23
+ def test_to_key
24
+ assert model.respond_to?(:to_key), "The model should respond to to_key"
25
+ def model.persisted?() false end
26
+ assert model.to_key.nil?
27
+ end
28
+
29
+ # == Responds to <tt>to_param</tt>
30
+ #
31
+ # Returns a string representing the object's key suitable for use in URLs
32
+ # or nil if model.persisted? is false.
33
+ #
34
+ # Implementers can decide to either raise an exception or provide a default
35
+ # in case the record uses a composite primary key. There are no tests for this
36
+ # behavior in lint because it doesn't make sense to force any of the possible
37
+ # implementation strategies on the implementer. However, if the resource is
38
+ # not persisted?, then to_param should always return nil.
39
+ def test_to_param
40
+ assert model.respond_to?(:to_param), "The model should respond to to_param"
41
+ def model.persisted?() false end
42
+ assert model.to_param.nil?
43
+ end
44
+
45
+ # == Responds to <tt>valid?</tt>
18
46
  #
19
47
  # Returns a boolean that specifies whether the object is in a valid or invalid
20
48
  # state.
@@ -23,30 +51,38 @@ module ActiveModel
23
51
  assert_boolean model.valid?, "valid?"
24
52
  end
25
53
 
26
- # new_record?
27
- # -----------
54
+ # == Responds to <tt>persisted?</tt>
28
55
  #
29
56
  # Returns a boolean that specifies whether the object has been persisted yet.
30
57
  # This is used when calculating the URL for an object. If the object is
31
58
  # not persisted, a form for that object, for instance, will be POSTed to the
32
- # collection. If it is persisted, a form for the object will put PUTed to the
59
+ # collection. If it is persisted, a form for the object will be PUT to the
33
60
  # URL for the object.
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?"
61
+ def test_persisted?
62
+ assert model.respond_to?(:persisted?), "The model should respond to persisted?"
63
+ assert_boolean model.persisted?, "persisted?"
37
64
  end
38
65
 
39
- def test_destroyed?
40
- assert model.respond_to?(:destroyed?), "The model should respond to destroyed?"
41
- assert_boolean model.destroyed?, "destroyed?"
66
+ # == Naming
67
+ #
68
+ # Model.model_name must return a string with some convenience methods as
69
+ # :human and :partial_path. Check ActiveModel::Naming for more information.
70
+ #
71
+ def test_model_naming
72
+ assert model.class.respond_to?(:model_name), "The model should respond to model_name"
73
+ model_name = model.class.model_name
74
+ assert_kind_of String, model_name
75
+ assert_kind_of String, model_name.human
76
+ assert_kind_of String, model_name.partial_path
77
+ assert_kind_of String, model_name.singular
78
+ assert_kind_of String, model_name.plural
42
79
  end
43
80
 
44
- # errors
45
- # ------
46
- #
81
+ # == Errors Testing
82
+ #
47
83
  # Returns an object that has :[] and :full_messages defined on it. See below
48
84
  # for more details.
49
-
85
+ #
50
86
  # Returns an Array of Strings that are the errors for the attribute in
51
87
  # question. If localization is used, the Strings should be localized
52
88
  # for the current locale. If no error is present, this method should
@@ -1,24 +1,27 @@
1
1
  en:
2
- activemodel:
3
- errors:
4
- # The values :model, :attribute and :value are always available for interpolation
5
- # The value :count is available when applicable. Can be used for pluralization.
6
- messages:
7
- inclusion: "is not included in the list"
8
- exclusion: "is reserved"
9
- invalid: "is invalid"
10
- confirmation: "doesn't match confirmation"
11
- accepted: "must be accepted"
12
- empty: "can't be empty"
13
- blank: "can't be blank"
14
- too_long: "is too long (maximum is {{count}} characters)"
15
- too_short: "is too short (minimum is {{count}} characters)"
16
- wrong_length: "is the wrong length (should be {{count}} characters)"
17
- not_a_number: "is not a number"
18
- greater_than: "must be greater than {{count}}"
19
- greater_than_or_equal_to: "must be greater than or equal to {{count}}"
20
- equal_to: "must be equal to {{count}}"
21
- less_than: "must be less than {{count}}"
22
- less_than_or_equal_to: "must be less than or equal to {{count}}"
23
- odd: "must be odd"
24
- even: "must be even"
2
+ errors:
3
+ # The default format to use in full error messages.
4
+ format: "%{attribute} %{message}"
5
+
6
+ # The values :model, :attribute and :value are always available for interpolation
7
+ # The value :count is available when applicable. Can be used for pluralization.
8
+ messages:
9
+ inclusion: "is not included in the list"
10
+ exclusion: "is reserved"
11
+ invalid: "is invalid"
12
+ confirmation: "doesn't match confirmation"
13
+ accepted: "must be accepted"
14
+ empty: "can't be empty"
15
+ blank: "can't be blank"
16
+ too_long: "is too long (maximum is %{count} characters)"
17
+ too_short: "is too short (minimum is %{count} characters)"
18
+ wrong_length: "is the wrong length (should be %{count} characters)"
19
+ not_a_number: "is not a number"
20
+ not_an_integer: "must be an integer"
21
+ greater_than: "must be greater than %{count}"
22
+ greater_than_or_equal_to: "must be greater than or equal to %{count}"
23
+ equal_to: "must be equal to %{count}"
24
+ less_than: "must be less than %{count}"
25
+ less_than_or_equal_to: "must be less than or equal to %{count}"
26
+ odd: "must be odd"
27
+ even: "must be even"
@@ -0,0 +1,160 @@
1
+ require 'active_support/core_ext/class/attribute.rb'
2
+ require 'active_model/mass_assignment_security/permission_set'
3
+
4
+ module ActiveModel
5
+ # = Active Model Mass-Assignment Security
6
+ module MassAssignmentSecurity
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ class_attribute :_accessible_attributes
11
+ class_attribute :_protected_attributes
12
+ class_attribute :_active_authorizer
13
+ end
14
+
15
+ # Mass assignment security provides an interface for protecting attributes
16
+ # from end-user assignment. For more complex permissions, mass assignment security
17
+ # may be handled outside the model by extending a non-ActiveRecord class,
18
+ # such as a controller, with this behavior.
19
+ #
20
+ # For example, a logged in user may need to assign additional attributes depending
21
+ # on their role:
22
+ #
23
+ # class AccountsController < ApplicationController
24
+ # include ActiveModel::MassAssignmentSecurity
25
+ #
26
+ # attr_accessible :first_name, :last_name
27
+ #
28
+ # def self.admin_accessible_attributes
29
+ # accessible_attributes + [ :plan_id ]
30
+ # end
31
+ #
32
+ # def update
33
+ # ...
34
+ # @account.update_attributes(account_params)
35
+ # ...
36
+ # end
37
+ #
38
+ # protected
39
+ #
40
+ # def account_params
41
+ # sanitize_for_mass_assignment(params[:account])
42
+ # end
43
+ #
44
+ # def mass_assignment_authorizer
45
+ # admin ? admin_accessible_attributes : super
46
+ # end
47
+ #
48
+ # end
49
+ #
50
+ module ClassMethods
51
+ # Attributes named in this macro are protected from mass-assignment
52
+ # whenever attributes are sanitized before assignment.
53
+ #
54
+ # Mass-assignment to these attributes will simply be ignored, to assign
55
+ # to them you can use direct writer methods. This is meant to protect
56
+ # sensitive attributes from being overwritten by malicious users
57
+ # tampering with URLs or forms.
58
+ #
59
+ # == Example
60
+ #
61
+ # class Customer
62
+ # include ActiveModel::MassAssignmentSecurity
63
+ #
64
+ # attr_accessor :name, :credit_rating
65
+ # attr_protected :credit_rating
66
+ #
67
+ # def attributes=(values)
68
+ # sanitize_for_mass_assignment(values).each do |k, v|
69
+ # send("#{k}=", v)
70
+ # end
71
+ # end
72
+ # end
73
+ #
74
+ # customer = Customer.new
75
+ # customer.attributes = { "name" => "David", "credit_rating" => "Excellent" }
76
+ # customer.name # => "David"
77
+ # customer.credit_rating # => nil
78
+ #
79
+ # customer.credit_rating = "Average"
80
+ # customer.credit_rating # => "Average"
81
+ #
82
+ # To start from an all-closed default and enable attributes as needed,
83
+ # have a look at +attr_accessible+.
84
+ #
85
+ # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+
86
+ # to sanitize attributes won't provide sufficient protection.
87
+ def attr_protected(*names)
88
+ self._protected_attributes = self.protected_attributes + names
89
+ self._active_authorizer = self._protected_attributes
90
+ end
91
+
92
+ # Specifies a white list of model attributes that can be set via
93
+ # mass-assignment.
94
+ #
95
+ # This is the opposite of the +attr_protected+ macro: Mass-assignment
96
+ # will only set attributes in this list, to assign to the rest of
97
+ # attributes you can use direct writer methods. This is meant to protect
98
+ # sensitive attributes from being overwritten by malicious users
99
+ # tampering with URLs or forms. If you'd rather start from an all-open
100
+ # default and restrict attributes as needed, have a look at
101
+ # +attr_protected+.
102
+ #
103
+ # class Customer
104
+ # include ActiveModel::MassAssignmentSecurity
105
+ #
106
+ # attr_accessor :name, :credit_rating
107
+ # attr_accessible :name
108
+ #
109
+ # def attributes=(values)
110
+ # sanitize_for_mass_assignment(values).each do |k, v|
111
+ # send("#{k}=", v)
112
+ # end
113
+ # end
114
+ # end
115
+ #
116
+ # customer = Customer.new
117
+ # customer.attributes = { :name => "David", :credit_rating => "Excellent" }
118
+ # customer.name # => "David"
119
+ # customer.credit_rating # => nil
120
+ #
121
+ # customer.credit_rating = "Average"
122
+ # customer.credit_rating # => "Average"
123
+ #
124
+ # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+
125
+ # to sanitize attributes won't provide sufficient protection.
126
+ def attr_accessible(*names)
127
+ self._accessible_attributes = self.accessible_attributes + names
128
+ self._active_authorizer = self._accessible_attributes
129
+ end
130
+
131
+ def protected_attributes
132
+ self._protected_attributes ||= BlackList.new(attributes_protected_by_default).tap do |w|
133
+ w.logger = self.logger if self.respond_to?(:logger)
134
+ end
135
+ end
136
+
137
+ def accessible_attributes
138
+ self._accessible_attributes ||= WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) }
139
+ end
140
+
141
+ def active_authorizer
142
+ self._active_authorizer ||= protected_attributes
143
+ end
144
+
145
+ def attributes_protected_by_default
146
+ []
147
+ end
148
+ end
149
+
150
+ protected
151
+
152
+ def sanitize_for_mass_assignment(attributes)
153
+ mass_assignment_authorizer.sanitize(attributes)
154
+ end
155
+
156
+ def mass_assignment_authorizer
157
+ self.class.active_authorizer
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,40 @@
1
+ require 'set'
2
+ require 'active_model/mass_assignment_security/sanitizer'
3
+
4
+ module ActiveModel
5
+ module MassAssignmentSecurity
6
+ class PermissionSet < Set
7
+ attr_accessor :logger
8
+
9
+ def +(values)
10
+ super(values.map(&:to_s))
11
+ end
12
+
13
+ def include?(key)
14
+ super(remove_multiparameter_id(key))
15
+ end
16
+
17
+ protected
18
+
19
+ def remove_multiparameter_id(key)
20
+ key.to_s.gsub(/\(.+/, '')
21
+ end
22
+ end
23
+
24
+ class WhiteList < PermissionSet
25
+ include Sanitizer
26
+
27
+ def deny?(key)
28
+ !include?(key)
29
+ end
30
+ end
31
+
32
+ class BlackList < PermissionSet
33
+ include Sanitizer
34
+
35
+ def deny?(key)
36
+ include?(key)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveModel
2
+ module MassAssignmentSecurity
3
+ module Sanitizer
4
+ # Returns all attributes not denied by the authorizer.
5
+ def sanitize(attributes)
6
+ sanitized_attributes = attributes.reject { |key, value| deny?(key) }
7
+ debug_protected_attribute_removal(attributes, sanitized_attributes)
8
+ sanitized_attributes
9
+ end
10
+
11
+ protected
12
+
13
+ def debug_protected_attribute_removal(attributes, sanitized_attributes)
14
+ removed_keys = attributes.keys - sanitized_attributes.keys
15
+ warn!(removed_keys) if removed_keys.any?
16
+ end
17
+
18
+ def warn!(attrs)
19
+ self.logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" if self.logger
20
+ end
21
+ end
22
+ end
23
+ end
@@ -2,25 +2,90 @@ require 'active_support/inflector'
2
2
 
3
3
  module ActiveModel
4
4
  class Name < String
5
- attr_reader :singular, :plural, :element, :collection, :partial_path, :human
5
+ attr_reader :singular, :plural, :element, :collection, :partial_path
6
6
  alias_method :cache_key, :collection
7
7
 
8
- def initialize(name)
9
- super
8
+ def initialize(klass)
9
+ super(klass.name)
10
+ @klass = klass
10
11
  @singular = ActiveSupport::Inflector.underscore(self).tr('/', '_').freeze
11
12
  @plural = ActiveSupport::Inflector.pluralize(@singular).freeze
12
13
  @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze
13
- @human = @element.gsub(/_/, " ")
14
+ @human = ActiveSupport::Inflector.humanize(@element).freeze
14
15
  @collection = ActiveSupport::Inflector.tableize(self).freeze
15
16
  @partial_path = "#{@collection}/#{@element}".freeze
16
17
  end
18
+
19
+ # Transform the model name into a more humane format, using I18n. By default,
20
+ # it will underscore then humanize the class name (BlogPost.model_name.human #=> "Blog post").
21
+ # Specify +options+ with additional translating options.
22
+ def human(options={})
23
+ return @human unless @klass.respond_to?(:lookup_ancestors) &&
24
+ @klass.respond_to?(:i18n_scope)
25
+
26
+ defaults = @klass.lookup_ancestors.map do |klass|
27
+ klass.model_name.underscore.to_sym
28
+ end
29
+
30
+ defaults << options.delete(:default) if options[:default]
31
+ defaults << @human
32
+
33
+ options.reverse_merge! :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults
34
+ I18n.translate(defaults.shift, options)
35
+ end
17
36
  end
18
37
 
38
+ # == Active Model Naming
39
+ #
40
+ # Creates a +model_name+ method on your object.
41
+ #
42
+ # To implement, just extend ActiveModel::Naming in your object:
43
+ #
44
+ # class BookCover
45
+ # extend ActiveModel::Naming
46
+ # end
47
+ #
48
+ # BookCover.model_name #=> "BookCover"
49
+ # BookCover.model_name.human #=> "Book cover"
50
+ #
51
+ # Providing the functionality that ActiveModel::Naming provides in your object
52
+ # is required to pass the Active Model Lint test. So either extending the provided
53
+ # method below, or rolling your own is required..
19
54
  module Naming
20
55
  # Returns an ActiveModel::Name object for module. It can be
21
56
  # used to retrieve all kinds of naming-related information.
22
57
  def model_name
23
- @_model_name ||= ActiveModel::Name.new(name)
58
+ @_model_name ||= ActiveModel::Name.new(self)
59
+ end
60
+
61
+ # Returns the plural class name of a record or class. Examples:
62
+ #
63
+ # ActiveModel::Naming.plural(post) # => "posts"
64
+ # ActiveModel::Naming.plural(Highrise::Person) # => "highrise_people"
65
+ def self.plural(record_or_class)
66
+ model_name_from_record_or_class(record_or_class).plural
67
+ end
68
+
69
+ # Returns the singular class name of a record or class. Examples:
70
+ #
71
+ # ActiveModel::Naming.singular(post) # => "post"
72
+ # ActiveModel::Naming.singular(Highrise::Person) # => "highrise_person"
73
+ def self.singular(record_or_class)
74
+ model_name_from_record_or_class(record_or_class).singular
24
75
  end
76
+
77
+ # Identifies whether the class name of a record or class is uncountable. Examples:
78
+ #
79
+ # ActiveModel::Naming.uncountable?(Sheep) # => true
80
+ # ActiveModel::Naming.uncountable?(Post) => false
81
+ def self.uncountable?(record_or_class)
82
+ plural(record_or_class) == singular(record_or_class)
83
+ end
84
+
85
+ private
86
+ def self.model_name_from_record_or_class(record_or_class)
87
+ (record_or_class.is_a?(Class) ? record_or_class : record_or_class.class).model_name
88
+ end
25
89
  end
90
+
26
91
  end