activemodel 3.0.0.beta4 → 3.0.pre

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 (39) hide show
  1. data/CHANGELOG +1 -39
  2. data/MIT-LICENSE +1 -1
  3. data/README +16 -200
  4. data/lib/active_model.rb +19 -28
  5. data/lib/active_model/attribute_methods.rb +27 -142
  6. data/lib/active_model/conversion.rb +1 -37
  7. data/lib/active_model/dirty.rb +12 -51
  8. data/lib/active_model/errors.rb +22 -146
  9. data/lib/active_model/lint.rb +14 -48
  10. data/lib/active_model/locale/en.yml +23 -26
  11. data/lib/active_model/naming.rb +5 -41
  12. data/lib/active_model/observing.rb +16 -35
  13. data/lib/active_model/serialization.rb +0 -57
  14. data/lib/active_model/serializers/json.rb +8 -13
  15. data/lib/active_model/serializers/xml.rb +123 -63
  16. data/lib/active_model/state_machine.rb +70 -0
  17. data/lib/active_model/state_machine/event.rb +62 -0
  18. data/lib/active_model/state_machine/machine.rb +75 -0
  19. data/lib/active_model/state_machine/state.rb +47 -0
  20. data/lib/active_model/state_machine/state_transition.rb +40 -0
  21. data/lib/active_model/test_case.rb +2 -0
  22. data/lib/active_model/validations.rb +62 -125
  23. data/lib/active_model/validations/acceptance.rb +18 -23
  24. data/lib/active_model/validations/confirmation.rb +10 -14
  25. data/lib/active_model/validations/exclusion.rb +13 -15
  26. data/lib/active_model/validations/format.rb +24 -26
  27. data/lib/active_model/validations/inclusion.rb +13 -15
  28. data/lib/active_model/validations/length.rb +65 -61
  29. data/lib/active_model/validations/numericality.rb +58 -76
  30. data/lib/active_model/validations/presence.rb +8 -8
  31. data/lib/active_model/validations/with.rb +22 -90
  32. data/lib/active_model/validations_repair_helper.rb +35 -0
  33. data/lib/active_model/version.rb +2 -3
  34. metadata +19 -63
  35. data/lib/active_model/callbacks.rb +0 -134
  36. data/lib/active_model/railtie.rb +0 -2
  37. data/lib/active_model/translation.rb +0 -60
  38. data/lib/active_model/validations/validates.rb +0 -108
  39. data/lib/active_model/validator.rb +0 -183
