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.
- 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
|