activemodel 3.1.12 → 3.2.0.rc1
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.md +81 -36
- data/README.rdoc +1 -1
- data/lib/active_model/attribute_methods.rb +123 -104
- data/lib/active_model/callbacks.rb +2 -2
- data/lib/active_model/conversion.rb +26 -2
- data/lib/active_model/dirty.rb +3 -3
- data/lib/active_model/errors.rb +63 -51
- data/lib/active_model/lint.rb +12 -3
- data/lib/active_model/mass_assignment_security.rb +27 -8
- data/lib/active_model/mass_assignment_security/permission_set.rb +5 -5
- data/lib/active_model/mass_assignment_security/sanitizer.rb +42 -6
- data/lib/active_model/naming.rb +18 -10
- data/lib/active_model/observer_array.rb +3 -3
- data/lib/active_model/observing.rb +1 -2
- data/lib/active_model/secure_password.rb +2 -2
- data/lib/active_model/serialization.rb +61 -10
- data/lib/active_model/serializers/json.rb +20 -14
- data/lib/active_model/serializers/xml.rb +55 -31
- data/lib/active_model/translation.rb +15 -3
- data/lib/active_model/validations.rb +1 -1
- data/lib/active_model/validations/acceptance.rb +3 -1
- data/lib/active_model/validations/confirmation.rb +3 -1
- data/lib/active_model/validations/exclusion.rb +5 -3
- data/lib/active_model/validations/format.rb +4 -2
- data/lib/active_model/validations/inclusion.rb +5 -3
- data/lib/active_model/validations/length.rb +22 -10
- data/lib/active_model/validations/numericality.rb +4 -2
- data/lib/active_model/validations/presence.rb +5 -3
- data/lib/active_model/validations/validates.rb +15 -3
- data/lib/active_model/validations/with.rb +4 -2
- data/lib/active_model/version.rb +3 -3
- metadata +21 -28
- checksums.yaml +0 -7
@@ -1,9 +1,14 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
|
1
3
|
module ActiveModel
|
2
4
|
module MassAssignmentSecurity
|
3
|
-
|
5
|
+
class Sanitizer
|
6
|
+
def initialize(target=nil)
|
7
|
+
end
|
8
|
+
|
4
9
|
# Returns all attributes not denied by the authorizer.
|
5
|
-
def sanitize(attributes)
|
6
|
-
sanitized_attributes = attributes.reject { |key, value| deny?(key) }
|
10
|
+
def sanitize(attributes, authorizer)
|
11
|
+
sanitized_attributes = attributes.reject { |key, value| authorizer.deny?(key) }
|
7
12
|
debug_protected_attribute_removal(attributes, sanitized_attributes)
|
8
13
|
sanitized_attributes
|
9
14
|
end
|
@@ -12,12 +17,43 @@ module ActiveModel
|
|
12
17
|
|
13
18
|
def debug_protected_attribute_removal(attributes, sanitized_attributes)
|
14
19
|
removed_keys = attributes.keys - sanitized_attributes.keys
|
15
|
-
|
20
|
+
process_removed_attributes(removed_keys) if removed_keys.any?
|
21
|
+
end
|
22
|
+
|
23
|
+
def process_removed_attributes(attrs)
|
24
|
+
raise NotImplementedError, "#process_removed_attributes(attrs) suppose to be overwritten"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class LoggerSanitizer < Sanitizer
|
29
|
+
delegate :logger, :to => :@target
|
30
|
+
|
31
|
+
def initialize(target)
|
32
|
+
@target = target
|
33
|
+
super
|
16
34
|
end
|
17
35
|
|
18
|
-
def
|
19
|
-
|
36
|
+
def logger?
|
37
|
+
@target.respond_to?(:logger) && @target.logger
|
20
38
|
end
|
39
|
+
|
40
|
+
def process_removed_attributes(attrs)
|
41
|
+
logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" if logger?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class StrictSanitizer < Sanitizer
|
46
|
+
def process_removed_attributes(attrs)
|
47
|
+
return if (attrs - insensitive_attributes).empty?
|
48
|
+
raise ActiveModel::MassAssignmentSecurity::Error, "Can't mass-assign protected attributes: #{attrs.join(', ')}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def insensitive_attributes
|
52
|
+
['id']
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Error < StandardError
|
21
57
|
end
|
22
58
|
end
|
23
59
|
end
|
data/lib/active_model/naming.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'active_support/inflector'
|
2
2
|
require 'active_support/core_ext/hash/except'
|
3
3
|
require 'active_support/core_ext/module/introspection'
|
4
|
+
require 'active_support/core_ext/module/deprecation'
|
4
5
|
|
5
6
|
module ActiveModel
|
6
7
|
class Name < String
|
@@ -9,17 +10,22 @@ module ActiveModel
|
|
9
10
|
|
10
11
|
alias_method :cache_key, :collection
|
11
12
|
|
13
|
+
deprecate :partial_path => "ActiveModel::Name#partial_path is deprecated. Call #to_partial_path on model instances directly instead."
|
14
|
+
|
12
15
|
def initialize(klass, namespace = nil, name = nil)
|
13
16
|
name ||= klass.name
|
17
|
+
|
18
|
+
raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if name.blank?
|
19
|
+
|
14
20
|
super(name)
|
15
|
-
@unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace
|
16
21
|
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@
|
20
|
-
@
|
21
|
-
@
|
22
|
-
@
|
22
|
+
@unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace
|
23
|
+
@klass = klass
|
24
|
+
@singular = _singularize(self).freeze
|
25
|
+
@plural = ActiveSupport::Inflector.pluralize(@singular).freeze
|
26
|
+
@element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze
|
27
|
+
@human = ActiveSupport::Inflector.humanize(@element).freeze
|
28
|
+
@collection = ActiveSupport::Inflector.tableize(self).freeze
|
23
29
|
@partial_path = "#{@collection}/#{@element}".freeze
|
24
30
|
@param_key = (namespace ? _singularize(@unnamespaced) : @singular).freeze
|
25
31
|
@i18n_key = self.underscore.to_sym
|
@@ -71,8 +77,8 @@ module ActiveModel
|
|
71
77
|
# BookCover.model_name # => "BookCover"
|
72
78
|
# BookCover.model_name.human # => "Book cover"
|
73
79
|
#
|
74
|
-
# BookCover.model_name.i18n_key # =>
|
75
|
-
# BookModule::BookCover.model_name.i18n_key # => "book_module
|
80
|
+
# BookCover.model_name.i18n_key # => :book_cover
|
81
|
+
# BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover"
|
76
82
|
#
|
77
83
|
# Providing the functionality that ActiveModel::Naming provides in your object
|
78
84
|
# is required to pass the Active Model Lint test. So either extending the provided
|
@@ -82,7 +88,9 @@ module ActiveModel
|
|
82
88
|
# used to retrieve all kinds of naming-related information.
|
83
89
|
def model_name
|
84
90
|
@_model_name ||= begin
|
85
|
-
namespace = self.parents.detect
|
91
|
+
namespace = self.parents.detect do |n|
|
92
|
+
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
|
93
|
+
end
|
86
94
|
ActiveModel::Name.new(self, namespace)
|
87
95
|
end
|
88
96
|
end
|
@@ -15,7 +15,7 @@ module ActiveModel
|
|
15
15
|
disabled_observers.include?(observer.class)
|
16
16
|
end
|
17
17
|
|
18
|
-
# Disables one or more observers.
|
18
|
+
# Disables one or more observers. This supports multiple forms:
|
19
19
|
#
|
20
20
|
# ORM.observers.disable :user_observer
|
21
21
|
# # => disables the UserObserver
|
@@ -38,7 +38,7 @@ module ActiveModel
|
|
38
38
|
set_enablement(false, observers, &block)
|
39
39
|
end
|
40
40
|
|
41
|
-
# Enables one or more observers.
|
41
|
+
# Enables one or more observers. This supports multiple forms:
|
42
42
|
#
|
43
43
|
# ORM.observers.enable :user_observer
|
44
44
|
# # => enables the UserObserver
|
@@ -59,7 +59,7 @@ module ActiveModel
|
|
59
59
|
# # just the duration of the block
|
60
60
|
# end
|
61
61
|
#
|
62
|
-
# Note: all observers are enabled by default.
|
62
|
+
# Note: all observers are enabled by default. This method is only
|
63
63
|
# useful when you have previously disabled one or more observers.
|
64
64
|
def enable(*observers, &block)
|
65
65
|
set_enablement(true, observers, &block)
|
@@ -187,8 +187,7 @@ module ActiveModel
|
|
187
187
|
def observe(*models)
|
188
188
|
models.flatten!
|
189
189
|
models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model }
|
190
|
-
|
191
|
-
define_method(:observed_classes) { models }
|
190
|
+
redefine_method(:observed_classes) { models }
|
192
191
|
end
|
193
192
|
|
194
193
|
# Returns an array of Classes to observe.
|
@@ -32,8 +32,8 @@ module ActiveModel
|
|
32
32
|
# User.find_by_name("david").try(:authenticate, "notright") # => nil
|
33
33
|
# User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user
|
34
34
|
def has_secure_password
|
35
|
-
# Load bcrypt-ruby only when
|
36
|
-
# (and by extension the entire framework) dependent on a binary library.
|
35
|
+
# Load bcrypt-ruby only when has_secure_password is used.
|
36
|
+
# This is to avoid ActiveModel (and by extension the entire framework) being dependent on a binary library.
|
37
37
|
gem 'bcrypt-ruby', '~> 3.0.0'
|
38
38
|
require 'bcrypt'
|
39
39
|
|
@@ -33,7 +33,7 @@ module ActiveModel
|
|
33
33
|
# you want to serialize and their current value.
|
34
34
|
#
|
35
35
|
# Most of the time though, you will want to include the JSON or XML
|
36
|
-
# serializations.
|
36
|
+
# serializations. Both of these modules automatically include the
|
37
37
|
# ActiveModel::Serialization module, so there is no need to explicitly
|
38
38
|
# include it.
|
39
39
|
#
|
@@ -71,18 +71,69 @@ module ActiveModel
|
|
71
71
|
def serializable_hash(options = nil)
|
72
72
|
options ||= {}
|
73
73
|
|
74
|
-
only = Array.wrap(options[:only]).map(&:to_s)
|
75
|
-
except = Array.wrap(options[:except]).map(&:to_s)
|
76
|
-
|
77
74
|
attribute_names = attributes.keys.sort
|
78
|
-
if only
|
79
|
-
attribute_names &= only
|
80
|
-
elsif except
|
81
|
-
attribute_names -= except
|
75
|
+
if only = options[:only]
|
76
|
+
attribute_names &= Array.wrap(only).map(&:to_s)
|
77
|
+
elsif except = options[:except]
|
78
|
+
attribute_names -= Array.wrap(except).map(&:to_s)
|
79
|
+
end
|
80
|
+
|
81
|
+
hash = {}
|
82
|
+
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
|
83
|
+
|
84
|
+
method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
|
85
|
+
method_names.each { |n| hash[n] = send(n) }
|
86
|
+
|
87
|
+
serializable_add_includes(options) do |association, records, opts|
|
88
|
+
hash[association] = if records.is_a?(Enumerable)
|
89
|
+
records.map { |a| a.serializable_hash(opts) }
|
90
|
+
else
|
91
|
+
records.serializable_hash(opts)
|
92
|
+
end
|
82
93
|
end
|
83
94
|
|
84
|
-
|
85
|
-
Hash[(attribute_names + method_names).map { |n| [n, send(n)] }]
|
95
|
+
hash
|
86
96
|
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# Hook method defining how an attribute value should be retrieved for
|
101
|
+
# serialization. By default this is assumed to be an instance named after
|
102
|
+
# the attribute. Override this method in subclasses should you need to
|
103
|
+
# retrieve the value for a given attribute differently:
|
104
|
+
#
|
105
|
+
# class MyClass
|
106
|
+
# include ActiveModel::Validations
|
107
|
+
#
|
108
|
+
# def initialize(data = {})
|
109
|
+
# @data = data
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# def read_attribute_for_serialization(key)
|
113
|
+
# @data[key]
|
114
|
+
# end
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
alias :read_attribute_for_serialization :send
|
118
|
+
|
119
|
+
# Add associations specified via the <tt>:include</tt> option.
|
120
|
+
#
|
121
|
+
# Expects a block that takes as arguments:
|
122
|
+
# +association+ - name of the association
|
123
|
+
# +records+ - the association record(s) to be serialized
|
124
|
+
# +opts+ - options for the association records
|
125
|
+
def serializable_add_includes(options = {}) #:nodoc:
|
126
|
+
return unless include = options[:include]
|
127
|
+
|
128
|
+
unless include.is_a?(Hash)
|
129
|
+
include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
|
130
|
+
end
|
131
|
+
|
132
|
+
include.each do |association, opts|
|
133
|
+
if records = send(association)
|
134
|
+
yield association, records, opts
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
87
138
|
end
|
88
139
|
end
|
@@ -15,7 +15,7 @@ module ActiveModel
|
|
15
15
|
self.include_root_in_json = true
|
16
16
|
end
|
17
17
|
|
18
|
-
# Returns a
|
18
|
+
# Returns a hash representing the model. Some configuration can be
|
19
19
|
# passed through +options+.
|
20
20
|
#
|
21
21
|
# The option <tt>include_root_in_json</tt> controls the top-level behavior
|
@@ -32,10 +32,17 @@ module ActiveModel
|
|
32
32
|
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
33
33
|
# "created_at": "2006/08/01", "awesome": true}
|
34
34
|
#
|
35
|
-
#
|
36
|
-
# is false.
|
35
|
+
# This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in:
|
37
36
|
#
|
38
|
-
#
|
37
|
+
# user = User.find(1)
|
38
|
+
# user.as_json(root: false)
|
39
|
+
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
40
|
+
# "created_at": "2006/08/01", "awesome": true}
|
41
|
+
#
|
42
|
+
# The remainder of the examples in this section assume include_root_in_json is set to
|
43
|
+
# <tt>false</tt>.
|
44
|
+
#
|
45
|
+
# Without any +options+, the returned Hash will include all the model's
|
39
46
|
# attributes. For example:
|
40
47
|
#
|
41
48
|
# user = User.find(1)
|
@@ -79,21 +86,20 @@ module ActiveModel
|
|
79
86
|
# "title": "Welcome to the weblog"},
|
80
87
|
# {"comments": [{"body": "Don't think too hard"}],
|
81
88
|
# "title": "So I was thinking"}]}
|
82
|
-
|
83
89
|
def as_json(options = nil)
|
84
|
-
|
85
|
-
|
86
|
-
if
|
87
|
-
|
88
|
-
|
90
|
+
root = include_root_in_json
|
91
|
+
root = options[:root] if options.try(:key?, :root)
|
92
|
+
if root
|
93
|
+
root = self.class.model_name.element if root == true
|
94
|
+
{ root => serializable_hash(options) }
|
95
|
+
else
|
96
|
+
serializable_hash(options)
|
89
97
|
end
|
90
|
-
|
91
|
-
hash
|
92
98
|
end
|
93
99
|
|
94
|
-
def from_json(json)
|
100
|
+
def from_json(json, include_root=include_root_in_json)
|
95
101
|
hash = ActiveSupport::JSON.decode(json)
|
96
|
-
hash = hash.values.first if
|
102
|
+
hash = hash.values.first if include_root
|
97
103
|
self.attributes = hash
|
98
104
|
self
|
99
105
|
end
|
@@ -15,10 +15,10 @@ module ActiveModel
|
|
15
15
|
class Attribute #:nodoc:
|
16
16
|
attr_reader :name, :value, :type
|
17
17
|
|
18
|
-
def initialize(name, serializable,
|
18
|
+
def initialize(name, serializable, value)
|
19
19
|
@name, @serializable = name, serializable
|
20
|
-
|
21
|
-
@value =
|
20
|
+
value = value.in_time_zone if value.respond_to?(:in_time_zone)
|
21
|
+
@value = value
|
22
22
|
@type = compute_type
|
23
23
|
end
|
24
24
|
|
@@ -49,40 +49,24 @@ module ActiveModel
|
|
49
49
|
def initialize(serializable, options = nil)
|
50
50
|
@serializable = serializable
|
51
51
|
@options = options ? options.dup : {}
|
52
|
-
|
53
|
-
@options[:only] = Array.wrap(@options[:only]).map { |n| n.to_s }
|
54
|
-
@options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s }
|
55
52
|
end
|
56
53
|
|
57
|
-
|
58
|
-
|
59
|
-
# for a N level model but is set for the N+1 level models,
|
60
|
-
# then because <tt>:except</tt> is set to a default value, the second
|
61
|
-
# level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if
|
62
|
-
# <tt>:only</tt> is set, always delete <tt>:except</tt>.
|
63
|
-
def attributes_hash
|
64
|
-
attributes = @serializable.attributes
|
65
|
-
if options[:only].any?
|
66
|
-
attributes.slice(*options[:only])
|
67
|
-
elsif options[:except].any?
|
68
|
-
attributes.except(*options[:except])
|
69
|
-
else
|
70
|
-
attributes
|
71
|
-
end
|
54
|
+
def serializable_hash
|
55
|
+
@serializable.serializable_hash(@options.except(:include))
|
72
56
|
end
|
73
57
|
|
74
|
-
def
|
75
|
-
|
76
|
-
|
58
|
+
def serializable_collection
|
59
|
+
methods = Array.wrap(options[:methods]).map(&:to_s)
|
60
|
+
serializable_hash.map do |name, value|
|
61
|
+
name = name.to_s
|
62
|
+
if methods.include?(name)
|
63
|
+
self.class::MethodAttribute.new(name, @serializable, value)
|
64
|
+
else
|
65
|
+
self.class::Attribute.new(name, @serializable, value)
|
66
|
+
end
|
77
67
|
end
|
78
68
|
end
|
79
69
|
|
80
|
-
def serializable_methods
|
81
|
-
Array.wrap(options[:methods]).map do |name|
|
82
|
-
self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s)
|
83
|
-
end.compact
|
84
|
-
end
|
85
|
-
|
86
70
|
def serialize
|
87
71
|
require 'builder' unless defined? ::Builder
|
88
72
|
|
@@ -101,6 +85,7 @@ module ActiveModel
|
|
101
85
|
|
102
86
|
@builder.tag!(*args) do
|
103
87
|
add_attributes_and_methods
|
88
|
+
add_includes
|
104
89
|
add_extra_behavior
|
105
90
|
add_procs
|
106
91
|
yield @builder if block_given?
|
@@ -113,13 +98,52 @@ module ActiveModel
|
|
113
98
|
end
|
114
99
|
|
115
100
|
def add_attributes_and_methods
|
116
|
-
|
101
|
+
serializable_collection.each do |attribute|
|
117
102
|
key = ActiveSupport::XmlMini.rename_key(attribute.name, options)
|
118
103
|
ActiveSupport::XmlMini.to_tag(key, attribute.value,
|
119
104
|
options.merge(attribute.decorations))
|
120
105
|
end
|
121
106
|
end
|
122
107
|
|
108
|
+
def add_includes
|
109
|
+
@serializable.send(:serializable_add_includes, options) do |association, records, opts|
|
110
|
+
add_associations(association, records, opts)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
|
115
|
+
def add_associations(association, records, opts)
|
116
|
+
merged_options = opts.merge(options.slice(:builder, :indent))
|
117
|
+
merged_options[:skip_instruct] = true
|
118
|
+
|
119
|
+
if records.is_a?(Enumerable)
|
120
|
+
tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
|
121
|
+
type = options[:skip_types] ? { } : {:type => "array"}
|
122
|
+
association_name = association.to_s.singularize
|
123
|
+
merged_options[:root] = association_name
|
124
|
+
|
125
|
+
if records.empty?
|
126
|
+
@builder.tag!(tag, type)
|
127
|
+
else
|
128
|
+
@builder.tag!(tag, type) do
|
129
|
+
records.each do |record|
|
130
|
+
if options[:skip_types]
|
131
|
+
record_type = {}
|
132
|
+
else
|
133
|
+
record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
|
134
|
+
record_type = {:type => record_class}
|
135
|
+
end
|
136
|
+
|
137
|
+
record.to_xml merged_options.merge(record_type)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
else
|
142
|
+
merged_options[:root] = association.to_s
|
143
|
+
records.to_xml(merged_options)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
123
147
|
def add_procs
|
124
148
|
if procs = options.delete(:procs)
|
125
149
|
Array.wrap(procs).each do |proc|
|