activemodel 3.0.pre → 3.0.0.rc

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