has_state_machine 1.0.0 → 1.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7aad95d7b489183fa7a7c49af178871cb5cc169dbc3a9ae6bd4a93f7ce48ebea
4
- data.tar.gz: 180dbc281d868326a908955013b17fcc935e54de43dbb367212f017e642b994d
3
+ metadata.gz: 4d16c6b958f2e470af9f3120259e802badad7fcd6b8a4ea1d004c84f64e8d09d
4
+ data.tar.gz: 2f9933db38477a1b1020303ab9848aa122fe7e699d153315b830144ffdbfd1a6
5
5
  SHA512:
6
- metadata.gz: 1eca09471018292e2cc4f41a6a53ec2cf12bc087de32af80595901099a4b589131b529bbf53891de89eacc86c5a9a3c16ca7102f31faec63443003c697c45a7e
7
- data.tar.gz: 1b5a1e0a93e9f0648deac92b4fcba653ffeec037e9b3959092e71b5f9b391c93118e352ced5e7c1f44f385ceed1130ab62f1fde11dc4282ea2fb17097e66d47a
6
+ metadata.gz: 35f89aad901be7f976ee42573449c0d7412a633285e41f869f50d7be3215da2752470cf5c4b1299d483605f90b8925f5baa5065729b06f2121678518beeafdb4
7
+ data.tar.gz: cb4e1a772fa2aa9828adfd3ab6bb548e5f489f403f4574cc464e9fa77f4633b886de5c486e4bb9b014eeb71c8ede380fab19320f8b85023e9643b38b3b9866c8
data/README.md CHANGED
@@ -96,6 +96,13 @@ module Workflow
96
96
  # after_transition callbacks as well.
97
97
  Rails.logger.info "== Transitioned from #{previous_state} ==\n"
98
98
  end
99
+
100
+ # after_transition_commit runs only once the transition has been
101
+ # committed: after the record is saved for normal transitions, and
102
+ # outside the transaction for transactional transitions (see below).
103
+ after_transition_commit do
104
+ MyJob.perform_later(object)
105
+ end
99
106
  end
100
107
  end
