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.
- data/CHANGELOG +44 -1
- data/MIT-LICENSE +1 -1
- data/README.rdoc +184 -0
- data/lib/active_model.rb +29 -19
- data/lib/active_model/attribute_methods.rb +167 -46
- data/lib/active_model/callbacks.rb +134 -0
- data/lib/active_model/conversion.rb +41 -1
- data/lib/active_model/deprecated_error_methods.rb +1 -1
- data/lib/active_model/dirty.rb +56 -12
- data/lib/active_model/errors.rb +205 -46
- data/lib/active_model/lint.rb +53 -17
- data/lib/active_model/locale/en.yml +26 -23
- data/lib/active_model/mass_assignment_security.rb +160 -0
- data/lib/active_model/mass_assignment_security/permission_set.rb +40 -0
- data/lib/active_model/mass_assignment_security/sanitizer.rb +23 -0
- data/lib/active_model/naming.rb +70 -5
- data/lib/active_model/observing.rb +40 -16
- data/lib/active_model/railtie.rb +2 -0
- data/lib/active_model/serialization.rb +59 -0
- data/lib/active_model/serializers/json.rb +17 -11
- data/lib/active_model/serializers/xml.rb +66 -123
- data/lib/active_model/test_case.rb +0 -2
- data/lib/active_model/translation.rb +64 -0
- data/lib/active_model/validations.rb +150 -68
- data/lib/active_model/validations/acceptance.rb +53 -33
- data/lib/active_model/validations/callbacks.rb +57 -0
- data/lib/active_model/validations/confirmation.rb +41 -23
- data/lib/active_model/validations/exclusion.rb +18 -13
- data/lib/active_model/validations/format.rb +28 -24
- data/lib/active_model/validations/inclusion.rb +18 -13
- data/lib/active_model/validations/length.rb +67 -65
- data/lib/active_model/validations/numericality.rb +83 -58
- data/lib/active_model/validations/presence.rb +10 -8
- data/lib/active_model/validations/validates.rb +110 -0
- data/lib/active_model/validations/with.rb +90 -23
- data/lib/active_model/validator.rb +186 -0
- data/lib/active_model/version.rb +3 -2
- metadata +79 -20
- data/README +0 -21
- data/lib/active_model/state_machine.rb +0 -70
- data/lib/active_model/state_machine/event.rb +0 -62
- data/lib/active_model/state_machine/machine.rb +0 -75
- data/lib/active_model/state_machine/state.rb +0 -47
- data/lib/active_model/state_machine/state_transition.rb +0 -40
- data/lib/active_model/validations_repair_helper.rb +0 -35
data/lib/active_model/lint.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
#
|
2
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
59
|
+
# collection. If it is persisted, a form for the object will be PUT to the
|
33
60
|
# URL for the object.
|
34
|
-
def
|
35
|
-
assert model.respond_to?(:
|
36
|
-
assert_boolean model.
|
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
|
-
|
40
|
-
|
41
|
-
|
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
|
-
#
|
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
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
data/lib/active_model/naming.rb
CHANGED
@@ -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
|
5
|
+
attr_reader :singular, :plural, :element, :collection, :partial_path
|
6
6
|
alias_method :cache_key, :collection
|
7
7
|
|
8
|
-
def initialize(
|
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.
|
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(
|
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
|