activemodel 3.0.0.beta4 → 3.0.pre

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