101
108
  ```
@@ -206,10 +213,20 @@ module Workflow
206
213
  rollback_transition unless notified_watchers?
207
214
  end
208
215
 
216
+ after_transition_commit do
217
+ enqueue_external_work
218
+ end
219
+
209
220
  private
210
221
 
222
+ def enqueue_external_work
223
+ # Any work you want to happen only after the transition is committed.
224
+ # Enqueuing a job, calling an external API, sending a webhook, etc.
225
+ end
226
+
211
227
  def notified_watchers?
212
- #...
228
+ # Any dependent work that you want to run that should play a part in determining
229
+ # whether the transition was successful or not and needs to be rolled back.
213
230
  end
214
231
  end
215
232
  end
@@ -15,6 +15,11 @@ module HasStateMachine
15
15
  # for use on a HasStateMachine::State instance.
16
16
  define_model_callbacks :transition, only: %i[before after]
17
17
 
18
+ ##
19
+ # Defines the after_transition_commit callback. It runs only after a
20
+ # transition has committed.
21
+ define_model_callbacks :transition_commit, only: %i[after]
22
+
18
23
  ##
19
24
  # possible_transitions - Retrieves the next available transitions for a given state.
20
25
  # transactional? - Determines whether or not the transition should happen with a transactional block.
@@ -80,25 +85,34 @@ module HasStateMachine
80
85
 
81
86
  ##
82
87
  # Makes the actual transition from one state to the next and
83
- # runs the before and after transition callbacks.
88
+ # runs the before and after transition callbacks. The
89
+ # after_transition_commit callbacks run after the update completes
90
+ # and only when it succeeds.
84
91
  def perform_transition!
85
- run_callbacks :transition do
86
- object.update("#{object.state_attribute}": state)
92
+ run_callbacks :transition_commit do
93
+ run_callbacks :transition do
94
+ object.update("#{object.state_attribute}": state)
95
+ end
87
96
  end
88
97
  end
89
98
 
90
99
  ##
91
100
  # Makes the actual transition from one state to the next and
92
101
  # runs the before and after transition callbacks in a transaction
93
- # to allow for roll backs.
102
+ # to allow for roll backs. The after_transition_commit callbacks run
103
+ # outside the transaction and only when it commits (not on rollback).
94
104
  def perform_transactional_transition!
95
- ActiveRecord::Base.transaction(requires_new: true, joinable: false) do
96
- run_callbacks :transition do
97
- rollback_transition unless object.update("#{object.state_attribute}": state)
105
+ run_callbacks :transition_commit do
106
+ ActiveRecord::Base.transaction(requires_new: true, joinable: false) do
107
+ run_callbacks :transition do
108
+ rollback_transition unless object.update("#{object.state_attribute}": state)
109
+ end
98
110
  end
99
- end
100
111
 
101
- object.reload.public_send(object.state_attribute) == state
112
+ @previous_state = previous_state
113
+
114
+ object.reload.public_send(object.state_attribute) == state
115
+ end
102
116
  end
103
117
 
104
118
  private
@@ -112,7 +126,7 @@ module HasStateMachine
112
126
  # it has been transitioned to the new state. Useful in
113
127
  # after_transition blocks
114
128
  def previous_state
115
- object.previous_changes[object.state_attribute]&.first
129
+ @previous_state.presence || object.previous_changes[object.state_attribute]&.first
116
130
  end
117
131
 
118
132
  def state_instance(desired_state, transient_values)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HasStateMachine
4
- VERSION = "1.0.0"
4
+ VERSION = "1.2.0"
5
5
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_lsp/addon"
4
+
5
+ require "has_state_machine/version"
6
+ require_relative "definition"
7
+
8
+ ::RubyLsp::Addon.depend_on_ruby_lsp!(">= 0.18.0", "< 1.0") if ::RubyLsp::Addon.respond_to?(:depend_on_ruby_lsp!)
9
+
10
+ module RubyLsp
11
+ module HasStateMachine
12
+ class Addon < ::RubyLsp::Addon
13
+ def activate(global_state, outgoing_queue)
14
+ @global_state = global_state
15
+ @rails_client = register_rails_server_addon(outgoing_queue)
16
+ end
17
+
18
+ def deactivate
19
+ @global_state = nil
20
+ @rails_client = nil
21
+ end
22
+
23
+ def name
24
+ "Has State Machine"
25
+ end
26
+
27
+ def version
28
+ ::HasStateMachine::VERSION
29
+ end
30
+
31
+ def create_definition_listener(response_builder, uri, node_context, dispatcher)
32
+ Definition.new(
33
+ response_builder,
34
+ uri,
35
+ node_context,
36
+ dispatcher,
37
+ index: @global_state&.index,
38
+ rails_client: @rails_client
39
+ )
40
+ end
41
+
42
+ private
43
+
44
+ def register_rails_server_addon(outgoing_queue)
45
+ register_rails_runner_client
46
+ rescue => error
47
+ handle_rails_registration_error(outgoing_queue, error)
48
+ end
49
+
50
+ def register_rails_runner_client
51
+ rails_addon = ::RubyLsp::Addon.get("Ruby LSP Rails", ">= 0") if ::RubyLsp::Addon.respond_to?(:get)
52
+ return unless rails_addon&.respond_to?(:rails_runner_client)
53
+
54
+ client = rails_addon.rails_runner_client
55
+ return unless client.respond_to?(:register_server_addon)
56
+
57
+ client.register_server_addon(File.expand_path("rails_server_addon.rb", __dir__))
58
+ client
59
+ end
60
+
61
+ def handle_rails_registration_error(outgoing_queue, error)
62
+ unless addon_not_found?(error)
63
+ log(outgoing_queue, "Has State Machine Ruby LSP Rails integration unavailable: #{error.message}")
64
+ end
65
+
66
+ nil
67
+ end
68
+
69
+ def addon_not_found?(error)
70
+ if defined?(::RubyLsp::Addon::AddonNotFoundError)
71
+ return true if error.is_a?(::RubyLsp::Addon::AddonNotFoundError)
72
+ end
73
+
74
+ error.class.name&.end_with?("AddonNotFoundError")
75
+ end
76
+
77
+ def log(outgoing_queue, message)
78
+ return if outgoing_queue.nil? || outgoing_queue.closed?
79
+ return unless defined?(::RubyLsp::Notification)
80
+
81
+ outgoing_queue << ::RubyLsp::Notification.window_log_message(message)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "workflow_resolver"
4
+
5
+ module RubyLsp
6
+ module HasStateMachine
7
+ class Definition
8
+ SERVER_ADDON_NAME = "has_state_machine"
9
+
10
+ def initialize(response_builder, _uri, node_context, dispatcher, index: nil, rails_client: nil)
11
+ @response_builder = response_builder
12
+ @node_context = node_context
13
+ @index = index
14
+ @rails_client = rails_client
15
+
16
+ dispatcher.register(self, :on_call_node_enter)
17
+ end
18
+
19
+ def on_call_node_enter(node)
20
+ return unless current_target?(node)
21
+ return unless resolved_model_name
22
+
23
+ if object_call?(node)
24
+ push_entries(constant_entries(resolved_model_name))
25
+ elsif object_method_call?(node)
26
+ method_name = message(node)
27
+ entries = method_entries(resolved_model_name, method_name)
28
+ entries = association_entries(resolved_model_name, method_name) if entries.empty?
29
+
30
+ push_entries(entries)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :index, :node_context, :rails_client, :response_builder
37
+
38
+ def current_target?(node)
39
+ target = node_context.node if node_context.respond_to?(:node)
40
+ call_node = node_context.call_node if node_context.respond_to?(:call_node)
41
+
42
+ node.equal?(target) || node.equal?(call_node)
43
+ end
44
+
45
+ def resolved_model_name
46
+ return @resolved_model_name if defined?(@resolved_model_name)
47
+ return @resolved_model_name = nil unless current_class_name
48
+
49
+ @resolved_model_name = model_name_from_rails(workflow_namespace) || convention_model_name
50
+ end
51
+
52
+ def convention_model_name
53
+ return @convention_model_name if defined?(@convention_model_name)
54
+
55
+ @convention_model_name = current_class_name && WorkflowResolver.model_name_for(current_class_name)
56
+ end
57
+
58
+ def workflow_namespace
59
+ return @workflow_namespace if defined?(@workflow_namespace)
60
+
61
+ @workflow_namespace = current_class_name && WorkflowResolver.workflow_namespace_for(current_class_name)
62
+ end
63
+
64
+ def current_class_name
65
+ return @current_class_name if defined?(@current_class_name)
66
+ return @current_class_name = nil unless node_context.respond_to?(:nesting)
67
+
68
+ @current_class_name = class_name_from_nesting(node_context.nesting)
69
+ end
70
+
71
+ def class_name_from_nesting(nesting)
72
+ parts = nesting.map(&:to_s).reject(&:empty?)
73
+ return if parts.empty?
74
+
75
+ # Nesting entries may already contain "::" (e.g. "Post::Draft").
76
+ parts.join("::").gsub(/:{3,}/, "::")
77
+ end
78
+
79
+ def model_name_from_rails(workflow_namespace)
80
+ return unless workflow_namespace && rails_client
81
+
82
+ result = rails_client.delegate_request(
83
+ server_addon_name: SERVER_ADDON_NAME,
84
+ request_name: "model_for_workflow_namespace",
85
+ workflow_namespace: workflow_namespace
86
+ )
87
+
88
+ response_name(result)
89
+ rescue
90
+ nil
91
+ end
92
+
93
+ def association_entries(model_name, association_name)
94
+ association_model_name = association_model_name(model_name, association_name)
95
+ return [] unless association_model_name
96
+
97
+ constant_entries(association_model_name)
98
+ end
99
+
100
+ def association_model_name(model_name, association_name)
101
+ return unless rails_client&.respond_to?(:association_target)
102
+
103
+ result = rails_client.association_target(model_name: model_name, association_name: association_name)
104
+ response_name(result)
105
+ rescue
106
+ nil
107
+ end
108
+
109
+ def response_name(result)
110
+ return unless result
111
+
112
+ result[:name] || result["name"]
113
+ end
114
+
115
+ def constant_entries(name)
116
+ return [] unless name && index
117
+
118
+ Array(index[name])
119
+ end
120
+
121
+ def method_entries(model_name, method_name)
122
+ return [] unless index && method_name
123
+
124
+ if index.respond_to?(:resolve_method)
125
+ entries = index.resolve_method(method_name, model_name)
126
+ return Array(entries)
127
+ end
128
+
129
+ Array(index[method_name]).select { |entry| entry_owner_name(entry) == model_name }
130
+ end
131
+
132
+ def entry_owner_name(entry)
133
+ owner = entry.owner if entry.respond_to?(:owner)
134
+ owner.name if owner.respond_to?(:name)
135
+ end
136
+
137
+ def push_entries(entries)
138
+ entries.each do |entry|
139
+ response_builder << location_for(entry)
140
+ end
141
+ end
142
+
143
+ def location_for(entry)
144
+ return entry unless defined?(::RubyLsp::Interface::Location)
145
+
146
+ location = entry_location(entry)
147
+ return entry unless location
148
+
149
+ ::RubyLsp::Interface::Location.new(
150
+ uri: entry.uri.to_s,
151
+ range: range_for(location)
152
+ )
153
+ end
154
+
155
+ def entry_location(entry)
156
+ return entry.location if entry.respond_to?(:location)
157
+ entry.name_location if entry.respond_to?(:name_location)
158
+ end
159
+
160
+ def range_for(location)
161
+ ::RubyLsp::Interface::Range.new(
162
+ start: ::RubyLsp::Interface::Position.new(
163
+ line: location.start_line - 1,
164
+ character: location.start_column
165
+ ),
166
+ end: ::RubyLsp::Interface::Position.new(
167
+ line: location.end_line - 1,
168
+ character: location.end_column
169
+ )
170
+ )
171
+ end
172
+
173
+ def object_method_call?(node)
174
+ message(node) && object_call?(receiver(node))
175
+ end
176
+
177
+ def object_call?(node)
178
+ node && receiver(node).nil? && message(node) == "object"
179
+ end
180
+
181
+ def receiver(node)
182
+ node.receiver if node.respond_to?(:receiver)
183
+ end
184
+
185
+ def message(node)
186
+ node.message.to_s if node.respond_to?(:message) && node.message
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_lsp/ruby_lsp_rails/server"
4
+
5
+ module RubyLsp
6
+ module HasStateMachine
7
+ class RailsServerAddon < ::RubyLsp::Rails::ServerAddon
8
+ def name
9
+ "has_state_machine"
10
+ end
11
+
12
+ def execute(request, params)
13
+ with_request_error_handling(request) do
14
+ case request
15
+ when "model_for_workflow_namespace"
16
+ send_result(model_for_workflow_namespace(params.fetch("workflow_namespace")))
17
+ else
18
+ raise NotImplementedError, "Unknown request: #{request}"
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def model_for_workflow_namespace(workflow_namespace)
26
+ model = models_by_workflow_namespace[workflow_namespace]
27
+ return unless model
28
+
29
+ {name: model.name}
30
+ end
31
+
32
+ def models_by_workflow_namespace
33
+ @models_by_workflow_namespace ||= active_record_models.each_with_object({}) do |model, index|
34
+ next unless model.respond_to?(:workflow_namespace)
35
+
36
+ index[model.workflow_namespace] = model
37
+ end
38
+ end
39
+
40
+ def active_record_models
41
+ @active_record_models ||= begin
42
+ ::Rails.application&.eager_load!
43
+ ::ActiveRecord::Base.descendants.reject(&:abstract_class?)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module HasStateMachine
5
+ module WorkflowResolver
6
+ WORKFLOW_PREFIX = "Workflow::"
7
+
8
+ module_function
9
+
10
+ def model_name_for(workflow_class_name)
11
+ namespace = workflow_namespace_for(workflow_class_name)
12
+ return unless namespace&.start_with?(WORKFLOW_PREFIX)
13
+
14
+ name = namespace.delete_prefix(WORKFLOW_PREFIX)
15
+ return if name.empty?
16
+
17
+ name
18
+ end
19
+
20
+ def workflow_namespace_for(workflow_class_name)
21
+ namespace = workflow_class_name.to_s.delete_prefix("::").sub(/::[^:]+\z/, "")
22
+ return if namespace.empty?
23
+
24
+ namespace
25
+ end
26
+ end
27
+ end
28
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: has_state_machine
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Hargett
@@ -31,6 +31,9 @@ dependencies:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '1.5'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '2.0'
34
37
  type: :development
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
@@ -38,6 +41,23 @@ dependencies:
38
41
  - - ">="
39
42
  - !ruby/object:Gem::Version
40
43
  version: '1.5'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: nokogiri
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "<"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.19'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.19'
41
61
  - !ruby/object:Gem::Dependency
42
62
  name: pry
43
63
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +100,20 @@ dependencies:
80
100
  - - ">="
81
101
  - !ruby/object:Gem::Version
82
102
  version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: parallel
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "<"
108
+ - !ruby/object:Gem::Version
109
+ version: '2.0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "<"
115
+ - !ruby/object:Gem::Version
116
+ version: '2.0'
83
117
  - !ruby/object:Gem::Dependency
84
118
  name: appraisal
85
119
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +128,104 @@ dependencies:
94
128
  - - ">="
95
129
  - !ruby/object:Gem::Version
96
130
  version: '0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: minitest
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '5.1'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '5.1'
145
+ - !ruby/object:Gem::Dependency
146
+ name: ruby-lsp
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0.18'
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0.18'
159
+ - !ruby/object:Gem::Dependency
160
+ name: ruby-lsp-rails
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: 0.3.17
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: 0.3.17
173
+ - !ruby/object:Gem::Dependency
174
+ name: base64
175
+ requirement: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ type: :development
181
+ prerelease: false
182
+ version_requirements: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ - !ruby/object:Gem::Dependency
188
+ name: bigdecimal
189
+ requirement: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ type: :development
195
+ prerelease: false
196
+ version_requirements: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ - !ruby/object:Gem::Dependency
202
+ name: drb
203
+ requirement: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '0'
208
+ type: :development
209
+ prerelease: false
210
+ version_requirements: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ - !ruby/object:Gem::Dependency
216
+ name: mutex_m
217
+ requirement: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - ">="
220
+ - !ruby/object:Gem::Version
221
+ version: '0'
222
+ type: :development
223
+ prerelease: false
224
+ version_requirements: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ">="
227
+ - !ruby/object:Gem::Version
228
+ version: '0'
97
229
  description: HasStateMachine uses ruby classes to make creating a finite state machine
98
230
  in your ActiveRecord models a breeze.
99
231
  email:
@@ -114,6 +246,10 @@ files:
114
246
  - lib/has_state_machine/state.rb
115
247
  - lib/has_state_machine/state_helpers.rb
116
248
  - lib/has_state_machine/version.rb
249
+ - lib/ruby_lsp/has_state_machine/addon.rb
250
+ - lib/ruby_lsp/has_state_machine/definition.rb
251
+ - lib/ruby_lsp/has_state_machine/rails_server_addon.rb
252
+ - lib/ruby_lsp/has_state_machine/workflow_resolver.rb
117
253
  homepage: https://www.github.com/encampment/has_state_machine
118
254
  licenses:
119
255
  - MIT