aasm 4.12.3 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +14 -12
  3. data/Appraisals +39 -16
  4. data/CHANGELOG.md +16 -3
  5. data/Dockerfile +44 -0
  6. data/Gemfile +1 -2
  7. data/README.md +41 -11
  8. data/docker-compose.yml +40 -0
  9. data/gemfiles/rails_3.2.gemfile +6 -6
  10. data/gemfiles/rails_4.0.gemfile +7 -7
  11. data/gemfiles/rails_4.2.gemfile +8 -8
  12. data/gemfiles/rails_4.2_mongoid_5.gemfile +5 -5
  13. data/gemfiles/rails_4.2_nobrainer.gemfile +9 -0
  14. data/gemfiles/rails_5.0.gemfile +5 -6
  15. data/gemfiles/rails_5.0_nobrainer.gemfile +9 -0
  16. data/gemfiles/rails_5.1.gemfile +13 -0
  17. data/lib/aasm.rb +5 -0
  18. data/lib/aasm/aasm.rb +1 -0
  19. data/lib/aasm/core/event.rb +6 -21
  20. data/lib/aasm/core/invoker.rb +129 -0
  21. data/lib/aasm/core/invokers/base_invoker.rb +75 -0
  22. data/lib/aasm/core/invokers/class_invoker.rb +52 -0
  23. data/lib/aasm/core/invokers/literal_invoker.rb +47 -0
  24. data/lib/aasm/core/invokers/proc_invoker.rb +59 -0
  25. data/lib/aasm/core/state.rb +10 -9
  26. data/lib/aasm/core/transition.rb +7 -68
  27. data/lib/aasm/errors.rb +2 -2
  28. data/lib/aasm/persistence.rb +3 -0
  29. data/lib/aasm/persistence/active_record_persistence.rb +5 -4
  30. data/lib/aasm/persistence/no_brainer_persistence.rb +105 -0
  31. data/lib/aasm/persistence/plain_persistence.rb +2 -1
  32. data/lib/aasm/persistence/sequel_persistence.rb +0 -1
  33. data/lib/aasm/rspec/allow_event.rb +5 -1
  34. data/lib/aasm/rspec/allow_transition_to.rb +5 -1
  35. data/lib/aasm/version.rb +1 -1
  36. data/lib/generators/nobrainer/aasm_generator.rb +28 -0
  37. data/lib/motion-aasm.rb +1 -0
  38. data/spec/database.rb +3 -0
  39. data/spec/generators/no_brainer_generator_spec.rb +29 -0
  40. data/spec/models/active_record/simple_new_dsl.rb +15 -0
  41. data/spec/models/nobrainer/complex_no_brainer_example.rb +36 -0
  42. data/spec/models/nobrainer/invalid_persistor_no_brainer.rb +39 -0
  43. data/spec/models/nobrainer/no_scope_no_brainer.rb +21 -0
  44. data/spec/models/nobrainer/nobrainer_relationships.rb +25 -0
  45. data/spec/models/nobrainer/silent_persistor_no_brainer.rb +39 -0
  46. data/spec/models/nobrainer/simple_new_dsl_nobrainer.rb +25 -0
  47. data/spec/models/nobrainer/simple_no_brainer.rb +23 -0
  48. data/spec/models/nobrainer/validator_no_brainer.rb +98 -0
  49. data/spec/models/simple_example.rb +2 -0
  50. data/spec/models/simple_example_with_guard_args.rb +17 -0
  51. data/spec/spec_helpers/active_record.rb +2 -1
  52. data/spec/spec_helpers/dynamoid.rb +7 -5
  53. data/spec/spec_helpers/mongoid.rb +20 -1
  54. data/spec/spec_helpers/nobrainer.rb +15 -0
  55. data/spec/spec_helpers/redis.rb +5 -2
  56. data/spec/spec_helpers/sequel.rb +1 -1
  57. data/spec/unit/callback_multiple_spec.rb +3 -3
  58. data/spec/unit/callbacks_spec.rb +2 -2
  59. data/spec/unit/exception_spec.rb +1 -1
  60. data/spec/unit/invoker_spec.rb +189 -0
  61. data/spec/unit/invokers/base_invoker_spec.rb +72 -0
  62. data/spec/unit/invokers/class_invoker_spec.rb +95 -0
  63. data/spec/unit/invokers/literal_invoker_spec.rb +86 -0
  64. data/spec/unit/invokers/proc_invoker_spec.rb +86 -0
  65. data/spec/unit/persistence/active_record_persistence_spec.rb +16 -0
  66. data/spec/unit/persistence/mongoid_persistence_multiple_spec.rb +0 -4
  67. data/spec/unit/persistence/mongoid_persistence_spec.rb +0 -4
  68. data/spec/unit/persistence/no_brainer_persistence_multiple_spec.rb +198 -0
  69. data/spec/unit/persistence/no_brainer_persistence_spec.rb +158 -0
  70. data/spec/unit/rspec_matcher_spec.rb +6 -0
  71. data/spec/unit/state_spec.rb +2 -2
  72. data/spec/unit/transition_spec.rb +1 -1
  73. data/test/minitest_helper.rb +2 -2
  74. data/test/unit/minitest_matcher_test.rb +1 -1
  75. metadata +51 -3
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AASM
4
+ module Core
5
+ module Invokers
6
+ ##
7
+ # Proc invoker which allows to use Procs as
8
+ # state/event/transition callbacks.
9
+ class ProcInvoker < BaseInvoker
10
+ def may_invoke?
11
+ subject.is_a?(Proc)
12
+ end
13
+
14
+ def log_failure
15
+ return log_source_location if Method.method_defined?(:source_location)
16
+ log_proc_info
17
+ end
18
+
19
+ def invoke_subject
20
+ @result = if support_parameters?
21
+ exec_proc(parameters_to_arity)
22
+ else
23
+ exec_proc(subject.arity)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def support_parameters?
30
+ subject.respond_to?(:parameters)
31
+ end
32
+
33
+ # rubocop:disable Metrics/AbcSize
34
+ def exec_proc(parameters_size)
35
+ return record.instance_exec(&subject) if parameters_size.zero?
36
+ return record.instance_exec(*args, &subject) if parameters_size < 0
37
+ record.instance_exec(*args[0..(parameters_size - 1)], &subject)
38
+ end
39
+ # rubocop:enable Metrics/AbcSize
40
+
41
+ def log_source_location
42
+ failures << subject.source_location.join('#')
43
+ end
44
+
45
+ def log_proc_info
46
+ failures << subject
47
+ end
48
+
49
+ def parameters_to_arity
50
+ subject.parameters.inject(0) do |memo, parameter|
51
+ memo += 1
52
+ memo *= -1 if parameter[0] == :rest && memo > 0
53
+ memo
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AASM::Core
2
4
  class State