@@ -0,0 +1,70 @@
1
+ module ActiveModel
2
+ module StateMachine
3
+ autoload :Event, 'active_model/state_machine/event'
4
+ autoload :Machine, 'active_model/state_machine/machine'
5
+ autoload :State, 'active_model/state_machine/state'
6
+ autoload :StateTransition, 'active_model/state_machine/state_transition'
7
+
8
+ extend ActiveSupport::Concern
9
+
10
+ class InvalidTransition < Exception
11
+ end
12
+
13
+ module ClassMethods
14
+ def inherited(klass)
15
+ super
16
+ klass.state_machines = state_machines
17
+ end
18
+
19
+ def state_machines
20
+ @state_machines ||= {}
21
+ end
22
+
23
+ def state_machines=(value)
24
+ @state_machines = value ? value.dup : nil
25
+ end
26
+
27
+ def state_machine(name = nil, options = {}, &block)
28
+ if name.is_a?(Hash)
29
+ options = name
30
+ name = nil
31
+ end
32
+ name ||= :default
33
+ state_machines[name] ||= Machine.new(self, name)
34
+ block ? state_machines[name].update(options, &block) : state_machines[name]
35
+ end
36
+
37
+ def define_state_query_method(state_name)
38
+ name = "#{state_name}?"
39
+ undef_method(name) if method_defined?(name)
40
+ class_eval "def #{name}; current_state.to_s == %(#{state_name}) end"
41
+ end
42
+ end
43
+
44
+ def current_state(name = nil, new_state = nil, persist = false)
45
+ sm = self.class.state_machine(name)
46
+ ivar = sm.current_state_variable
47
+ if name && new_state
48
+ if persist && respond_to?(:write_state)
49
+ write_state(sm, new_state)
50
+ end
51
+
52
+ if respond_to?(:write_state_without_persistence)
53
+ write_state_without_persistence(sm, new_state)
54
+ end
55
+
56
+ instance_variable_set(ivar, new_state)
57
+ else
58
+ instance_variable_set(ivar, nil) unless instance_variable_defined?(ivar)
59
+ value = instance_variable_get(ivar)
60
+ return value if value
61
+
62
+ if respond_to?(:read_state)
63
+ value = instance_variable_set(ivar, read_state(sm))
64
+ end
65
+
66
+ value || sm.initial_state
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,62 @@
1
+ module ActiveModel
2
+ module StateMachine
3
+ class Event
4
+ attr_reader :name, :success
5
+
6
+ def initialize(machine, name, options = {}, &block)
7
+ @machine, @name, @transitions = machine, name, []
8
+ if machine
9
+ machine.klass.send(:define_method, "#{name}!") do |*args|
10
+ machine.fire_event(name, self, true, *args)
11
+ end
12
+
13
+ machine.klass.send(:define_method, name.to_s) do |*args|
14
+ machine.fire_event(name, self, false, *args)
15
+ end
16
+ end
17
+ update(options, &block)
18
+ end
19
+
20
+ def fire(obj, to_state = nil, *args)
21
+ transitions = @transitions.select { |t| t.from == obj.current_state(@machine ? @machine.name : nil) }
22
+ raise InvalidTransition if transitions.size == 0
23
+
24
+ next_state = nil
25
+ transitions.each do |transition|
26
+ next if to_state && !Array(transition.to).include?(to_state)
27
+ if transition.perform(obj)
28
+ next_state = to_state || Array(transition.to).first
29
+ transition.execute(obj, *args)
30
+ break
31
+ end
32
+ end
33
+ next_state
34
+ end
35
+
36
+ def transitions_from_state?(state)
37
+ @transitions.any? { |t| t.from? state }
38
+ end
39
+
40
+ def ==(event)
41
+ if event.is_a? Symbol
42
+ name == event
43
+ else
44
+ name == event.name
45
+ end
46
+ end
47
+
48
+ def update(options = {}, &block)
49
+ if options.key?(:success) then @success = options[:success] end
50
+ if block then instance_eval(&block) end
51
+ self
52
+ end
53
+
54
+ private
55
+ def transitions(trans_opts)
56
+ Array(trans_opts[:from]).each do |s|
57
+ @transitions << StateTransition.new(trans_opts.merge({:from => s.to_sym}))
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,75 @@
1
+ module ActiveModel
2
+ module StateMachine
3
+ class Machine
4
+ attr_writer :initial_state
5
+ attr_accessor :states, :events, :state_index
6
+ attr_reader :klass, :name
7
+
8
+ def initialize(klass, name, options = {}, &block)
9
+ @klass, @name, @states, @state_index, @events = klass, name, [], {}, {}
10
+ update(options, &block)
11
+ end
12
+
13
+ def initial_state
14
+ @initial_state ||= (states.first ? states.first.name : nil)
15
+ end
16
+
17
+ def update(options = {}, &block)
18
+ if options.key?(:initial) then @initial_state = options[:initial] end
19
+ if block then instance_eval(&block) end
20
+ self
21
+ end
22
+
23
+ def fire_event(event, record, persist, *args)
24
+ state_index[record.current_state(@name)].call_action(:exit, record)
25
+ if new_state = @events[event].fire(record, *args)
26
+ state_index[new_state].call_action(:enter, record)
27
+
28
+ if record.respond_to?(event_fired_callback)
29
+ record.send(event_fired_callback, record.current_state, new_state)
30
+ end
31
+
32
+ record.current_state(@name, new_state, persist)
33
+ record.send(@events[event].success) if @events[event].success
34
+ true
35
+ else
36
+ if record.respond_to?(event_failed_callback)
37
+ record.send(event_failed_callback, event)
38
+ end
39
+
40
+ false
41
+ end
42
+ end
43
+
44
+ def states_for_select
45
+ states.map { |st| [st.display_name, st.name.to_s] }
46
+ end
47
+
48
+ def events_for(state)
49
+ events = @events.values.select { |event| event.transitions_from_state?(state) }
50
+ events.map! { |event| event.name }
51
+ end
52
+
53
+ def current_state_variable
54
+ "@#{@name}_current_state"
55
+ end
56
+
57
+ private
58
+ def state(name, options = {})
59
+ @states << (state_index[name] ||= State.new(name, :machine => self)).update(options)
60
+ end
61
+
62
+ def event(name, options = {}, &block)
63
+ (@events[name] ||= Event.new(self, name)).update(options, &block)
64
+ end
65
+
66
+ def event_fired_callback
67
+ @event_fired_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_fired'
68
+ end
69
+
70
+ def event_failed_callback
71
+ @event_failed_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_failed'
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,47 @@
1
+ module ActiveModel
2
+ module StateMachine
3
+ class State
4
+ attr_reader :name, :options
5
+
6
+ def initialize(name, options = {})
7
+ @name = name
8
+ if machine = options.delete(:machine)
9
+ machine.klass.define_state_query_method(name)
10
+ end
11
+ update(options)
12
+ end
13
+
14
+ def ==(state)
15
+ if state.is_a? Symbol
16
+ name == state
17
+ else
18
+ name == state.name
19
+ end
20
+ end
21
+
22
+ def call_action(action, record)
23
+ action = @options[action]
24
+ case action
25
+ when Symbol, String
26
+ record.send(action)
27
+ when Proc
28
+ action.call(record)
29
+ end
30
+ end
31
+
32
+ def display_name
33
+ @display_name ||= name.to_s.gsub(/_/, ' ').capitalize
34
+ end
35
+
36
+ def for_select
37
+ [display_name, name.to_s]
38
+ end
39
+
40
+ def update(options = {})
41
+ if options.key?(:display) then @display_name = options.delete(:display) end
42
+ @options = options
43
+ self
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ module ActiveModel
2
+ module StateMachine
3
+ class StateTransition
4
+ attr_reader :from, :to, :options
5
+
6
+ def initialize(opts)
7
+ @from, @to, @guard, @on_transition = opts[:from], opts[:to], opts[:guard], opts[:on_transition]
8
+ @options = opts
9
+ end
10
+
11
+ def perform(obj)
12
+ case @guard
13
+ when Symbol, String
14
+ obj.send(@guard)
15
+ when Proc
16
+ @guard.call(obj)
17
+ else
18
+ true
19
+ end
20
+ end
21
+
22
+ def execute(obj, *args)
23
+ case @on_transition
24
+ when Symbol, String
25
+ obj.send(@on_transition, *args)
26
+ when Proc
27
+ @on_transition.call(obj, *args)
28
+ end
29
+ end
30
+
31
+ def ==(obj)
32
+ @from == obj.from && @to == obj.to
33
+ end
34
+
35
+ def from?(value)
36
+ @from == value
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,3 +1,5 @@
1
+ require "active_support/test_case"
2
+
1
3
  module ActiveModel #:nodoc:
