has_state_machine 1.1.0 → 1.2.1
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 +4 -4
- data/README.md +19 -3
- data/Rakefile +2 -2
- data/lib/has_state_machine/core_ext/string.rb +3 -1
- data/lib/has_state_machine/state.rb +46 -25
- data/lib/has_state_machine/version.rb +1 -1
- data/lib/ruby_lsp/has_state_machine/addon.rb +85 -0
- data/lib/ruby_lsp/has_state_machine/definition.rb +208 -0
- data/lib/ruby_lsp/has_state_machine/rails_server_addon.rb +60 -0
- data/lib/ruby_lsp/has_state_machine/workflow_resolver.rb +28 -0
- metadata +152 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d311fd40183ff49f406df6216c0a48f910f05bae0d91dc8a77d1456cfbafadd5
|
|
4
|
+
data.tar.gz: a08e788ca3012cb4a74570d463f0c30344b476732702848184439efaa3a6b79a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a6cd82cab96a81efd97f3495770baa6542c65f23ae602d00362c27b6517779df747b330460f833eb318e313e23e40318ef32c0fe4c9685a3c587df6912a4e6ab
|
|
7
|
+
data.tar.gz: 86620a6b8db88cfd2a267c06197269cea8f7320f2cc0cc70829d04e9d00736b276609af2bf0ba93d376d079aa2e6afcd16bb4bad0059f707a1bd63c7edff0856
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# HasStateMachine
|
|
2
2
|
|
|
3
3
|
[](https://github.com/beehiiv/has_state_machine/actions/workflows/ci.yml)
|
|
4
|
-
[](https://github.com/rubocop/rubocop)
|
|
5
5
|
|
|
6
6
|
HasStateMachine uses ruby classes to make creating a finite state machine for your ActiveRecord models a breeze.
|
|
7
7
|
|
|
@@ -98,8 +98,10 @@ module Workflow
|
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
# after_transition_commit runs only once the transition has been
|
|
101
|
-
# committed
|
|
102
|
-
#
|
|
101
|
+
# committed to the database: after the OUTERMOST database transaction
|
|
102
|
+
# commits, or immediately after the transition when no transaction is
|
|
103
|
+
# open. It never runs if the transition or a surrounding transaction
|
|
104
|
+
# rolls back. (Requires Rails 7.2+; see Advanced Usage below.)
|
|
103
105
|
after_transition_commit do
|
|
104
106
|
MyJob.perform_later(object)
|
|
105
107
|
end
|
|
@@ -232,6 +234,20 @@ module Workflow
|
|
|
232
234
|
end
|
|
233
235
|
```
|
|
234
236
|
|
|
237
|
+
#### `after_transition_commit` Semantics
|
|
238
|
+
|
|
239
|
+
`after_transition_commit` callbacks run after the outermost database transaction commits. In detail:
|
|
240
|
+
|
|
241
|
+
- If no transaction is open when `transition_to` is called, they run immediately after the transition commits.
|
|
242
|
+
- If the transition happens inside an open transaction — an app-level `ActiveRecord::Base.transaction`, or a nested transition triggered from another state's `before_transition`/`after_transition` — they are deferred until that outermost transaction commits.
|
|
243
|
+
- They never run if the transition fails, if `rollback_transition` is raised, or if a surrounding transaction rolls back after the transition. Multiple transitions inside one transaction each fire their callbacks after commit, in transition order.
|
|
244
|
+
|
|
245
|
+
This makes `after_transition_commit` the safe place to enqueue background jobs, call external APIs, etc.: the work only happens once the new state is actually visible to other database connections.
|
|
246
|
+
|
|
247
|
+
**Note on Rails versions:** deferral to an outer transaction requires Rails 7.2+. On older Rails versions the callbacks fire as soon as the transition itself completes, which may be inside an uncommitted outer transaction.
|
|
248
|
+
|
|
249
|
+
**Footgun: `ActiveRecord.after_all_transactions_commit`** does not work as a substitute inside `before_transition`/`after_transition`. Rails skips non-joinable transactions when deciding whether any transaction is open, and transactional transitions run inside the gem's own non-joinable transaction — so the block yields *immediately*, while the transition is still uncommitted. Use `after_transition_commit` instead.
|
|
250
|
+
|
|
235
251
|
#### Transient Transition Variables
|
|
236
252
|
|
|
237
253
|
Sometimes you may may want to pass additional arguments to a state transition for additional context in your transition callbacks. To do this, add the `transients` option to the `state_options` declaration. This allows you to define any additional attributes you want to be able to pass along during a state transition to that state.
|
data/Rakefile
CHANGED
|
@@ -10,7 +10,9 @@ class String
|
|
|
10
10
|
#
|
|
11
11
|
# @example
|
|
12
12
|
# "some random string".transition_to("draft") #=> false
|
|
13
|
-
|
|
13
|
+
# rubocop:disable Naming/PredicateMethod -- mirrors HasStateMachine::State#transition_to API
|
|
14
|
+
def transition_to(_desired_state, **_options)
|
|
14
15
|
false
|
|
15
16
|
end
|
|
17
|
+
# rubocop:enable Naming/PredicateMethod
|
|
16
18
|
end
|
|
@@ -16,8 +16,8 @@ module HasStateMachine
|
|
|
16
16
|
define_model_callbacks :transition, only: %i[before after]
|
|
17
17
|
|
|
18
18
|
##
|
|
19
|
-
# Defines the after_transition_commit callback
|
|
20
|
-
# transition
|
|
19
|
+
# Defines the after_transition_commit callback, which runs once a
|
|
20
|
+
# successful transition is committed to the database.
|
|
21
21
|
define_model_callbacks :transition_commit, only: %i[after]
|
|
22
22
|
|
|
23
23
|
##
|
|
@@ -63,7 +63,7 @@ module HasStateMachine
|
|
|
63
63
|
# @return [Boolean] whether or not the transition took place
|
|
64
64
|
def transition_to(desired_state, **options)
|
|
65
65
|
transitioned = false
|
|
66
|
-
options = options.
|
|
66
|
+
options = options.symbolize_keys
|
|
67
67
|
desired_state_instance = state_instance(desired_state, options)
|
|
68
68
|
|
|
69
69
|
with_transition_options(options) do
|
|
@@ -78,45 +78,66 @@ module HasStateMachine
|
|
|
78
78
|
|
|
79
79
|
transitioned
|
|
80
80
|
ensure
|
|
81
|
-
|
|
81
|
+
desired_state_instance&.errors&.each do |error|
|
|
82
82
|
object.errors.add(error.attribute, error.type)
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
##
|
|
87
87
|
# Makes the actual transition from one state to the next and
|
|
88
|
-
# runs the before and after transition callbacks.
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
def perform_transition!
|
|
92
|
-
run_callbacks :
|
|
93
|
-
|
|
94
|
-
object.update("#{object.state_attribute}": state)
|
|
95
|
-
end
|
|
88
|
+
# runs the before and after transition callbacks.
|
|
89
|
+
#
|
|
90
|
+
# @return [Boolean] whether or not the transition succeeded
|
|
91
|
+
def perform_transition! # rubocop:disable Naming/PredicateMethod -- public API
|
|
92
|
+
transitioned = run_callbacks :transition do
|
|
93
|
+
object.update("#{object.state_attribute}": state)
|
|
96
94
|
end
|
|
95
|
+
return false unless transitioned
|
|
96
|
+
|
|
97
|
+
@previous_state = previous_state
|
|
98
|
+
enqueue_transition_commit_callbacks
|
|
99
|
+
true
|
|
97
100
|
end
|
|
98
101
|
|
|
99
102
|
##
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
def perform_transactional_transition!
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
rollback_transition unless object.update("#{object.state_attribute}": state)
|
|
109
|
-
end
|
|
103
|
+
# Same as {#perform_transition!}, but wrapped in a transaction so
|
|
104
|
+
# callbacks can roll the transition back.
|
|
105
|
+
#
|
|
106
|
+
# @return [Boolean] whether or not the transition succeeded
|
|
107
|
+
def perform_transactional_transition! # rubocop:disable Naming/PredicateMethod -- public API
|
|
108
|
+
ActiveRecord::Base.transaction(requires_new: true, joinable: false) do
|
|
109
|
+
run_callbacks :transition do
|
|
110
|
+
rollback_transition unless object.update("#{object.state_attribute}": state)
|
|
110
111
|
end
|
|
112
|
+
end
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
@previous_state = previous_state
|
|
115
|
+
return false unless object.reload.public_send(object.state_attribute) == state
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
enqueue_transition_commit_callbacks
|
|
118
|
+
true
|
|
116
119
|
end
|
|
117
120
|
|
|
118
121
|
private
|
|
119
122
|
|
|
123
|
+
##
|
|
124
|
+
# Runs the after_transition_commit callbacks once the outermost open
|
|
125
|
+
# transaction commits, discarding them on rollback.
|
|
126
|
+
#
|
|
127
|
+
# @note Registers on the connection's current transaction because
|
|
128
|
+
# +ActiveRecord.after_all_transactions_commit+ ignores non-joinable
|
|
129
|
+
# transactions (like the gem's own) and would fire immediately.
|
|
130
|
+
# @return [void]
|
|
131
|
+
def enqueue_transition_commit_callbacks
|
|
132
|
+
current_transaction = object.class.connection.current_transaction
|
|
133
|
+
|
|
134
|
+
# Rails < 7.2 has no Transaction#after_commit, so callbacks fire
|
|
135
|
+
# immediately with no deferral. Drop this guard at Rails 7.2+.
|
|
136
|
+
return run_callbacks(:transition_commit) { true } unless current_transaction.respond_to?(:after_commit)
|
|
137
|
+
|
|
138
|
+
current_transaction.after_commit { run_callbacks(:transition_commit) { true } }
|
|
139
|
+
end
|
|
140
|
+
|
|
120
141
|
def rollback_transition
|
|
121
142
|
raise ActiveRecord::Rollback
|
|
122
143
|
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,208 @@
|
|
|
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
|
+
model_name_cache: nil)
|
|
12
|
+
@response_builder = response_builder
|
|
13
|
+
@node_context = node_context
|
|
14
|
+
@index = index
|
|
15
|
+
@rails_client = rails_client
|
|
16
|
+
@model_name_cache = model_name_cache
|
|
17
|
+
|
|
18
|
+
dispatcher.register(self, :on_call_node_enter)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def on_call_node_enter(node)
|
|
22
|
+
return unless current_target?(node)
|
|
23
|
+
|
|
24
|
+
# Check the node shape first: resolving the model name may require a
|
|
25
|
+
# round-trip to the Rails runner, so only pay it for `object` calls.
|
|
26
|
+
if object_call?(node)
|
|
27
|
+
return unless resolved_model_name
|
|
28
|
+
|
|
29
|
+
push_entries(constant_entries(resolved_model_name))
|
|
30
|
+
elsif object_method_call?(node)
|
|
31
|
+
return unless resolved_model_name
|
|
32
|
+
|
|
33
|
+
method_name = message(node)
|
|
34
|
+
entries = method_entries(resolved_model_name, method_name)
|
|
35
|
+
entries = association_entries(resolved_model_name, method_name) if entries.empty?
|
|
36
|
+
|
|
37
|
+
push_entries(entries)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
attr_reader :index, :model_name_cache, :node_context, :rails_client, :response_builder
|
|
44
|
+
|
|
45
|
+
def current_target?(node)
|
|
46
|
+
target = node_context.node if node_context.respond_to?(:node)
|
|
47
|
+
call_node = node_context.call_node if node_context.respond_to?(:call_node)
|
|
48
|
+
|
|
49
|
+
node.equal?(target) || node.equal?(call_node)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def resolved_model_name
|
|
53
|
+
return @resolved_model_name if defined?(@resolved_model_name)
|
|
54
|
+
return @resolved_model_name = nil unless current_class_name
|
|
55
|
+
|
|
56
|
+
@resolved_model_name = model_name_from_rails(workflow_namespace) || convention_model_name
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def convention_model_name
|
|
60
|
+
return @convention_model_name if defined?(@convention_model_name)
|
|
61
|
+
|
|
62
|
+
@convention_model_name = current_class_name && WorkflowResolver.model_name_for(current_class_name)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def workflow_namespace
|
|
66
|
+
return @workflow_namespace if defined?(@workflow_namespace)
|
|
67
|
+
|
|
68
|
+
@workflow_namespace = current_class_name && WorkflowResolver.workflow_namespace_for(current_class_name)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def current_class_name
|
|
72
|
+
return @current_class_name if defined?(@current_class_name)
|
|
73
|
+
return @current_class_name = nil unless node_context.respond_to?(:nesting)
|
|
74
|
+
|
|
75
|
+
@current_class_name = class_name_from_nesting(node_context.nesting)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def class_name_from_nesting(nesting)
|
|
79
|
+
parts = nesting.map(&:to_s).reject(&:empty?)
|
|
80
|
+
return if parts.empty?
|
|
81
|
+
|
|
82
|
+
# Nesting entries may already contain "::" (e.g. "Post::Draft").
|
|
83
|
+
parts.join("::").gsub(/:{3,}/, "::")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def model_name_from_rails(workflow_namespace)
|
|
87
|
+
return unless workflow_namespace && rails_client
|
|
88
|
+
|
|
89
|
+
# Cache hits and misses for the life of the LSP process; the set of
|
|
90
|
+
# models rarely changes mid-session. Failures raise past the cache
|
|
91
|
+
# write, so a slow-to-boot Rails runner can succeed on a later request.
|
|
92
|
+
cache = model_name_cache.to_h
|
|
93
|
+
cache.fetch(workflow_namespace) do
|
|
94
|
+
cache[workflow_namespace] = request_model_name(workflow_namespace)
|
|
95
|
+
end
|
|
96
|
+
rescue
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def request_model_name(workflow_namespace)
|
|
101
|
+
result = rails_client.delegate_request(
|
|
102
|
+
server_addon_name: SERVER_ADDON_NAME,
|
|
103
|
+
request_name: "model_for_workflow_namespace",
|
|
104
|
+
workflow_namespace: workflow_namespace
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
response_name(result)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def association_entries(model_name, association_name)
|
|
111
|
+
association_model_name = association_model_name(model_name, association_name)
|
|
112
|
+
return [] unless association_model_name
|
|
113
|
+
|
|
114
|
+
constant_entries(association_model_name)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def association_model_name(model_name, association_name)
|
|
118
|
+
return unless rails_client&.respond_to?(:association_target)
|
|
119
|
+
|
|
120
|
+
result = rails_client.association_target(model_name: model_name, association_name: association_name)
|
|
121
|
+
response_name(result)
|
|
122
|
+
rescue
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def response_name(result)
|
|
127
|
+
return unless result
|
|
128
|
+
|
|
129
|
+
result[:name] || result["name"]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def constant_entries(name)
|
|
133
|
+
return [] unless name && index
|
|
134
|
+
|
|
135
|
+
Array(index[name])
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def method_entries(model_name, method_name)
|
|
139
|
+
return [] unless index && method_name
|
|
140
|
+
|
|
141
|
+
if index.respond_to?(:resolve_method)
|
|
142
|
+
entries = index.resolve_method(method_name, model_name)
|
|
143
|
+
return Array(entries)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
Array(index[method_name]).select { |entry| entry_owner_name(entry) == model_name }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def entry_owner_name(entry)
|
|
150
|
+
owner = entry.owner if entry.respond_to?(:owner)
|
|
151
|
+
owner.name if owner.respond_to?(:name)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def push_entries(entries)
|
|
155
|
+
entries.each do |entry|
|
|
156
|
+
response_builder << location_for(entry)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def location_for(entry)
|
|
161
|
+
return entry unless defined?(::RubyLsp::Interface::Location)
|
|
162
|
+
|
|
163
|
+
location = entry_location(entry)
|
|
164
|
+
return entry unless location
|
|
165
|
+
|
|
166
|
+
::RubyLsp::Interface::Location.new(
|
|
167
|
+
uri: entry.uri.to_s,
|
|
168
|
+
range: range_for(location)
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def entry_location(entry)
|
|
173
|
+
return entry.location if entry.respond_to?(:location)
|
|
174
|
+
|
|
175
|
+
entry.name_location if entry.respond_to?(:name_location)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def range_for(location)
|
|
179
|
+
::RubyLsp::Interface::Range.new(
|
|
180
|
+
start: ::RubyLsp::Interface::Position.new(
|
|
181
|
+
line: location.start_line - 1,
|
|
182
|
+
character: location.start_column
|
|
183
|
+
),
|
|
184
|
+
end: ::RubyLsp::Interface::Position.new(
|
|
185
|
+
line: location.end_line - 1,
|
|
186
|
+
character: location.end_column
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def object_method_call?(node)
|
|
192
|
+
message(node) && object_call?(receiver(node))
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def object_call?(node)
|
|
196
|
+
node && receiver(node).nil? && message(node) == "object"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def receiver(node)
|
|
200
|
+
node.receiver if node.respond_to?(:receiver)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def message(node)
|
|
204
|
+
node.message.to_s if node.respond_to?(:message) && node.message
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
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 = conventional_model_for(workflow_namespace) || models_by_workflow_namespace[workflow_namespace]
|
|
27
|
+
return unless model
|
|
28
|
+
|
|
29
|
+
{name: model.name}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Fast path: for the default "Workflow::<Model>" namespace, autoload just
|
|
34
|
+
# that one constant instead of eager loading the whole application. Only
|
|
35
|
+
# custom workflow_namespace configurations need the full scan below.
|
|
36
|
+
def conventional_model_for(workflow_namespace)
|
|
37
|
+
workflow_namespace = workflow_namespace.to_s
|
|
38
|
+
return unless workflow_namespace.start_with?("Workflow::")
|
|
39
|
+
|
|
40
|
+
model = workflow_namespace.delete_prefix("Workflow::").safe_constantize
|
|
41
|
+
model if model.try(:workflow_namespace) == workflow_namespace
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def models_by_workflow_namespace
|
|
45
|
+
@models_by_workflow_namespace ||= active_record_models.each_with_object({}) do |model, index|
|
|
46
|
+
next unless model.respond_to?(:workflow_namespace)
|
|
47
|
+
|
|
48
|
+
index[model.workflow_namespace] = model
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def active_record_models
|
|
53
|
+
@active_record_models ||= begin
|
|
54
|
+
::Rails.application&.eager_load!
|
|
55
|
+
::ActiveRecord::Base.descendants.reject(&:abstract_class?)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
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.1
|
|
4
|
+
version: 1.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Benjamin Hargett
|
|
@@ -38,6 +38,20 @@ dependencies:
|
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '1.5'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: nokogiri
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "<"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.19'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "<"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.19'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: pry
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -67,7 +81,21 @@ dependencies:
|
|
|
67
81
|
- !ruby/object:Gem::Version
|
|
68
82
|
version: '0'
|
|
69
83
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
84
|
+
name: rubocop
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.74'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.74'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rubocop-performance
|
|
71
99
|
requirement: !ruby/object:Gem::Requirement
|
|
72
100
|
requirements:
|
|
73
101
|
- - ">="
|
|
@@ -80,6 +108,20 @@ dependencies:
|
|
|
80
108
|
- - ">="
|
|
81
109
|
- !ruby/object:Gem::Version
|
|
82
110
|
version: '0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: parallel
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "<"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '2.0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "<"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '2.0'
|
|
83
125
|
- !ruby/object:Gem::Dependency
|
|
84
126
|
name: appraisal
|
|
85
127
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -95,19 +137,117 @@ dependencies:
|
|
|
95
137
|
- !ruby/object:Gem::Version
|
|
96
138
|
version: '0'
|
|
97
139
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name:
|
|
140
|
+
name: rspec-rails
|
|
99
141
|
requirement: !ruby/object:Gem::Requirement
|
|
100
142
|
requirements:
|
|
101
|
-
- - "
|
|
143
|
+
- - ">="
|
|
102
144
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '
|
|
145
|
+
version: '6.1'
|
|
104
146
|
type: :development
|
|
105
147
|
prerelease: false
|
|
106
148
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
149
|
requirements:
|
|
108
|
-
- - "
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '6.1'
|
|
153
|
+
- !ruby/object:Gem::Dependency
|
|
154
|
+
name: ruby-lsp
|
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
|
156
|
+
requirements:
|
|
157
|
+
- - ">="
|
|
158
|
+
- !ruby/object:Gem::Version
|
|
159
|
+
version: '0.18'
|
|
160
|
+
type: :development
|
|
161
|
+
prerelease: false
|
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - ">="
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '0.18'
|
|
167
|
+
- !ruby/object:Gem::Dependency
|
|
168
|
+
name: ruby-lsp-rails
|
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
|
170
|
+
requirements:
|
|
171
|
+
- - ">="
|
|
172
|
+
- !ruby/object:Gem::Version
|
|
173
|
+
version: 0.3.17
|
|
174
|
+
type: :development
|
|
175
|
+
prerelease: false
|
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
177
|
+
requirements:
|
|
178
|
+
- - ">="
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: 0.3.17
|
|
181
|
+
- !ruby/object:Gem::Dependency
|
|
182
|
+
name: ruby-lsp-rspec
|
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
|
184
|
+
requirements:
|
|
185
|
+
- - ">="
|
|
109
186
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '
|
|
187
|
+
version: '0'
|
|
188
|
+
type: :development
|
|
189
|
+
prerelease: false
|
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
191
|
+
requirements:
|
|
192
|
+
- - ">="
|
|
193
|
+
- !ruby/object:Gem::Version
|
|
194
|
+
version: '0'
|
|
195
|
+
- !ruby/object:Gem::Dependency
|
|
196
|
+
name: base64
|
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
|
198
|
+
requirements:
|
|
199
|
+
- - ">="
|
|
200
|
+
- !ruby/object:Gem::Version
|
|
201
|
+
version: '0'
|
|
202
|
+
type: :development
|
|
203
|
+
prerelease: false
|
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
205
|
+
requirements:
|
|
206
|
+
- - ">="
|
|
207
|
+
- !ruby/object:Gem::Version
|
|
208
|
+
version: '0'
|
|
209
|
+
- !ruby/object:Gem::Dependency
|
|
210
|
+
name: bigdecimal
|
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
|
212
|
+
requirements:
|
|
213
|
+
- - ">="
|
|
214
|
+
- !ruby/object:Gem::Version
|
|
215
|
+
version: '0'
|
|
216
|
+
type: :development
|
|
217
|
+
prerelease: false
|
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
219
|
+
requirements:
|
|
220
|
+
- - ">="
|
|
221
|
+
- !ruby/object:Gem::Version
|
|
222
|
+
version: '0'
|
|
223
|
+
- !ruby/object:Gem::Dependency
|
|
224
|
+
name: drb
|
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
|
226
|
+
requirements:
|
|
227
|
+
- - ">="
|
|
228
|
+
- !ruby/object:Gem::Version
|
|
229
|
+
version: '0'
|
|
230
|
+
type: :development
|
|
231
|
+
prerelease: false
|
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
233
|
+
requirements:
|
|
234
|
+
- - ">="
|
|
235
|
+
- !ruby/object:Gem::Version
|
|
236
|
+
version: '0'
|
|
237
|
+
- !ruby/object:Gem::Dependency
|
|
238
|
+
name: mutex_m
|
|
239
|
+
requirement: !ruby/object:Gem::Requirement
|
|
240
|
+
requirements:
|
|
241
|
+
- - ">="
|
|
242
|
+
- !ruby/object:Gem::Version
|
|
243
|
+
version: '0'
|
|
244
|
+
type: :development
|
|
245
|
+
prerelease: false
|
|
246
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
247
|
+
requirements:
|
|
248
|
+
- - ">="
|
|
249
|
+
- !ruby/object:Gem::Version
|
|
250
|
+
version: '0'
|
|
111
251
|
description: HasStateMachine uses ruby classes to make creating a finite state machine
|
|
112
252
|
in your ActiveRecord models a breeze.
|
|
113
253
|
email:
|
|
@@ -128,6 +268,10 @@ files:
|
|
|
128
268
|
- lib/has_state_machine/state.rb
|
|
129
269
|
- lib/has_state_machine/state_helpers.rb
|
|
130
270
|
- lib/has_state_machine/version.rb
|
|
271
|
+
- lib/ruby_lsp/has_state_machine/addon.rb
|
|
272
|
+
- lib/ruby_lsp/has_state_machine/definition.rb
|
|
273
|
+
- lib/ruby_lsp/has_state_machine/rails_server_addon.rb
|
|
274
|
+
- lib/ruby_lsp/has_state_machine/workflow_resolver.rb
|
|
131
275
|
homepage: https://www.github.com/encampment/has_state_machine
|
|
132
276
|
licenses:
|
|
133
277
|
- MIT
|
|
@@ -146,7 +290,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
146
290
|
- !ruby/object:Gem::Version
|
|
147
291
|
version: '0'
|
|
148
292
|
requirements: []
|
|
149
|
-
rubygems_version: 3.6.
|
|
293
|
+
rubygems_version: 3.6.9
|
|
150
294
|
specification_version: 4
|
|
151
295
|
summary: Class based state machine for ActiveRecord models.
|
|
152
296
|
test_files: []
|