circulator 1.0.0 → 2.0.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: 34d38ad852dfc0dea3071103c368fa9d77224c7a390b7fc10c6a39fde2e2b507
4
- data.tar.gz: c67068423ece22963cdf64391f0424251a8c23caebf08280a6bb977b1a18bd4b
3
+ metadata.gz: f0a33536db417f338113b2ef4a5656a38994652f4454f8583ea8e22d626a0e9e
4
+ data.tar.gz: 394085a68c1a7444192264fa69152be2827663284f4221b844f4fbfcb45cf64b
5
5
  SHA512:
6
- metadata.gz: dc4264165cdb3fdf72ec246dcabc02c0bacff9d20f24a167984e6521517550cc131fd0711f289892c8263fea6ac6c07dcc1d39c0ed77165dbf74d2a3d044f331
7
- data.tar.gz: d85440b2e40cc2aa2ff1f7848f5b4dc63b94c2217ebdd7918488967489badb439edcb1c5f39c0b762634f1739ce38405ad19e841a7f4b84c2bb8671262418ec3
6
+ metadata.gz: 4489ca3bf464fe0b37ad3b2e41096fcce05a2d5bcc5348d25e7750f278c666fd10c266e397635846b0093d1ace1064da754d50a0d6973b07f2a4086b5983926d
7
+ data.tar.gz: 4da4067d6b07dccc9207e17c6095b236dab9f9e7c1d6399f5ff0c475b761b84da34490ec5d8d61ab71678e18fd014b327f6f9358b5aa571b8c70e19306698da6
data/CHANGELOG.md CHANGED
@@ -5,8 +5,8 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [1.0.0] - 2025-09-11
8
+ ## [2.0.0] - 2025-09-12
9
9
 
10
- ###
10
+ ### Removed
11
11
 
12
- - Initial release.
12
+ - The `Circulator::Diverter` module has been and features added to `Circulator` itself.
data/README.md CHANGED
@@ -39,7 +39,7 @@ gem install circulator
39
39
 
40
40
  ```ruby
41
41
  class Order
42
- extend Circulator::Diverter
42
+ extend Circulator
43
43
 
44
44
  attr_accessor :status
45
45
 
@@ -77,7 +77,7 @@ order.status_deliver # => :delivered
77
77
 
78
78
  ```ruby
79
79
  class Document
80
- extend Circulator::Diverter
80
+ extend Circulator
81
81
 
82
82
  attr_accessor :state, :reviewed_by
83
83
 
@@ -97,7 +97,7 @@ end
97
97
 
98
98
  ```ruby
99
99
  class Task
100
- extend Circulator::Diverter
100
+ extend Circulator
101
101
 
102
102
  attr_accessor :priority, :urgency_level
103
103
 
@@ -114,7 +114,7 @@ end
114
114
 
115
115
  ```ruby
116
116
  class Server
117
- extend Circulator::Diverter
117
+ extend Circulator
118
118
 
119
119
  attr_accessor :power_state, :network_state
120
120
 
@@ -147,7 +147,7 @@ end
147
147
 