2
4
  class TestCase < ActiveSupport::TestCase #:nodoc:
3
5
  def with_kcode(kcode)
@@ -1,103 +1,23 @@
1
1
  require 'active_support/core_ext/array/extract_options'
2
- require 'active_support/core_ext/array/wrap'
3
- require 'active_support/core_ext/class/attribute'
4
2
  require 'active_support/core_ext/hash/keys'
5
- require 'active_model/errors'
6
3
 
7
4
  module ActiveModel
8
-
9
- # Provides a full validation framework to your objects.
10
- #
11
- # A minimal implementation could be:
12
- #
13
- # class Person
14
- # include ActiveModel::Validations
15
- #
16
- # attr_accessor :first_name, :last_name
17
- #
18
- # validates_each :first_name, :last_name do |record, attr, value|
19
- # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
20
- # end
21
- # end
22
- #
23
- # Which provides you with the full standard validation stack that you
24
- # know from ActiveRecord.
25
- #
26
- # person = Person.new
27
- # person.valid?
28
- # #=> true
29
- # person.invalid?
30
- # #=> false
31
- # person.first_name = 'zoolander'
32
- # person.valid?
33
- # #=> false
34
- # person.invalid?
35
- # #=> true
36
- # person.errors
37
- # #=> #<OrderedHash {:first_name=>["starts with z."]}>
38
- #
39
- # Note that ActiveModel::Validations automatically adds an +errors+ method
40
- # to your instances initialized with a new ActiveModel::Errors object, so
41
- # there is no need for you to add this manually.
42
- #
43
5
  module Validations