3
5
  attr_reader :name, :state_machine, :options
@@ -13,7 +15,13 @@ module AASM::Core
13
15
  def initialize_copy(orig)
14
16
  super
15
17
  @options = {}
16
- orig.options.each_pair { |name, setting| @options[name] = setting.is_a?(Hash) || setting.is_a?(Array) ? setting.dup : setting }
18
+ orig.options.each_pair do |name, setting|
19
+ @options[name] = if setting.is_a?(Hash) || setting.is_a?(Array)
20
+ setting.dup
21
+ else
22
+ setting
23
+ end
24
+ end
17
25
  end
18
26
 
19
27
  def ==(state)
@@ -75,14 +83,7 @@ module AASM::Core
75
83
  end
76
84
 
77
85
  def _fire_callbacks(action, record, args)
78
- case action
79
- when Symbol, String
80
- arity = record.__send__(:method, action.to_sym).arity
81
- record.__send__(action, *(arity < 0 ? args : args[0...arity]))
82
- when Proc
83
- arity = action.arity
84
- action.call(record, *(arity < 0 ? args : args[0...arity]))
85
- end
86
+ Invoker.new(action, record, args).invoke
86
87
  end
87
88
 
88
89
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AASM::Core
2
4
  class Transition
3
5
  include DslHelper
@@ -67,77 +69,14 @@ module AASM::Core
67
69
  record.aasm(event.state_machine.name).to_state = @to if record.aasm(event.state_machine.name).respond_to?(:to_state=)
68
70
  end
69
71
 
