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.
Files changed (45) hide show
  1. data/CHANGELOG +44 -1
  2. data/MIT-LICENSE +1 -1
  3. data/README.rdoc +184 -0
  4. data/lib/active_model.rb +29 -19
  5. data/lib/active_model/attribute_methods.rb +167 -46
  6. data/lib/active_model/callbacks.rb +134 -0
  7. data/lib/active_model/conversion.rb +41 -1
  8. data/lib/active_model/deprecated_error_methods.rb +1 -1
  9. data/lib/active_model/dirty.rb +56 -12
  10. data/lib/active_model/errors.rb +205 -46
  11. data/lib/active_model/lint.rb +53 -17
  12. data/lib/active_model/locale/en.yml +26 -23
  13. data/lib/active_model/mass_assignment_security.rb +160 -0
  14. data/lib/active_model/mass_assignment_security/permission_set.rb +40 -0
  15. data/lib/active_model/mass_assignment_security/sanitizer.rb +23 -0
  16. data/lib/active_model/naming.rb +70 -5
  17. data/lib/active_model/observing.rb +40 -16
  18. data/lib/active_model/railtie.rb +2 -0
  19. data/lib/active_model/serialization.rb +59 -0
  20. data/lib/active_model/serializers/json.rb +17 -11
  21. data/lib/active_model/serializers/xml.rb +66 -123
  22. data/lib/active_model/test_case.rb +0 -2
  23. data/lib/active_model/translation.rb +64 -0
  24. data/lib/active_model/validations.rb +150 -68
  25. data/lib/active_model/validations/acceptance.rb +53 -33
  26. data/lib/active_model/validations/callbacks.rb +57 -0
  27. data/lib/active_model/validations/confirmation.rb +41 -23
  28. data/lib/active_model/validations/exclusion.rb +18 -13
  29. data/lib/active_model/validations/format.rb +28 -24
  30. data/lib/active_model/validations/inclusion.rb +18 -13
  31. data/lib/active_model/validations/length.rb +67 -65
  32. data/lib/active_model/validations/numericality.rb +83 -58
  33. data/lib/active_model/validations/presence.rb +10 -8
  34. data/lib/active_model/validations/validates.rb +110 -0
  35. data/lib/active_model/validations/with.rb +90 -23
  36. data/lib/active_model/validator.rb +186 -0
  37. data/lib/active_model/version.rb +3 -2
  38. metadata +79 -20
  39. data/README +0 -21
  40. data/lib/active_model/state_machine.rb +0 -70
  41. data/lib/active_model/state_machine/event.rb +0 -62
  42. data/lib/active_model/state_machine/machine.rb +0 -75
  43. data/lib/active_model/state_machine/state.rb +0 -47
  44. data/lib/active_model/state_machine/state_transition.rb +0 -40
  45. data/lib/active_model/validations_repair_helper.rb +0 -35
@@ -1,18 +1,16 @@
1
- require 'observer'
2
1
  require 'singleton'
3
2
  require 'active_support/core_ext/array/wrap'
4
3
  require 'active_support/core_ext/module/aliasing'
4
+ require 'active_support/core_ext/module/remove_method'
5
5
  require 'active_support/core_ext/string/inflections'
6
6
 
7
7
  module ActiveModel
8
8
  module Observing
9
9
  extend ActiveSupport::Concern
10
10
 
11
- included do
12
- extend Observable
13
- end
14
-
15
11
  module ClassMethods
12
+ # == Active Model Observers Activation
13
+ #
16
14
  # Activates the observers assigned. Examples:
17
15
  #
18
16
  # # Calls PersonObserver.instance
@@ -24,8 +22,9 @@ module ActiveModel
24
22
  # # Same as above, just using explicit class references
25
23
  # ActiveRecord::Base.observers = Cacher, GarbageCollector
26
24
  #
27
- # Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is
28
- # called during startup, and before each development request.
25
+ # Note: Setting this does not instantiate the observers yet.
26
+ # +instantiate_observers+ is called during startup, and before
27
+ # each development request.
29
28
  def observers=(*values)
30
29
  @observers = values.flatten
31
30
  end
@@ -40,6 +39,26 @@ module ActiveModel
40
39
  observers.each { |o| instantiate_observer(o) }
41
40
  end
42
41
 
