activemodel 3.1.12 → 3.2.0.rc1

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