70
- case code
71
- when Symbol, String
72
- result = (record.__send__(:method, code.to_sym).arity == 0 ? record.__send__(code) : record.__send__(code, *args))
73
- failures << code unless result
74
- result
75
- when Proc
76
- if code.respond_to?(:parameters)
77
- # In Ruby's Proc, the 'arity' method is not a good condidate to know if
78
- # we should pass the arguments or not, since it does return 0 even in
79
- # presence of optional parameters.
80
- result = (code.parameters.size == 0 ? record.instance_exec(&code) : record.instance_exec(*args, &code))
81
-
82
- failures << code.source_location.join('#') unless result
83
- else
84
- # In RubyMotion's Proc, the 'parameter' method does not exists, however its
85
- # 'arity' method works just like the one from Method, only returning 0 when
86
- # there is no parameters whatsoever, optional or not.
87
- result = (code.arity == 0 ? record.instance_exec(&code) : record.instance_exec(*args, &code))
88
-
89
- # Sadly, RubyMotion's Proc does not define the method 'source_location' either.
90
- failures << code unless result
91
- end
92
-
93
- result
94
- when Class
95
- arity = code.instance_method(:initialize).arity
96
- if arity == 0
97
- instance = code.new
98
- elsif arity == 1
99
- instance = code.new(record)
100
- else
101
- instance = code.new(record, *args)
102
- end
103
- result = instance.call
104
-
105
- if Method.method_defined?(:source_location)
106
- failures << instance.method(:call).source_location.join('#') unless result
107
- else
108
- # RubyMotion support ('source_location' not defined for Method)
109
- failures << instance.method(:call) unless result
110
- end
111
-
112
- result
113
- when Array
114
- if options[:guard]
115
- # invoke guard callbacks
116
- code.all? {|a| invoke_callbacks_compatible_with_guard(a, record, args)}
117
- elsif options[:unless]
118
- # invoke unless callbacks
119
- code.all? {|a| !invoke_callbacks_compatible_with_guard(a, record, args)}
120
- else
121
- # invoke after callbacks
122
- code.map {|a| invoke_callbacks_compatible_with_guard(a, record, args)}
123
- end
124
- else
125
- true
126
- end
72
+ Invoker.new(code, record, args)
73
+ .with_options(options)
74
+ .with_failures(failures)
75
+ .invoke
127
76
  end
128
77
 
129
78
  def _fire_callbacks(code, record, args)
130
- case code
131
- when Symbol, String
132
- arity = record.send(:method, code.to_sym).arity
133
- record.send(code, *(arity < 0 ? args : args[0...arity]))
134
- when Proc
135
- code.arity == 0 ? record.instance_exec(&code) : record.instance_exec(*args, &code)
136
- when Array
137
- code.map {|a| _fire_callbacks(a, record, args)}
138
- else
139
- true
140
- end
79
+ Invoker.new(code, record, args).invoke
141
80
  end
142
81
 
143
82
  end
data/lib/aasm/errors.rb CHANGED
@@ -7,11 +7,11 @@ module AASM
7
7
 
8
8
  def initialize(object, event_name, state_machine_name, failures = [])
9
9
  @object, @event_name, @originating_state, @failures = object, event_name, object.aasm(state_machine_name).current_state, failures
10
- super("Event '#{event_name}' cannot transition from '#{originating_state}'. #{reasoning}")
10
+ super("Event '#{event_name}' cannot transition from '#{originating_state}'.#{reasoning}")
11
11
  end
12
12
 
13
13
  def reasoning
14
- "Failed callback(s): #{failures}." unless failures.empty?
14
+ " Failed callback(s): #{failures}." unless failures.empty?
15
15
  end
16
16
  end
17
17
 
@@ -12,6 +12,9 @@ module AASM
12
12
  elsif hierarchy.include?("Mongoid::Document")
13
13
  require_persistence :mongoid
14
14
  include_persistence base, :mongoid
15
+ elsif hierarchy.include?("NoBrainer::Document")
16
+ require_persistence :no_brainer
17
+ include_persistence base, :no_brainer
15
18
  elsif hierarchy.include?("Sequel::Model")
16
19
  require_persistence :sequel
17
20
  include_persistence base, :sequel
@@ -41,14 +41,15 @@ module AASM
41
41
 
42
42
  module ClassMethods
43
43
  def aasm_create_scope(state_machine_name, scope_name)
44
- conditions = {
45
- table_name => { aasm(state_machine_name).attribute_name => scope_name.to_s }
46
- }
47
44
  if ActiveRecord::VERSION::MAJOR >= 3