42
+ def add_observer(observer)
43
+ unless observer.respond_to? :update
44
+ raise ArgumentError, "observer needs to respond to `update'"
45
+ end
46
+ @observer_instances ||= []
47
+ @observer_instances << observer
48
+ end
49
+
50
+ def notify_observers(*arg)
51
+ if defined? @observer_instances
52
+ for observer in @observer_instances
53
+ observer.update(*arg)
54
+ end
55
+ end
56
+ end
57
+
58
+ def count_observers
59
+ @observer_instances.size
60
+ end
61
+
43
62
  protected
44
63
  def instantiate_observer(observer) #:nodoc:
45
64
  # string/symbol
@@ -55,7 +74,6 @@ module ActiveModel
55
74
  # Notify observers when the observed class is subclassed.
56
75
  def inherited(subclass)
57
76
  super
58
- changed
59
77
  notify_observers :observed_class_inherited, subclass
60
78
  end
61
79
  end
@@ -69,11 +87,12 @@ module ActiveModel
69
87
  # notify_observers(:after_save)
70
88
  # end
71
89
  def notify_observers(method)
72
- self.class.changed
73
90
  self.class.notify_observers(method, self)
74
91
  end
75
92
  end
76
93
 
94
+ # == Active Model Observers
95
+ #
77
96
  # Observer classes respond to lifecycle callbacks to implement trigger-like
78
97
  # behavior outside the original class. This is a great way to reduce the
79
98
  # clutter that normally comes when the model class is burdened with
@@ -102,10 +121,12 @@ module ActiveModel
102
121
  #
103
122
  # == Observing a class that can't be inferred
104
123
  #
105
- # Observers will by default be mapped to the class with which they share a name. So CommentObserver will
106
- # be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
107
- # differently than the class you're interested in observing, you can use the Observer.observe class method which takes
108
- # either the concrete class (Product) or a symbol for that class (:product):
124
+ # Observers will by default be mapped to the class with which they share a
125
+ # name. So CommentObserver will be tied to observing Comment, ProductManagerObserver
126
+ # to ProductManager, and so on. If you want to name your observer differently than
127
+ # the class you're interested in observing, you can use the Observer.observe class
128
+ # method which takes either the concrete class (Product) or a symbol for that
129
+ # class (:product):
109
130
  #
110
131
  # class AuditObserver < ActiveModel::Observer
111
132
  # observe :account
@@ -115,7 +136,8 @@ module ActiveModel
115
136
  # end
116
137
  # end
117
138
  #
118
- # If the audit observer needs to watch more than one kind of object, this can be specified with multiple arguments:
139
+ # If the audit observer needs to watch more than one kind of object, this can be
140
+ # specified with multiple arguments:
119
141
  #
120
142
  # class AuditObserver < ActiveModel::Observer
121
143
  # observe :account, :balance
@@ -125,7 +147,8 @@ module ActiveModel
125
147
  # end
126
148
  # end
127
149
  #
128
- # The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
150
+ # The AuditObserver will now act on both updates to Account and Balance by treating
151
+ # them both as records.
129
152
  #
130
153
  class Observer
131
154
  include Singleton
@@ -135,6 +158,7 @@ module ActiveModel
135
158
  def observe(*models)
136
159
  models.flatten!
137
160
  models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model }
161
+ remove_possible_method(:observed_classes)
138
162
  define_method(:observed_classes) { models }
139
163
  end
140
164
 
@@ -144,7 +168,7 @@ module ActiveModel
144
168
  #
145
169
  # class AuditObserver < ActiveModel::Observer
146
170
  # def self.observed_classes
147
- # [AccountObserver, BalanceObserver]
171
+ # [Account, Balance]
148
172
  # end
149
173
  # end
150
174
  def observed_classes
@@ -0,0 +1,2 @@
1
+ require "active_model"
2
+ require "rails"
@@ -2,6 +2,65 @@ require 'active_support/core_ext/hash/except'
2
2
  require 'active_support/core_ext/hash/slice'
3
3
 
4
4
  module ActiveModel