44
6
  extend ActiveSupport::Concern
45
7
  include ActiveSupport::Callbacks
46
8
 
47
9
  included do
48
- extend ActiveModel::Translation
49
-
50
- extend HelperMethods
51
- include HelperMethods
52
-
53
10
  define_callbacks :validate, :scope => :name
54
-
55
- attr_accessor :validation_context
56
-
57
- class_attribute :_validators
58
- self._validators = Hash.new { |h,k| h[k] = [] }
59
11
  end
60
12
 
61
13
  module ClassMethods
62
- # Validates each attribute against a block.
63
- #
64
- # class Person
65
- # include ActiveModel::Validations
66
- #
67
- # attr_accessor :first_name, :last_name
68
- #
69
- # validates_each :first_name, :last_name do |record, attr, value|
70
- # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
71
- # end
72
- # end
73
- #
74
- # Options:
75
- # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>,
76
- # other options <tt>:create</tt>, <tt>:update</tt>).
77
- # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
78
- # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
79
- # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
80
- # occur (e.g. <tt>:if => :allow_validation</tt>, or
81
- # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
82
- # method, proc or string should return or evaluate to a true or false value.
83
- # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
84
- # not occur (e.g. <tt>:unless => :skip_validation</tt>, or
85
- # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
86
- # method, proc or string should return or evaluate to a true or false value.
87
- def validates_each(*attr_names, &block)
88
- options = attr_names.extract_options!.symbolize_keys
89
- validates_with BlockValidator, options.merge(:attributes => attr_names.flatten), &block
90
- end
91
-
92
14
  # Adds a validation method or block to the class. This is useful when
93
15
  # overriding the +validate+ instance method becomes too unwieldly and
94
16
  # you're looking for more descriptive declaration of your validations.
95
17
  #
96
18
  # This can be done with a symbol pointing to a method:
97
19
  #
98
- # class Comment
99
- # include ActiveModel::Validations
100
- #
20
+ # class Comment < ActiveRecord::Base
101
21
  # validate :must_be_friends
102
22
  #
103
23
  # def must_be_friends
@@ -107,9 +27,7 @@ module ActiveModel
107
27
  #
108
28
  # Or with a block which is passed the current record to be validated:
109
29
  #
110
- # class Comment
111
- # include ActiveModel::Validations
112
- #
30
+ # class Comment < ActiveRecord::Base
113
31
  # validate do |comment|
114
32
  # comment.must_be_friends
115
33
  # end
@@ -119,29 +37,48 @@ module ActiveModel
119
37
  # end
120
38
  # end
121
39
  #