45
+ conditions = { aasm(state_machine_name).attribute_name => scope_name.to_s }
48
46
  class_eval do
49
- scope scope_name, lambda { where(conditions) }
47
+ scope scope_name, lambda { where(table_name => conditions) }
50
48
  end
51
49
  else
50
+ conditions = {
51
+ table_name => { aasm(state_machine_name).attribute_name => scope_name.to_s }
52
+ }
52
53
  class_eval do
53
54
  named_scope scope_name, :conditions => conditions
54
55
  end
@@ -0,0 +1,105 @@
1
+ require 'aasm/persistence/orm'
2
+ module AASM
3
+ module Persistence
4
+ module NoBrainerPersistence
5
+ # This method:
6
+ #
7
+ # * extends the model with ClassMethods
8
+ # * includes InstanceMethods
9
+ #
10
+ # Adds
11
+ #
12
+ # before_validation :aasm_ensure_initial_state
13
+ #
14
+ # As a result, it doesn't matter when you define your methods - the following 2 are equivalent
15
+ #
16
+ # class Foo
17
+ # include NoBrainer::Document
18
+ # def aasm_write_state(state)
19
+ # "bar"
20
+ # end
21
+ # include AASM
22
+ # end
23
+ #
24
+ # class Foo
25
+ # include NoBrainer::Document
26
+ # include AASM
27
+ # def aasm_write_state(state)
28
+ # "bar"
29
+ # end
30
+ # end
31
+ #
32
+ def self.included(base)
33
+ base.send(:include, AASM::Persistence::Base)
34
+ base.send(:include, AASM::Persistence::ORM)
35
+ base.send(:include, AASM::Persistence::NoBrainerPersistence::InstanceMethods)
36
+ base.extend AASM::Persistence::NoBrainerPersistence::ClassMethods
37
+
38
+ base.after_initialize :aasm_ensure_initial_state
39
+ end
40
+
41
+ module ClassMethods
42
+ def aasm_create_scope(state_machine_name, scope_name)
43
+ scope_options = lambda {
44
+ where(aasm(state_machine_name).attribute_name.to_sym => scope_name.to_s)
45
+ }
46
+ send(:scope, scope_name, scope_options)
47
+ end
48
+ end
49
+
50
+ module InstanceMethods
51
+
52
+ private
53
+
54
+ def aasm_save
55
+ self.save
56
+ end
57
+
58
+ def aasm_raise_invalid_record
59
+ raise NoBrainer::Error::DocumentInvalid.new(self)
60
+ end
61
+
62
+ def aasm_supports_transactions?
63
+ false
64
+ end
65
+
66
+ def aasm_update_column(attribute_name, value)
67
+ write_attribute(attribute_name, value)
68
+ save(validate: false)
69
+
70
+ true
71
+ end
72
+
73
+ def aasm_read_attribute(name)
74
+ read_attribute(name)
75
+ end
76
+
77
+ def aasm_write_attribute(name, value)
78
+ write_attribute(name, value)
79
+ end
80
+
81
+ # Ensures that if the aasm_state column is nil and the record is new
82
+ # that the initial state gets populated before validation on create
83
+ #
84
+ # foo = Foo.new
85
+ # foo.aasm_state # => nil
86
+ # foo.valid?
87
+ # foo.aasm_state # => "open" (where :open is the initial state)
88
+ #
89
+ #
90
+ # foo = Foo.find(:first)
91
+ # foo.aasm_state # => 1
92
+ # foo.aasm_state = nil
93
+ # foo.valid?
94
+ # foo.aasm_state # => nil
95
+ #
96
+ def aasm_ensure_initial_state
97
+ AASM::StateMachineStore.fetch(self.class, true).machine_names.each do |name|
98
+ aasm_column = self.class.aasm(name).attribute_name
99
+ aasm(name).enter_initial_state if read_attribute(aasm_column).blank?
100
+ end
101
+ end
102
+ end # InstanceMethods
103
+ end
104
+ end # Persistence
105
+ end # AASM
@@ -5,7 +5,8 @@ module AASM
5
5
  # may be overwritten by persistence mixins
6
6
  def aasm_read_state(name=:default)
7
7
  # all the following lines behave like @current_state ||= aasm(name).enter_initial_state
