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.
Files changed (33) hide show
  1. data/CHANGELOG.md +81 -36
  2. data/README.rdoc +1 -1
  3. data/lib/active_model/attribute_methods.rb +123 -104
  4. data/lib/active_model/callbacks.rb +2 -2
  5. data/lib/active_model/conversion.rb +26 -2
  6. data/lib/active_model/dirty.rb +3 -3
  7. data/lib/active_model/errors.rb +63 -51
  8. data/lib/active_model/lint.rb +12 -3
  9. data/lib/active_model/mass_assignment_security.rb +27 -8
  10. data/lib/active_model/mass_assignment_security/permission_set.rb +5 -5
  11. data/lib/active_model/mass_assignment_security/sanitizer.rb +42 -6
  12. data/lib/active_model/naming.rb +18 -10
  13. data/lib/active_model/observer_array.rb +3 -3
  14. data/lib/active_model/observing.rb +1 -2
  15. data/lib/active_model/secure_password.rb +2 -2
  16. data/lib/active_model/serialization.rb +61 -10
  17. data/lib/active_model/serializers/json.rb +20 -14
  18. data/lib/active_model/serializers/xml.rb +55 -31
  19. data/lib/active_model/translation.rb +15 -3
  20. data/lib/active_model/validations.rb +1 -1
  21. data/lib/active_model/validations/acceptance.rb +3 -1
  22. data/lib/active_model/validations/confirmation.rb +3 -1
  23. data/lib/active_model/validations/exclusion.rb +5 -3
  24. data/lib/active_model/validations/format.rb +4 -2
  25. data/lib/active_model/validations/inclusion.rb +5 -3
  26. data/lib/active_model/validations/length.rb +22 -10
  27. data/lib/active_model/validations/numericality.rb +4 -2
  28. data/lib/active_model/validations/presence.rb +5 -3
  29. data/lib/active_model/validations/validates.rb +15 -3
  30. data/lib/active_model/validations/with.rb +4 -2
  31. data/lib/active_model/version.rb +3 -3
  32. metadata +21 -28
  33. 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
- module Sanitizer
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
- warn!(removed_keys) if removed_keys.any?
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 warn!(attrs)
19
- self.logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" if self.logger
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
@@ -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
- @klass = klass
18
- @singular = _singularize(self).freeze
19
- @plural = ActiveSupport::Inflector.pluralize(@singular).freeze
20
- @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze
21
- @human = ActiveSupport::Inflector.humanize(@element).freeze
22
- @collection = ActiveSupport::Inflector.tableize(self).freeze
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 # => "book_cover"
75
- # BookModule::BookCover.model_name.i18n_key # => "book_module.book_cover"
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 { |n| n.respond_to?(:_railtie) }
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. This supports multiple forms:
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. This supports multiple forms:
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. This method is only
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
- remove_possible_method(:observed_classes)
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 has_secured_password is used to avoid make ActiveModel
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. Both of these modules automatically include the
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.any?
79
- attribute_names &= only
80
- elsif except.any?
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
- method_names = Array.wrap(options[:methods]).map { |n| n if respond_to?(n.to_s) }.compact
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 JSON string representing the model. Some configuration can be
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
- # The remainder of the examples in this section assume +include_root_in_json+
36
- # is false.
35
+ # This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in:
37
36
  #
38
- # Without any +options+, the returned JSON string will include all the model's
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
- hash = serializable_hash(options)
85
-
86
- if include_root_in_json
87
- custom_root = options && options[:root]
88
- hash = { custom_root || self.class.model_name.element => hash }
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 include_root_in_json
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, raw_value=nil)
18
+ def initialize(name, serializable, value)
19
19
  @name, @serializable = name, serializable
20
- raw_value = raw_value.in_time_zone if raw_value.respond_to?(:in_time_zone)
21
- @value = raw_value || @serializable.send(name)
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
- # To replicate the behavior in ActiveRecord#attributes, <tt>:except</tt>
58
- # takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set
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 serializable_attributes
75
- attributes_hash.map do |name, value|
76
- self.class::Attribute.new(name, @serializable, value)
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
- (serializable_attributes + serializable_methods).each do |attribute|
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|