5
+ # == Active Model Serialization
6
+ #
7
+ # Provides a basic serialization to a serializable_hash for your object.
8
+ #
9
+ # A minimal implementation could be:
10
+ #
11
+ # class Person
12
+ #
13
+ # include ActiveModel::Serialization
14
+ #
15
+ # attr_accessor :name
16
+ #
17
+ # def attributes
18
+ # @attributes ||= {'name' => 'nil'}
19
+ # end
20
+ #
21
+ # end
22
+ #
23
+ # Which would provide you with:
24
+ #
25
+ # person = Person.new
26
+ # person.serializable_hash # => {"name"=>nil}
27
+ # person.name = "Bob"
28
+ # person.serializable_hash # => {"name"=>"Bob"}
29
+ #
30
+ # You need to declare some sort of attributes hash which contains the attributes
31
+ # you want to serialize and their current value.
32
+ #
33
+ # Most of the time though, you will want to include the JSON or XML
34
+ # serializations. Both of these modules automatically include the
35
+ # ActiveModel::Serialization module, so there is no need to explicitly
36
+ # include it.
37
+ #
38
+ # So a minimal implementation including XML and JSON would be:
39
+ #
40
+ # class Person
41
+ #
42
+ # include ActiveModel::Serializers::JSON
43
+ # include ActiveModel::Serializers::Xml
44
+ #
45
+ # attr_accessor :name
46
+ #
47
+ # def attributes
48
+ # @attributes ||= {'name' => 'nil'}
49
+ # end
50
+ #
51
+ # end
52
+ #
53
+ # Which would provide you with:
54
+ #
55
+ # person = Person.new
56
+ # person.serializable_hash # => {"name"=>nil}
57
+ # person.to_json # => "{\"name\":null}"
58
+ # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
59
+ #
60
+ # person.name = "Bob"
61
+ # person.serializable_hash # => {"name"=>"Bob"}
62
+ # person.to_json # => "{\"name\":\"Bob\"}"
63
+ # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
5
64
  module Serialization
6
65
  def serializable_hash(options = nil)
7
66
  options ||= {}
@@ -1,7 +1,8 @@
1
1
  require 'active_support/json'
2
- require 'active_support/core_ext/class/attribute_accessors'
2
+ require 'active_support/core_ext/class/attribute'
3
3
 
4
4
  module ActiveModel
5
+ # == Active Model JSON Serializer
5
6
  module Serializers
6
7
  module JSON
7
8
  extend ActiveSupport::Concern
@@ -10,19 +11,18 @@ module ActiveModel
10
11
  included do
11
12
  extend ActiveModel::Naming
12
13
 
13
- cattr_accessor :include_root_in_json, :instance_writer => false
14
+ class_attribute :include_root_in_json
15
+ self.include_root_in_json = true
14
16
  end
15
17
 
16
- # Returns a JSON string representing the model. Some configuration is
17
- # available through +options+.
18
+ # Returns a JSON string representing the model. Some configuration can be
19
+ # passed through +options+.
18
20
  #
19
- # The option <tt>ActiveRecord::Base.include_root_in_json</tt> controls the
20
- # top-level behavior of to_json. In a new Rails application, it is set to
21
- # <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>,
22
- # to_json will emit a single root node named after the object's type. For example:
21
+ # The option <tt>ActiveModel::Base.include_root_in_json</tt> controls the
22
+ # top-level behavior of <tt>to_json</tt>. It is <tt>true</tt> by default. When it is <tt>true</tt>,
23
+ # <tt>to_json</tt> will emit a single root node named after the object's type. For example:
23
24
  #
24
25
  # konata = User.find(1)
25
- # ActiveRecord::Base.include_root_in_json = true
26
26
  # konata.to_json
27
27
  # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
28
28
  # "created_at": "2006/08/01", "awesome": true} }
@@ -81,7 +81,11 @@ module ActiveModel
81
81
  # "title": "So I was thinking"}]}
82
82
  def encode_json(encoder)
83
83
  hash = serializable_hash(encoder.options)
84
- hash = { self.class.model_name.element => hash } if include_root_in_json
84
+ if include_root_in_json
85
+ custom_root = encoder.options && encoder.options[:root]
86
+ hash = { custom_root || self.class.model_name.element => hash }
87
+ end
88
+
85
89
  ActiveSupport::JSON.encode(hash)
86
90
  end
87
91
 
@@ -90,7 +94,9 @@ module ActiveModel
90
94
  end
91
95
 
92
96
  def from_json(json)
93
- self.attributes = ActiveSupport::JSON.decode(json)
97
+ hash = ActiveSupport::JSON.decode(json)
98
+ hash = hash.values.first if include_root_in_json
99
+ self.attributes = hash
94
100
  self