8
- current = aasm(name).instance_variable_get("@current_state_#{name}")
8
+ current = aasm(name).instance_variable_defined?("@current_state_#{name}") &&
9
+ aasm(name).instance_variable_get("@current_state_#{name}")
9
10
  return current if current
10
11
  aasm(name).instance_variable_set("@current_state_#{name}", aasm(name).enter_initial_state)
11
12
  end
@@ -15,7 +15,6 @@ module AASM
15
15
  end
16
16
 
17
17
  def before_create
18
- aasm_ensure_initial_state
19
18
  super
20
19
  end
21
20
 
@@ -1,13 +1,17 @@
1
1
  RSpec::Matchers.define :allow_event do |event|
2
2
  match do |obj|
3
3
  @state_machine_name ||= :default
4
- obj.aasm(@state_machine_name).may_fire_event?(event)
4
+ obj.aasm(@state_machine_name).may_fire_event?(event, *@args)
5
5
  end
6
6
 
7
7
  chain :on do |state_machine_name|
8
8
  @state_machine_name = state_machine_name
9
9
  end
10
10
 
11
+ chain :with do |*args|
12
+ @args = args
13
+ end
14
+
11
15
  description do
12
16
  "allow event #{expected} (on :#{@state_machine_name})"
13
17
  end
@@ -1,13 +1,17 @@
1
1
  RSpec::Matchers.define :allow_transition_to do |state|
2
2
  match do |obj|
3
3
  @state_machine_name ||= :default
4
- obj.aasm(@state_machine_name).states(:permitted => true).include?(state)
4
+ obj.aasm(@state_machine_name).states({:permitted => true}, *@args).include?(state)
5
5
  end
6
6
 
7
7
  chain :on do |state_machine_name|
8
8
  @state_machine_name = state_machine_name
9
9
  end
10
10
 
11
+ chain :with do |*args|
12
+ @args = args
13
+ end
14
+
11
15
  description do
12
16
  "allow transition to #{expected} (on :#{@state_machine_name})"
13
17
  end
data/lib/aasm/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module AASM
2
- VERSION = "4.12.3"
2
+ VERSION = "5.0.1"
3
3
  end
@@ -0,0 +1,28 @@
1
+ require 'rails/generators/named_base'
2
+ require 'generators/aasm/orm_helpers'
3
+
4
+ module NoBrainer
5
+ module Generators
6
+ class AASMGenerator < Rails::Generators::NamedBase
7
+ include AASM::Generators::OrmHelpers
8
+ namespace 'nobrainer:aasm'
9
+ argument :column_name, type: :string, default: 'aasm_state'
10
+
11
+ def generate_model
12
+ invoke 'nobrainer:model', [name] unless model_exists?
13
+ end
14
+
15
+ def inject_aasm_content
16
+ inject_into_file model_path, model_contents, after: "include NoBrainer::Document::Timestamps\n" if model_exists?
17
+ end
18
+
19
+ def inject_field_types
20
+ inject_into_file model_path, migration_data, after: "include NoBrainer::Document::Timestamps\n" if model_exists?
21
+ end
22
+
23
+ def migration_data
24
+ " field :#{column_name}"
25
+ end
26
+ end
27
+ end
28
+ end
data/lib/motion-aasm.rb CHANGED
@@ -16,6 +16,7 @@ exclude_files = [
16
16
  'aasm/persistence/active_record_persistence.rb',
17
17
  'aasm/persistence/dynamoid_persistence.rb',
18
18
  'aasm/persistence/mongoid_persistence.rb',
19
+ 'aasm/persistence/no_brainer_persistence.rb',
19
20
  'aasm/persistence/sequel_persistence.rb',
20
21
  'aasm/persistence/redis_persistence.rb'
21
22
  ]
data/spec/database.rb CHANGED
@@ -11,6 +11,9 @@ ActiveRecord::Migration.suppress_messages do
11
11
  ActiveRecord::Migration.create_table "multiple_simple_new_dsls", :force => true do |t|
12
12
  t.string "status"
13
13
  end
14
+ ActiveRecord::Migration.create_table "implemented_abstract_class_dsls", :force => true do |t|
15
+ t.string "status"
16
+ end
14
17
 
15
18
  ActiveRecord::Migration.create_table "complex_active_record_examples", :force => true do |t|
16
19
  t.string "left"