148
148
  ```ruby
149
149
  class Payment
150
- extend Circulator::Diverter
150
+ extend Circulator
151
151
 
152
152
  attr_accessor :status, :processed_at
153
153
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "diverter"
4
-
5
3
  module Circulator
6
4
  class Flow
7
5
  def initialize(klass, attribute_name, states = Set.new, &block)
@@ -70,4 +68,4 @@ module Circulator
70
68
  end
71
69
  end
72
70
  end
73
- end
71
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Circulator
4
- VERSION = "1.0.0"
4
+ VERSION = "2.0.0"
5
5
  end
data/lib/circulator.rb CHANGED
@@ -1,3 +1,241 @@
1
1
  require "circulator/version"
2
- require "circulator/diverter"
3
2
  require "circulator/flow"
3
+
4
+ module Circulator
5
+ # Declare a flow for an attribute.
6
+ #
7
+ # Specify the attribute to be used for states and actions.
8
+ #
9
+ # Example:
10
+ #
11
+ # flow(:status) do
12
+ # state :pending do
13
+ # action :approve, to: :approved
14
+ # end
15
+ # end
16
+ #
17
+ # The above declares a flow for the `status` attribute. When in the `pending`
18
+ # state, the `approve` action will transition the `status` to `approved`.
19
+ #
20
+ # This creates a `status_approve` method which will change the state in memory.
21
+ #
22
+ # You will also have a instance method `flow` which will allow you to specify
23
+ # the action to take on the attribute.
24
+ #
25
+ # Example:
26
+ #
27
+ # test_object.status_approve
28
+ # # OR
29
+ # test_object.flow(:approve, :status)
30
+ #
31
+ # You can also provide a block to receive arguments
32
+ #
33
+ # Example:
34
+ #
35
+ # flow(:status) do
36
+ # state :pending do
37
+ # action :approve, to: :approved do |*args, **kwargs|
38
+ # @args_received = {args: args, kwargs: kwargs}
39
+ # end
40
+ # action_allowed(:approve) { true } # Optional. Check some value on self
41
+ # end
42
+ # end
43
+ #
44
+ # The block will be evalutaed on the instance of the class declaring the flow.
45
+ # So `self` inside that action block will be the instance of the class.
46
+ #
47
+ # Example:
48
+ #
49
+ # test_object.status_approve("arg1", "arg2", key: "value")
50
+ # # @args_received will be {args: ["arg1", "arg2"], kwargs: {key: "value"}}
51
+ #
52
+ # If the action is not allowed, the transition will not be executed.
53
+ #
54
+ # Example:
55
+ #
56
+ # flow(:status) do
57
+ # state :pending do
58
+ # action :approve, to: :approved do
59
+ # @args_received = {args: args, kwargs: kwargs}
60
+ # end
61
+ # action_allowed(:approve) { false }
62
+ # end
63
+ # end
64
+ #
65
+ # test_object.status_approve
66
+ # # Will not transition because the action is not allowed
67
+ #
68
+ # You may also specify the `allow_if` option to check a condition before
69
+ # the action is allowed. The callable will be evaluated on the instance of
70
+ # the class declaring the flow. So `self` inside that block will be the
71
+ # instance of the class.
72
+ #
73
+ # Example:
74
+ #
75
+ # flow(:status) do
76
+ # state :pending do
77
+ # action :approve, to: :approved, allow_if: -> { true }
78
+ # end
79
+ # end
80
+ #
81
+ # test_object.status_approve
82
+ # # Will transition to :approved if the condition is true
83
+ #
84
+ # If you declare states separately, for example in an enum, you can use the
85
+ # `action` method to declare the action on the attribute.
86
+ #
87
+ # Example:
88
+ #
89
+ # enum :status, {pending: 0, approved: 1, rejected: 2}
90
+ # flow(:status) do
91
+ # action :approve, to: :approved, from: :pending
92
+ # action :reject, to: :rejected, from: :approved do |rejected_at|
93
+ # self.rejected_at = rejected_at
94
+ # end
95
+ # end
96
+ #
97
+ # test_object.status_approve
98
+ # # Will transition to :approved
99
+ # test_object.status_reject
100
+ # # Will transition to :rejected and set the rejected_at attribute
101
+ #
102
+ # By default, if there is no transition for the current state, the flow will
103
+ # raise an error. You can specify a no_action block to handle this case.
104
+ #
105
+ # Example:
106
+ #
107
+ # flow(:status) do
108
+ # no_action { |attribute_name, action| raise "Nope!" }
109
+ # end
110
+ #
111
+ # test_object.status_approve
112
+ # # Will raise an error
113
+ #
114
+ # You can also provide a custom action for other behavior for a set of states and
115
+ # use the `to` option as a callable to set the attribute.
116
+ #
117
+ # Example:
118
+ #
119
+ # flow(:status) do
120
+ # action :unknown, to: -> { status }, from: [:enforcing, :monitoring, :ignoring] do |signal|
121
+ # raise UnhandledSignalError, signal
122
+ # end
123
+ # end
124
+ #
125
+ # test_object.flow(:unknown, :status, "signal")
126
+ # # Will raise an UnhandledSignalError
127
+ #
128
+ def flow(attribute_name, model: to_s, &block)
129
+ @flows ||= {}
130
+ model_key = Circulator.model_key(model)
131
+ @flows[model_key] ||= {}
132
+ @flows[model_key][attribute_name] = Flow.new(model, attribute_name, &block)
133
+
134
+ flow_module = ancestors.find { |ancestor|
135
+ ancestor.name.to_s =~ /FlowMethods/
136
+ } || Module.new.tap do |mod|
137
+ include mod
138
+
139
+ const_set(:FlowMethods, mod)
140
+ end
141
+
142
+ object = if model == to_s
143
+ nil
144
+ else
145
+ Circulator.methodize_name(model)
146
+ end
147
+
148
+ @flows.dig(model_key, attribute_name).transition_map.each do |action, transitions|
149
+ define_flow_method(attribute_name:, action:, transitions:, object:, owner: flow_module)
150
+ end
151
+ end
152
+ alias_method :circulator, :flow
153
+
154
+ def define_flow_method(attribute_name:, action:, transitions:, object:, owner:)
155
+ object_attribute_method = [object, attribute_name, action].compact.join("_")
156
+ raise ArgumentError, "Method already defined: #{object_attribute_method}" if owner.method_defined?(object_attribute_method)
157
+
158
+ owner.define_method(object_attribute_method) do |*args, flow_target: self, **kwargs, &block|
159
+ current_value = flow_target.send(attribute_name)
160
+
161
+ transition = if current_value.respond_to?(:to_sym)
162
+ transitions[current_value.to_sym]
163
+ else
164
+ transitions[current_value]
165
+ end
166
+
167
+ unless transition
168
+ flow_target.instance_exec(attribute_name, action, &flows.dig(Circulator.model_key(flow_target), attribute_name).no_action)
169
+ return
170
+ end
171
+
172
+ if transition[:allow_if]
173
+ return unless flow_target.instance_exec(*args, **kwargs, &transition[:allow_if])
174
+ end
175
+
176
+ if transition[:block]
177
+ flow_target.instance_exec(*args, **kwargs, &transition[:block])
178
+ end
179
+
180
+ if transition[:to].respond_to?(:call)
181
+ flow_target.send("#{attribute_name}=", flow_target.instance_exec(*args, **kwargs, &transition[:to]))
182
+ else
183
+ flow_target.send("#{attribute_name}=", transition[:to])
184
+ end.tap do
185
+ if block
186
+ flow_target.instance_exec(*args, **kwargs, &block)
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ module_function def model_key(object)
193
+ if object.is_a?(String)
194
+ if object.start_with?("#<Class:")
195
+ "anonymous_#{object.split("0x")[1]}".sub(">", "")
196
+ else
197
+ object
198
+ end
199
+ else
200
+ model_key(object.class.name || object.class.to_s)
201
+ end
202
+ end
203
+
204
+ module_function def methodize_name(name)
205
+ name.split("::").map { |part| part.gsub(/([a-z])([A-Z])/, '\1_\2') }.join("_").downcase
206
+ end
207
+
208
+ def self.extended(base)
209
+ base.include(InstanceMethods)
210
+ base.singleton_class.attr_reader :flows
211
+ end
212
+
213
+ module InstanceMethods
214
+ # Use this method to call an action on the attribute.
215
+ #
216
+ # Example:
217
+ #
218
+ # test_object.flow(:approve, :status)
219
+ # test_object.flow(:approve, :status, "arg1", "arg2", key: "value")
220
+ def flow(action, attribute, *args, flow_target: self, **kwargs, &block)
221
+ target_name = if flow_target != self
222
+ Circulator.methodize_name(Circulator.model_key(flow_target))
223
+ end
224
+ external_attribute_name = [target_name, attribute].compact.join("_")
225
+ method_name = "#{external_attribute_name}_#{action}"
226
+ if respond_to?(method_name)
227
+ send(method_name, *args, flow_target:, **kwargs, &block)
228
+ elsif flow_target.respond_to?(method_name)
229
+ flow_target.send(method_name, *args, **kwargs, &block)
230
+ else
231
+ raise "Invalid action for the current state of #{attribute} (#{flow_target.send(attribute).inspect}): #{action}"
232
+ end
233
+ end
234
+
235
+ private
236
+
237
+ def flows
238
+ self.class.flows
239
+ end
240
+ end
241
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: circulator
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay
@@ -20,7 +20,6 @@ files:
20
20
  - README.md
21
21
  - Rakefile
22
22
  - lib/circulator.rb
23
- - lib/circulator/diverter.rb
24
23
  - lib/circulator/flow.rb
25
24
  - lib/circulator/version.rb
26
25
  homepage: https://github.com/SOFware/circulator
@@ -1,242 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Circulator
4
- module Diverter
5
- # Declare a flow for an attribute.
6
- #
7
- # Specify the attribute to be used for states and actions.
8
- #
9
- # Example:
10
- #
11
- # flow(:status) do
12
- # state :pending do
13
- # action :approve, to: :approved
14
- # end
15
- # end
16
- #
17
- # The above declares a flow for the `status` attribute. When in the `pending`
18
- # state, the `approve` action will transition the `status` to `approved`.
19
- #
20
- # This creates a `status_approve` method which will change the state in memory.
21
- #
22
- # You will also have a instance method `flow` which will allow you to specify
23
- # the action to take on the attribute.
24
- #
25
- # Example:
26
- #
27
- # test_object.status_approve
28
- # # OR
29
- # test_object.flow(:approve, :status)
30
- #
31
- # You can also provide a block to receive arguments
32
- #
33
- # Example:
34
- #
35
- # flow(:status) do
36
- # state :pending do
37
- # action :approve, to: :approved do |*args, **kwargs|
38
- # @args_received = {args: args, kwargs: kwargs}
39
- # end
40
- # action_allowed(:approve) { true } # Optional. Check some value on self
41
- # end
42
- # end
43
- #
44
- # The block will be evalutaed on the instance of the class declaring the flow.
45
- # So `self` inside that action block will be the instance of the class.
46
- #
47
- # Example:
48
- #
49
- # test_object.status_approve("arg1", "arg2", key: "value")
50
- # # @args_received will be {args: ["arg1", "arg2"], kwargs: {key: "value"}}
51
- #
52
- # If the action is not allowed, the transition will not be executed.
53
- #
54
- # Example:
55
- #
56
- # flow(:status) do
57
- # state :pending do
58
- # action :approve, to: :approved do
59
- # @args_received = {args: args, kwargs: kwargs}
60
- # end
61
- # action_allowed(:approve) { false }
62
- # end
63
- # end
64
- #
65
- # test_object.status_approve
66
- # # Will not transition because the action is not allowed
67
- #
68
- # You may also specify the `allow_if` option to check a condition before
69
- # the action is allowed. The callable will be evaluated on the instance of
70
- # the class declaring the flow. So `self` inside that block will be the
71
- # instance of the class.
72
- #
73
- # Example:
74
- #
75
- # flow(:status) do
76
- # state :pending do
77
- # action :approve, to: :approved, allow_if: -> { true }
78
- # end
79
- # end
80
- #
81
- # test_object.status_approve
82
- # # Will transition to :approved if the condition is true
83
- #
84
- # If you declare states separately, for example in an enum, you can use the
85
- # `action` method to declare the action on the attribute.
86
- #
87
- # Example:
88
- #
89
- # enum :status, {pending: 0, approved: 1, rejected: 2}
90
- # flow(:status) do
91
- # action :approve, to: :approved, from: :pending
92
- # action :reject, to: :rejected, from: :approved do |rejected_at|
93
- # self.rejected_at = rejected_at
94
- # end
95
- # end
96
- #
97
- # test_object.status_approve
98
- # # Will transition to :approved
99
- # test_object.status_reject
100
- # # Will transition to :rejected and set the rejected_at attribute
101
- #
102
- # By default, if there is no transition for the current state, the flow will
103
- # raise an error. You can specify a no_action block to handle this case.
104
- #
105
- # Example:
106
- #
107
- # flow(:status) do
108
- # no_action { |attribute_name, action| raise "Nope!" }
109
- # end
110
- #
111
- # test_object.status_approve
112
- # # Will raise an error
113
- #
114
- # You can also provide a custom action for other behavior for a set of states and
115
- # use the `to` option as a callable to set the attribute.
116
- #
117
- # Example:
118
- #
119
- # flow(:status) do
120
- # action :unknown, to: -> { status }, from: [:enforcing, :monitoring, :ignoring] do |signal|
121
- # raise UnhandledSignalError, signal
122
- # end
123
- # end
124
- #
125
- # test_object.flow(:unknown, :status, "signal")
126
- # # Will raise an UnhandledSignalError
127
- #
128
- def flow(attribute_name, model: to_s, &block)
129
- @flows ||= {}
130
- model_key = Circulator::Diverter.model_key(model)
131
- @flows[model_key] ||= {}
132
- @flows[model_key][attribute_name] = Circulator::Flow.new(model, attribute_name, &block)
133
-
134
- flow_module = ancestors.find { |ancestor|
135
- ancestor.name.to_s =~ /FlowMethods/
136
- } || Module.new.tap do |mod|
137
- include mod
138
-
139
- const_set(:FlowMethods, mod)
140
- end
141
-
142
- object = if model == to_s
143
- nil
144
- else
145
- Circulator::Diverter.methodize_name(model)
146
- end
147
-
148
- @flows.dig(model_key, attribute_name).transition_map.each do |action, transitions|
149
- define_flow_method(attribute_name:, action:, transitions:, object:, owner: flow_module)
150
- end
151
- end
152
- alias_method :circulator, :flow
153
-
154
- def define_flow_method(attribute_name:, action:, transitions:, object:, owner:)
155
- object_attribute_method = [object, attribute_name, action].compact.join("_")
156
- raise ArgumentError, "Method already defined: #{object_attribute_method}" if owner.method_defined?(object_attribute_method)
157
-
158
- owner.define_method(object_attribute_method) do |*args, flow_target: self, **kwargs, &block|
159
- current_value = flow_target.send(attribute_name)
160
-
161
- transition = if current_value.respond_to?(:to_sym)
162
- transitions[current_value.to_sym]
163
- else
164
- transitions[current_value]
165
- end
166
-
167
- unless transition
168
- flow_target.instance_exec(attribute_name, action, &flows.dig(Circulator::Diverter.model_key(flow_target), attribute_name).no_action)
169
- return
170
- end
171
-
172
- if transition[:allow_if]
173
- return unless flow_target.instance_exec(*args, **kwargs, &transition[:allow_if])
174
- end
175
-
176
- if transition[:block]
177
- flow_target.instance_exec(*args, **kwargs, &transition[:block])
178
- end
179
-
180
- if transition[:to].respond_to?(:call)
181
- flow_target.send("#{attribute_name}=", flow_target.instance_exec(*args, **kwargs, &transition[:to]))
182
- else
183
- flow_target.send("#{attribute_name}=", transition[:to])
184
- end.tap do
185
- if block
186
- flow_target.instance_exec(*args, **kwargs, &block)
187
- end
188
- end
189
- end
190
- end
191
-
192
- module_function def model_key(object)
193
- if object.is_a?(String)
194
- if object.start_with?("#<Class:")
195
- "anonymous_#{object.split("0x")[1]}".sub(">", "")
196
- else
197
- object
198
- end
199
- else
200
- model_key(object.class.name || object.class.to_s)
201
- end
202
- end
203
-
204
- module_function def methodize_name(name)
205
- name.split("::").map { |part| part.gsub(/([a-z])([A-Z])/, '\1_\2') }.join("_").downcase
206
- end
207
-
208
- def self.extended(base)
209
- base.include(InstanceMethods)
210
- base.singleton_class.attr_reader :flows
211
- end
212
-
213
- module InstanceMethods
214
- # Use this method to call an action on the attribute.
215
- #
216
- # Example:
217
- #
218
- # test_object.flow(:approve, :status)
219
- # test_object.flow(:approve, :status, "arg1", "arg2", key: "value")
220
- def flow(action, attribute, *args, flow_target: self, **kwargs, &block)
221
- target_name = if flow_target != self
222
- Circulator::Diverter.methodize_name(Circulator::Diverter.model_key(flow_target))
223
- end
224
- external_attribute_name = [target_name, attribute].compact.join("_")
225
- method_name = "#{external_attribute_name}_#{action}"
226
- if respond_to?(method_name)
227
- send(method_name, *args, flow_target:, **kwargs, &block)
228
- elsif flow_target.respond_to?(method_name)
229
- flow_target.send(method_name, *args, **kwargs, &block)
230
- else
231
- raise "Invalid action for the current state of #{attribute} (#{flow_target.send(attribute).inspect}): #{action}"
232
- end
233
- end
234
-
235
- private
236
-
237
- def flows
238
- self.class.flows
239
- end
240
- end
241
- end
242
- end