95
101
  end
96
102
  end
@@ -1,7 +1,11 @@
1
+ require 'active_support/core_ext/array/wrap'
1
2
  require 'active_support/core_ext/class/attribute_accessors'
3
+ require 'active_support/core_ext/array/conversions'
2
4
  require 'active_support/core_ext/hash/conversions'
5
+ require 'active_support/core_ext/hash/slice'
3
6
 
4
7
  module ActiveModel
8
+ # == Active Model XML Serializer
5
9
  module Serializers
6
10
  module Xml
7
11
  extend ActiveSupport::Concern
@@ -11,68 +15,31 @@ module ActiveModel
11
15
  class Attribute #:nodoc:
12
16
  attr_reader :name, :value, :type
13
17
 
14
- def initialize(name, serializable)
18
+ def initialize(name, serializable, raw_value=nil)
15
19
  @name, @serializable = name, serializable
20
+ @value = value || @serializable.send(name)
16
21
  @type = compute_type
17
- @value = compute_value
18
22
  end
19
23
 
20
- # There is a significant speed improvement if the value
21
- # does not need to be escaped, as <tt>tag!</tt> escapes all values
22
- # to ensure that valid XML is generated. For known binary
23
- # values, it is at least an order of magnitude faster to
24
- # Base64 encode binary values and directly put them in the
25
- # output XML than to pass the original value or the Base64
26
- # encoded value to the <tt>tag!</tt> method. It definitely makes
27
- # no sense to Base64 encode the value and then give it to
28
- # <tt>tag!</tt>, since that just adds additional overhead.
29
- def needs_encoding?
30
- ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
31
- end
32
-
33
- def decorations(include_types = true)
24
+ def decorations
34
25
  decorations = {}
35
-
36
- if type == :binary
37
- decorations[:encoding] = 'base64'
38
- end
39
-
40
- if include_types && type != :string
41
- decorations[:type] = type
42
- end
43
-
44
- if value.nil?
45
- decorations[:nil] = true
46
- end
47
-
26
+ decorations[:encoding] = 'base64' if type == :binary
27
+ decorations[:type] = type unless type == :string
28
+ decorations[:nil] = true if value.nil?
48
29
  decorations
49
30
  end
50
31
 
51
- protected
52
- def compute_type
53
- value = @serializable.send(name)
54
- type = Hash::XML_TYPE_NAMES[value.class.name]
55
- type ||= :string if value.respond_to?(:to_str)
56
- type ||= :yaml
57
- type
58
- end
59
-
60
- def compute_value
61
- value = @serializable.send(name)
32
+ protected
62
33
 
63
- if formatter = Hash::XML_FORMATTING[type.to_s]
64
- value ? formatter.call(value) : nil
65
- else
66
- value
67
- end
68
- end
34
+ def compute_type
35
+ type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name]
36
+ type ||= :string if value.respond_to?(:to_str)
37
+ type ||= :yaml
38
+ type
39
+ end
69
40
  end
70
41
 
71
42
  class MethodAttribute < Attribute #:nodoc:
72
- protected
73
- def compute_type
74
- Hash::XML_TYPE_NAMES[@serializable.send(name).class.name] || :string
75
- end
76
43
  end
77
44
 
78
45
  attr_reader :options
@@ -85,112 +52,88 @@ module ActiveModel
85
52
  @options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s }
86
53
  end
87
54
 
88
- # To replicate the behavior in ActiveRecord#attributes,
89
- # <tt>:except</tt> takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set
55
+ # To replicate the behavior in ActiveRecord#attributes, <tt>:except</tt>
56
+ # takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set
90
57
  # for a N level model but is set for the N+1 level models,
91
58
  # then because <tt>:except</tt> is set to a default value, the second
92
59
  # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if
93
60
  # <tt>:only</tt> is set, always delete <tt>:except</tt>.
94
- def serializable_attribute_names
95
- attribute_names = @serializable.attributes.keys.sort
96
-
61
+ def attributes_hash
62
+ attributes = @serializable.attributes
97
63
  if options[:only].any?
98
- attribute_names &= options[:only]
64
+ attributes.slice(*options[:only])
99
65
  elsif options[:except].any?
100
- attribute_names -= options[:except]
66
+ attributes.except(*options[:except])
67
+ else
68
+ attributes
101
69
  end