40
+ # This usage applies to +validate_on_create+ and +validate_on_update as well+.
41
+
42
+ # Validates each attribute against a block.
43
+ #
44
+ # class Person < ActiveRecord::Base
45
+ # validates_each :first_name, :last_name do |record, attr, value|
46
+ # record.errors.add attr, 'starts with z.' if value[0] == ?z
47
+ # end
48
+ # end
49
+ #
50
+ # Options:
51
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
52
+ # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
53
+ # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
54
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
55
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
56
+ # method, proc or string should return or evaluate to a true or false value.
57
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
58
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
59
+ # method, proc or string should return or evaluate to a true or false value.
60
+ def validates_each(*attrs)
61
+ options = attrs.extract_options!.symbolize_keys
62
+ attrs = attrs.flatten
63
+
64
+ # Declare the validation.
65
+ validate options do |record|
66
+ attrs.each do |attr|
67
+ value = record.send(:read_attribute_for_validation, attr)
68
+ next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
69
+ yield record, attr, value
70
+ end
71
+ end
72
+ end
73
+
122
74
  def validate(*args, &block)
123
75
  options = args.last
124
76
  if options.is_a?(Hash) && options.key?(:on)
125
- options[:if] = Array.wrap(options[:if])
126
- options[:if] << "validation_context == :#{options[:on]}"
77
+ options[:if] = Array(options[:if])
78
+ options[:if] << "@_on_validate == :#{options[:on]}"
127
79
  end
128
80
  set_callback(:validate, *args, &block)
129
81
  end
130
-
131
- # List all validators that being used to validate the model using +validates_with+
132
- # method.
133
- def validators
134
- _validators.values.flatten.uniq
135
- end
136
-
137
- # List all validators that being used to validate a specific attribute.
138
- def validators_on(attribute)
139
- _validators[attribute.to_sym]
140
- end
141
-
142
- def attribute_method?(attribute)
143
- method_defined?(attribute)
144
- end
145
82
  end
146
83
 
147
84
  # Returns the Errors object that holds all information about attribute error messages.
@@ -150,38 +87,38 @@ module ActiveModel
150
87
  end
151
88
 
152
89
  # Runs all the specified validations and returns true if no errors were added otherwise false.
153
- # Context can optionally be supplied to define which callbacks to test against (the context is
154
- # defined on the validations using :on).
155
- def valid?(context = nil)
156
- current_context, self.validation_context = validation_context, context
90
+ def valid?
157
91
  errors.clear
158
92
  _run_validate_callbacks
159
93
  errors.empty?
160
- ensure
161
- self.validation_context = current_context
162
94
  end
163
95
 
164
96
  # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise.
165
- def invalid?(context = nil)
166
- !valid?(context)
97
+ def invalid?
98
+ !valid?
167
99
  end
168
100
 
169
- # Hook method defining how an attribute value should be retieved. By default this is assumed
170
- # to be an instance named after the attribute. Override this method in subclasses should you
171
- # need to retrieve the value for a given attribute differently e.g.
172
- # class MyClass
173
- # include ActiveModel::Validations
174
- #
175
- # def initialize(data = {})
176
- # @data = data
177
- # end
178
- #
179
- # def read_attribute_for_validation(key)
180
- # @data[key]
181
- # end
182
- # end
183
- #
184
- alias :read_attribute_for_validation :send
101
+ protected
102
+ # Hook method defining how an attribute value should be retieved. By default this is assumed
103
+ # to be an instance named after the attribute. Override this method in subclasses should you
104
+ # need to retrieve the value for a given attribute differently e.g.
105
+ # class MyClass
106
+ # include ActiveModel::Validations
107
+ #
108
+ # def initialize(data = {})
109
+ # @data = data
110
+ # end
111
+ #
112
+ # private
113
+ #
114
+ # def read_attribute_for_validation(key)
115
+ # @data[key]
116
+ # end
117
+ # end
118
+ #
119
+ def read_attribute_for_validation(key)
120
+ send(key)
121
+ end
185
122
  end
186
123
  end
187
124