smash_the_state 1.3.0 → 1.4.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +14 -13
- data/lib/smash_the_state/operation/dry_run.rb +8 -0
- data/lib/smash_the_state/operation/sequence.rb +29 -7
- data/lib/smash_the_state/operation/state.rb +2 -2
- data/lib/smash_the_state/operation.rb +17 -0
- data/lib/smash_the_state/version.rb +1 -1
- data/spec/unit/operation/sequence_spec.rb +28 -15
- data/spec/unit/operation_spec.rb +50 -7
- metadata +5 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a4407b73af1d296ca49dc0e0d112e3fd4a497d37e66cc97026bcf75835e8d48
|
4
|
+
data.tar.gz: 19c36a89864c088ff0ff068768049dc68c798b57b3b074dd4cd4721f46386abb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3054575953e6e59b15a454609698b8346a6a2fab75ee578c94029b895e03568ed0af214db70cd283c7db0eb97954a41d752d8ed4b18898282940a01534969b49
|
7
|
+
data.tar.gz: b08cde0d76694574172be59467ffe790013aaa462664182345fb494d84976837102db57c60ba74e0753cda7c1b7925fa85adef77fae15e2eefecf5bb0903b41e
|
data/README.md
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
# Smash the State
|
4
4
|
A useful utility for transforming state that provides step sequencing, middleware, and validation.
|
5
5
|
|
6
|
+
Inspired by [Arpeggiate](https://github.com/onyxrev/arpeggiate), an Elixir operation library by [@onyxrev](https://github.com/onyxrev).
|
7
|
+
|
6
8
|
# Example
|
7
9
|
|
8
10
|
``` ruby
|
@@ -96,24 +98,23 @@ class CreateAnalyticsOperation < SmashTheState::Operation
|
|
96
98
|
end
|
97
99
|
```
|
98
100
|
|
99
|
-
## Dynamic
|
101
|
+
## Dynamic Schemas (built at runtime)
|
100
102
|
|
101
|
-
Maybe your operation needs a more flexible schema than a static state class can provide. If you need your state class to be evaluated at runtime, you can
|
103
|
+
Maybe your operation needs a more flexible schema than a static state class can provide. Maybe you need to base your schema on some other data model that isn't available at the time the class is evaluated. If you need your state class to be evaluated at runtime, you can specify `dynamic_schema` with a block. The raw params hash will be passed in as the initial state. From there you can create whatever state class you desire at runtime. Be careful with this because this can quickly get out of hand. If you find yourself using dynamic schemas frequently, you may actually want distinct operations with static schemas.
|
102
104
|
|
103
105
|
```ruby
|
104
|
-
class
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
106
|
+
class BillingStuff < SmashTheState::Operation
|
107
|
+
dynamic_schema do |params|
|
108
|
+
# let's say we want to base our schema on an external service.
|
109
|
+
# we can call the service and build our schema off of the keys it returns
|
110
|
+
# let's say it's something like {name: "...", id: "...", is_paid: "..."}
|
111
|
+
BillingService.get_billing_things.keys do |key|
|
112
|
+
attribute key, :string
|
109
113
|
end
|
110
|
-
|
111
|
-
c.new(params)
|
112
114
|
end
|
113
115
|
|
114
|
-
step :do_more_things do |state
|
115
|
-
#
|
116
|
-
# ... and so on
|
116
|
+
step :do_more_things do |state|
|
117
|
+
# ... we receive a state with name, id, and is_paid attributes ...
|
117
118
|
end
|
118
119
|
end
|
119
120
|
```
|
@@ -318,7 +319,7 @@ end
|
|
318
319
|
|
319
320
|
## Continuation
|
320
321
|
|
321
|
-
Smash the State operations
|
322
|
+
Smash the State operations can be chained. To simplify this, you may use the `continues_from` helper, which frontloads an existing operation in front of the operation being defined. It feeds the state result of the first operation into the first step of the second.
|
322
323
|
|
323
324
|
``` ruby
|
324
325
|
class SecondOperation < SmashTheState::Operation
|
@@ -7,6 +7,13 @@ module SmashTheState
|
|
7
7
|
def initialize(wet_sequence)
|
8
8
|
@wet_sequence = wet_sequence
|
9
9
|
@dry_sequence = Operation::Sequence.new
|
10
|
+
|
11
|
+
# in the case of a dynamic schema, front-load it as the first step
|
12
|
+
dynamic_schema_step = @wet_sequence.dynamic_schema_step
|
13
|
+
|
14
|
+
return if dynamic_schema_step.nil?
|
15
|
+
|
16
|
+
step(dynamic_schema_step.name, &dynamic_schema_step.implementation)
|
10
17
|
end
|
11
18
|
|
12
19
|
def step(step_name, &block)
|
@@ -54,6 +61,7 @@ module SmashTheState
|
|
54
61
|
sequence.side_effect_free
|
55
62
|
end
|
56
63
|
|
64
|
+
seq.run_options[:dry] = true
|
57
65
|
run_sequence(seq, params)
|
58
66
|
end
|
59
67
|
alias dry_call dry_run
|
@@ -32,7 +32,6 @@ module SmashTheState
|
|
32
32
|
# return a copy without the steps that produce side-effects
|
33
33
|
def side_effect_free
|
34
34
|
dup.tap do |seq|
|
35
|
-
seq.run_options[:dry] = true
|
36
35
|
seq.instance_eval do
|
37
36
|
@steps = seq.steps.select(&:side_effect_free?)
|
38
37
|
end
|
@@ -94,13 +93,20 @@ module SmashTheState
|
|
94
93
|
step.error_handler = block
|
95
94
|
end
|
96
95
|
|
97
|
-
# rubocop:disable Lint/ShadowedException
|
98
96
|
def middleware_class(state, original_state = nil)
|
99
|
-
middleware_class_block.call(state, original_state)
|
100
|
-
|
101
|
-
|
97
|
+
klass = middleware_class_block.call(state, original_state)
|
98
|
+
|
99
|
+
case klass
|
100
|
+
when Module, Class
|
101
|
+
klass
|
102
|
+
else
|
103
|
+
begin
|
104
|
+
klass.constantize if klass.respond_to?(:constantize)
|
105
|
+
rescue NameError
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
102
109
|
end
|
103
|
-
# rubocop:enable Lint/ShadowedException
|
104
110
|
|
105
111
|
def add_middleware_step(step_name, options = {})
|
106
112
|
step = Operation::Step.new step_name, options do |state, original_state|
|
@@ -115,12 +121,28 @@ module SmashTheState
|
|
115
121
|
@steps << step
|
116
122
|
end
|
117
123
|
|
124
|
+
def dynamic_schema?
|
125
|
+
dynamic_schema_step.nil? == false
|
126
|
+
end
|
127
|
+
|
128
|
+
def dynamic_schema_step
|
129
|
+
steps_for_name(:_dynamic_schema).first
|
130
|
+
end
|
131
|
+
|
118
132
|
private
|
119
133
|
|
134
|
+
def make_original_state(state)
|
135
|
+
return dynamic_schema_step.implementation.call(state, state, run_options) if dynamic_schema?
|
136
|
+
|
137
|
+
state.
|
138
|
+
dup.
|
139
|
+
freeze # don't allow any modifications to the original state
|
140
|
+
end
|
141
|
+
|
120
142
|
def run_steps(steps_to_run, state)
|
121
143
|
# retain a copy of the original state so that we can refer to it for posterity as
|
122
144
|
# the operation state gets mutated over time
|
123
|
-
original_state = state
|
145
|
+
original_state = make_original_state(state)
|
124
146
|
current_step = nil
|
125
147
|
|
126
148
|
steps_to_run.reduce(state) do |memo, s|
|
@@ -35,12 +35,26 @@ module SmashTheState
|
|
35
35
|
@state_class = Operation::State.build(&block)
|
36
36
|
end
|
37
37
|
|
38
|
+
def dynamic_schema(&block)
|
39
|
+
sequence.add_step :_dynamic_schema do |params|
|
40
|
+
Operation::State.build(params, &block).new(params)
|
41
|
+
end
|
42
|
+
|
43
|
+
# make sure that the dynamic schema step that we just added above is always first
|
44
|
+
sequence.steps.unshift sequence.steps.pop
|
45
|
+
end
|
46
|
+
|
38
47
|
def step(step_name, options = {}, &block)
|
39
48
|
sequence.add_step(step_name, options, &block)
|
40
49
|
end
|
41
50
|
|
42
51
|
def override_step(step_name, options = {}, &block)
|
43
52
|
sequence.override_step(step_name, options, &block)
|
53
|
+
|
54
|
+
# also override the dry run step
|
55
|
+
return if dry_run_sequence.steps_for_name(step_name).length.zero?
|
56
|
+
|
57
|
+
dry_run_sequence.override_step(step_name, options, &block)
|
44
58
|
end
|
45
59
|
|
46
60
|
def error(*steps, &block)
|
@@ -122,6 +136,9 @@ module SmashTheState
|
|
122
136
|
|
123
137
|
# also copy the state class over
|
124
138
|
child_class.instance_variable_set(:@state_class, state_class && state_class.dup)
|
139
|
+
|
140
|
+
# also copy the dry run sequence
|
141
|
+
child_class.dry_run_sequence.steps.concat(dry_run_sequence.steps.map(&:dup))
|
125
142
|
end
|
126
143
|
end
|
127
144
|
end
|
@@ -204,28 +204,40 @@ describe SmashTheState::Operation::Sequence do
|
|
204
204
|
end
|
205
205
|
|
206
206
|
context "with a middleware_class_block" do
|
207
|
-
|
208
|
-
|
207
|
+
context "that returns a string" do
|
208
|
+
it "runs the middleware_class_block, passes in the state, constantizes" do
|
209
|
+
expect(instance.middleware_class("String")).to eq(String)
|
210
|
+
end
|
209
211
|
end
|
210
|
-
end
|
211
212
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
213
|
+
context "that returns a constant" do
|
214
|
+
before do
|
215
|
+
instance.middleware_class_block = proc do |state|
|
216
|
+
state
|
217
|
+
end
|
218
|
+
end
|
217
219
|
|
218
|
-
|
219
|
-
|
220
|
+
context "that is a class" do
|
221
|
+
let!(:klass) { Class.new }
|
220
222
|
|
221
|
-
|
222
|
-
|
223
|
-
|
223
|
+
it "runs the middleware_class_block, passes in the state" do
|
224
|
+
expect(instance.middleware_class(klass)).to eq(klass)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
context "that returns a module" do
|
229
|
+
let!(:modyule) { Module.new }
|
230
|
+
|
231
|
+
it "runs the middleware_class_block, passes in the state" do
|
232
|
+
expect(instance.middleware_class(modyule)).to eq(modyule)
|
233
|
+
end
|
224
234
|
end
|
225
235
|
end
|
236
|
+
end
|
226
237
|
|
238
|
+
context "with a NameError" do
|
227
239
|
it "returns nil" do
|
228
|
-
expect(instance.middleware_class(
|
240
|
+
expect(instance.middleware_class("Whazzat")).to eq(nil)
|
229
241
|
end
|
230
242
|
end
|
231
243
|
end
|
@@ -254,10 +266,11 @@ describe SmashTheState::Operation::Sequence do
|
|
254
266
|
instance.add_middleware_step :extra_step
|
255
267
|
end
|
256
268
|
|
257
|
-
it "block receives both the state and original state" do
|
269
|
+
it "block receives both the state and frozen original state" do
|
258
270
|
instance.middleware_class_block = proc do |state, original_state|
|
259
271
|
expect(state).to eq(baz: "bing")
|
260
272
|
expect(original_state).to eq(foo: "bar")
|
273
|
+
expect(original_state.frozen?).to eq(true)
|
261
274
|
end
|
262
275
|
|
263
276
|
instance.call(foo: "bar")
|
data/spec/unit/operation_spec.rb
CHANGED
@@ -120,9 +120,9 @@ describe SmashTheState::Operation do
|
|
120
120
|
step_name,
|
121
121
|
step_options,
|
122
122
|
&implementation
|
123
|
-
)
|
123
|
+
)
|
124
124
|
|
125
|
-
|
125
|
+
klass.override_step(step_name, step_options, &implementation)
|
126
126
|
end
|
127
127
|
end
|
128
128
|
|
@@ -147,6 +147,15 @@ describe SmashTheState::Operation do
|
|
147
147
|
state.countup += "two"
|
148
148
|
state
|
149
149
|
end
|
150
|
+
|
151
|
+
dry_run_sequence do
|
152
|
+
step :one
|
153
|
+
step :two
|
154
|
+
step :three do |state|
|
155
|
+
state.countup += "blar"
|
156
|
+
state
|
157
|
+
end
|
158
|
+
end
|
150
159
|
end
|
151
160
|
end
|
152
161
|
end
|
@@ -189,13 +198,46 @@ describe SmashTheState::Operation do
|
|
189
198
|
too_large = child.call(countup: "")
|
190
199
|
expect(too_large.errors[:countup]).to eq(["can't be blank"])
|
191
200
|
end
|
201
|
+
|
202
|
+
it "populates the dry run sequence and honors overridden steps" do
|
203
|
+
expect(child.dry_run(countup: "zero").countup).to eq("zerooneoneandahalfblar")
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe "self#dynamic_schema" do
|
208
|
+
let!(:klass) do
|
209
|
+
Class.new(SmashTheState::Operation).tap do |k|
|
210
|
+
k.class_eval do
|
211
|
+
dynamic_schema do |params|
|
212
|
+
attribute "#{params[:adjective]}_#{params[:animal]}", :string
|
213
|
+
end
|
214
|
+
|
215
|
+
step :step_one do |state|
|
216
|
+
state.fat_ocelot = "maybe"
|
217
|
+
state
|
218
|
+
end
|
219
|
+
|
220
|
+
step :step_two do |state, original_state|
|
221
|
+
[state, original_state]
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
it "produces an 'inline' schema that is evaluated each time the operation "\
|
228
|
+
"runs while passing the 'original' dynamic state onto the next step" do
|
229
|
+
state, original_state = klass.call(adjective: "fat", animal: "ocelot", fat_ocelot: "no")
|
230
|
+
|
231
|
+
expect(state.fat_ocelot).to eq("maybe")
|
232
|
+
expect(original_state.fat_ocelot).to eq("no")
|
233
|
+
end
|
192
234
|
end
|
193
235
|
|
194
236
|
describe "self#dry_run_sequence" do
|
195
237
|
context "with a custom dry run sequence block" do
|
196
238
|
before do
|
197
|
-
klass.step :step_one do |state|
|
198
|
-
state.name = state.name + " one"
|
239
|
+
klass.step :step_one do |state, _original_state, run_options|
|
240
|
+
state.name = state.name + " one " + run_options.to_json
|
199
241
|
state
|
200
242
|
end
|
201
243
|
|
@@ -224,16 +266,17 @@ describe SmashTheState::Operation do
|
|
224
266
|
end
|
225
267
|
|
226
268
|
it "provides a custom sequence for the dry run that " \
|
227
|
-
"contains only side-effect free steps"
|
269
|
+
"contains only side-effect free steps, and specifies " \
|
270
|
+
"whether the run is dry the run options" do
|
228
271
|
result = klass.call(name: "Sam")
|
229
|
-
expect(result.name).to eq("Sam one two three")
|
272
|
+
expect(result.name).to eq("Sam one {\"dry\":false} two three")
|
230
273
|
|
231
274
|
expect(
|
232
275
|
klass.dry_run_sequence.steps.all?(&:side_effect_free?)
|
233
276
|
).to eq(true)
|
234
277
|
|
235
278
|
dry_result = klass.dry_run(name: "Sam")
|
236
|
-
expect(dry_result.name).to eq("Sam one custom")
|
279
|
+
expect(dry_result.name).to eq("Sam one {\"dry\":true} custom")
|
237
280
|
end
|
238
281
|
end
|
239
282
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smash_the_state
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Connor
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-10-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: active_model_attributes
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 6.0.3.1
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 6.0.3.1
|
41
41
|
description: ''
|
42
42
|
email:
|
43
43
|
- dan@danconnor.com
|
@@ -83,8 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
83
|
- !ruby/object:Gem::Version
|
84
84
|
version: '0'
|
85
85
|
requirements: []
|
86
|
-
|
87
|
-
rubygems_version: 2.7.7
|
86
|
+
rubygems_version: 3.1.4
|
88
87
|
signing_key:
|
89
88
|
specification_version: 4
|
90
89
|
summary: A useful utility for transforming state that provides step sequencing, middleware,
|