102
-
103
- attribute_names
104
70
  end
105
71
 
106
72
  def serializable_attributes
107
- serializable_attribute_names.collect { |name| Attribute.new(name, @serializable) }
73
+ attributes_hash.map do |name, value|
74
+ self.class::Attribute.new(name, @serializable, value)
75
+ end
108
76
  end
109
77
 
110
- def serializable_method_attributes
111
- Array(options[:methods]).inject([]) do |methods, name|
112
- methods << MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s)
78
+ def serializable_methods
79
+ Array.wrap(options[:methods]).inject([]) do |methods, name|
80
+ methods << self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s)
113
81
  methods
114
82
  end
115
83
  end
116
84
 
117
85
  def serialize
118
- args = [root]
119
-
120
- if options[:namespace]
121
- args << {:xmlns => options[:namespace]}
122
- end
86
+ require 'builder' unless defined? ::Builder
123
87
 
124
- if options[:type]
125
- args << {:type => options[:type]}
126
- end
88
+ options[:indent] ||= 2
89
+ options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
127
90
 
128
- builder.tag!(*args) do
129
- add_attributes
130
- procs = options.delete(:procs)
131
- options[:procs] = procs
132
- add_procs
133
- yield builder if block_given?
134
- end
135
- end
91
+ @builder = options[:builder]
92
+ @builder.instruct! unless options[:skip_instruct]
136
93
 
137
- private
138
- def builder
139
- @builder ||= begin
140
- require 'builder' unless defined? ::Builder
141
- options[:indent] ||= 2
142
- builder = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
94
+ root = (options[:root] || @serializable.class.model_name.element).to_s
95
+ root = ActiveSupport::XmlMini.rename_key(root, options)
143
96
 
144
- unless options[:skip_instruct]
145
- builder.instruct!
146
- options[:skip_instruct] = true
147
- end
148
-
149
- builder
150
- end
151
- end
152
-
153
- def root
154
- root = (options[:root] || @serializable.class.model_name.singular).to_s
155
- reformat_name(root)
156
- end
97
+ args = [root]
98
+ args << {:xmlns => options[:namespace]} if options[:namespace]
99
+ args << {:type => options[:type]} if options[:type] && !options[:skip_types]
157
100
 
158
- def dasherize?
159
- !options.has_key?(:dasherize) || options[:dasherize]
101
+ @builder.tag!(*args) do
102
+ add_attributes_and_methods
103
+ add_extra_behavior
104
+ add_procs
105
+ yield @builder if block_given?
160
106
  end
107
+ end
161
108
 
162
- def camelize?
163
- options.has_key?(:camelize) && options[:camelize]
164
- end
109
+ private
165
110
 
166
- def reformat_name(name)
167
- name = name.camelize if camelize?
168
- dasherize? ? name.dasherize : name
169
- end
111
+ def add_extra_behavior
112
+ end
170
113
 
171
- def add_attributes
172
- (serializable_attributes + serializable_method_attributes).each do |attribute|
173
- builder.tag!(
174
- reformat_name(attribute.name),
175
- attribute.value.to_s,
176
- attribute.decorations(!options[:skip_types])
177
- )
178
- end
114
+ def add_attributes_and_methods
115
+ (serializable_attributes + serializable_methods).each do |attribute|
116
+ key = ActiveSupport::XmlMini.rename_key(attribute.name, options)
117
+ ActiveSupport::XmlMini.to_tag(key, attribute.value,
118
+ options.merge(attribute.decorations))
179
119
  end
120
+ end
180
121
 
181
- def add_procs
182
- if procs = options.delete(:procs)
183
- [ *procs ].each do |proc|
184
- if proc.arity > 1
185
- proc.call(options, @serializable)
186
- else
187
- proc.call(options)
188
- end
122
+ def add_procs
123
+ if procs = options.delete(:procs)
124
+ Array.wrap(procs).each do |proc|
125
+ if proc.arity == 1
126
+ proc.call(options)
127
+ else
128
+ proc.call(options, @serializable)
189
129
  end
190
130
  end
191
131
  end
132
+ end
192
133
  end
193
134
 
135
+ # Returns XML representing the model. Configuration can be
136
+ # passed through +options+.
194
137
  def to_xml(options = {}, &block)
195
138
  Serializer.new(self, options).serialize(&block)
196
139
  end