activemodel 3.0.pre → 3.0.0.rc

Sign up to get free protection for your applications and to get access to all the features.
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