smash_the_state 1.2.4 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +63 -0
- data/lib/smash_the_state/operation/dry_run.rb +12 -6
- data/lib/smash_the_state/operation/sequence.rb +38 -6
- data/lib/smash_the_state/operation/state.rb +6 -0
- data/lib/smash_the_state/operation/step.rb +4 -0
- data/lib/smash_the_state/operation/validation_step.rb +47 -0
- data/lib/smash_the_state/operation.rb +20 -4
- data/lib/smash_the_state/version.rb +1 -1
- data/spec/unit/operation/sequence_spec.rb +57 -0
- data/spec/unit/operation_spec.rb +88 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ffbecb9f1fd850903e2f88e7576bf684f03e38d14e70c3e07e61c0db93621961
|
4
|
+
data.tar.gz: f259e6b012ff0bfca16320de3403967518de35e9c9b8e5d4baf53c5cd03ce6d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 143ca4fdccc3fd34300f133dae21d70e55ceff9121523a456ad499839403901d56d086059913defe0fddc6ba612046489065d8c9160e13b189da0a6a1cfe0719
|
7
|
+
data.tar.gz: 2c40f91ac88831ea57b0ab44b24e4b514eea3524b5445c3cfb246dc12f1d5047731ed7620145f7a4d57478bf292fc93d540bb38916bc5c23a7e513aad83385db
|
data/README.md
CHANGED
@@ -159,6 +159,69 @@ class CreateDatabase < SmashTheState::Operation
|
|
159
159
|
end
|
160
160
|
```
|
161
161
|
|
162
|
+
## Inheritance-like Behavior
|
163
|
+
|
164
|
+
Smash is a library that generally follows the functional approach and you should focus on using that approach when using it. However, there are times when sprinkling a dash of inheritance into the mix can make your life easier.
|
165
|
+
|
166
|
+
When using the inheritance pattern, all validation blocks are evaluated together and are run together as one big validation step. The parent class' schema is copied to the child class. All steps are copied to the child class, but individual steps may be overridden using `override_step`. Overridden steps are run in the same step index in which they were originally defined in the parent sequence.
|
167
|
+
|
168
|
+
``` ruby
|
169
|
+
class CreateOperation < SmashTheState::Operation
|
170
|
+
schema do
|
171
|
+
attribute :version, :string
|
172
|
+
attribute :name, :string
|
173
|
+
end
|
174
|
+
|
175
|
+
validate do
|
176
|
+
validates_presence_of :name
|
177
|
+
end
|
178
|
+
|
179
|
+
step :download_image do |state|
|
180
|
+
GenericImage.download(name)
|
181
|
+
end
|
182
|
+
|
183
|
+
step :create do |state|
|
184
|
+
Deployment.create(state.to_hash)
|
185
|
+
end
|
186
|
+
|
187
|
+
step :set_up_billing do |deployment|
|
188
|
+
Billing.charge_for_deployment(deployment)
|
189
|
+
|
190
|
+
deployment
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
class RestoreOperation < CreateOperation
|
195
|
+
# we get the parent class' schema for free when we inherit. if you want to extend the
|
196
|
+
# schema in child classes, I recommend breaking out your schema into modules
|
197
|
+
|
198
|
+
# the validation of CreateOperation will be evaluated at the same time as this following
|
199
|
+
# block. in other words, not only will :name have to be present, but for restoration,
|
200
|
+
# the name has to be the name of a known source image
|
201
|
+
validate do
|
202
|
+
validate :source_image_exists
|
203
|
+
|
204
|
+
def source_image_exists
|
205
|
+
unless SourceImage.exist?(name)
|
206
|
+
add_error(:name, "is not a restorable image")
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# steps from the parent class are copied over in order. you can override specific steps
|
212
|
+
# in the child class
|
213
|
+
|
214
|
+
# ...download_image runs
|
215
|
+
|
216
|
+
override_step :create do |state|
|
217
|
+
# we're going to diverge from create by "restoring" an image rather than "creating" an image
|
218
|
+
Deployment.restore(state.to_hash)
|
219
|
+
end
|
220
|
+
|
221
|
+
# ... set_up_billing runs
|
222
|
+
end
|
223
|
+
```
|
224
|
+
|
162
225
|
## Representation
|
163
226
|
|
164
227
|
Let's say you want to represent the state of the operation, wrapped in a class that defines some `as_*` and `to_*` methods. You can do this with a `represent` step.
|
@@ -13,7 +13,7 @@ module SmashTheState
|
|
13
13
|
referenced_steps = wet_sequence.steps_for_name(step_name)
|
14
14
|
|
15
15
|
if block
|
16
|
-
|
16
|
+
add_dry_run_step(step_name, &block)
|
17
17
|
return
|
18
18
|
end
|
19
19
|
|
@@ -27,11 +27,17 @@ module SmashTheState
|
|
27
27
|
# we're gonna copy the implementation verbatim but add a new step marked as
|
28
28
|
# side-effect-free, because if the step was added to the dry run sequence it
|
29
29
|
# must be assumed to be side-effect-free
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
30
|
+
add_dry_run_step(step_name, &referenced_step.implementation)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def add_dry_run_step(step_name, &block)
|
37
|
+
if step_name == :validate
|
38
|
+
dry_sequence.add_validation_step(&block)
|
39
|
+
else
|
40
|
+
dry_sequence.add_step(step_name, side_effect_free: true, &block)
|
35
41
|
end
|
36
42
|
end
|
37
43
|
end
|
@@ -1,6 +1,9 @@
|
|
1
1
|
module SmashTheState
|
2
2
|
class Operation
|
3
3
|
class Sequence
|
4
|
+
class BadOverride < StandardError; end
|
5
|
+
class StepConflict < StandardError; end
|
6
|
+
|
4
7
|
attr_accessor :middleware_class_block
|
5
8
|
attr_reader :steps, :run_options
|
6
9
|
|
@@ -42,14 +45,39 @@ module SmashTheState
|
|
42
45
|
end
|
43
46
|
|
44
47
|
def add_step(step_name, options = {}, &block)
|
45
|
-
#
|
46
|
-
|
47
|
-
raise
|
48
|
+
# steps need to be unique
|
49
|
+
unless steps_for_name(step_name).empty?
|
50
|
+
raise(
|
51
|
+
StepConflict,
|
52
|
+
"an operation step named #{step_name.inspect} already exists"
|
53
|
+
)
|
48
54
|
end
|
49
55
|
|
50
56
|
@steps << Step.new(step_name, options, &block)
|
51
57
|
end
|
52
58
|
|
59
|
+
def add_validation_step(options = {}, &block)
|
60
|
+
step = steps_for_name(:validate).first ||
|
61
|
+
SmashTheState::Operation::ValidationStep.new(options)
|
62
|
+
|
63
|
+
step.add_implementation(&block)
|
64
|
+
@steps |= [step]
|
65
|
+
end
|
66
|
+
|
67
|
+
def override_step(step_name, options = {}, &block)
|
68
|
+
step = steps_for_name(step_name).first
|
69
|
+
|
70
|
+
if step.nil?
|
71
|
+
raise(
|
72
|
+
BadOverride,
|
73
|
+
"overriding step #{step_name.inspect} failed because it does " \
|
74
|
+
"not exist"
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
@steps[@steps.index(step)] = Step.new(step_name, options, &block)
|
79
|
+
end
|
80
|
+
|
53
81
|
# returns steps named the specified name. it's generally bad form to have mulitple
|
54
82
|
# steps with the same name, but it can happen in some reasonable cases (the most
|
55
83
|
# common being :validate)
|
@@ -95,14 +123,18 @@ module SmashTheState
|
|
95
123
|
original_state = state.dup
|
96
124
|
current_step = nil
|
97
125
|
|
98
|
-
steps_to_run.reduce(state) do |memo,
|
99
|
-
current_step =
|
126
|
+
steps_to_run.reduce(state) do |memo, s|
|
127
|
+
current_step = s
|
100
128
|
|
101
129
|
# we're gonna pass the state from the previous step into the implementation as
|
102
130
|
# 'memo', but for convenience, we'll also always pass the original state into
|
103
131
|
# the implementation as 'original_state' so that no matter what you can get to
|
104
132
|
# your original input
|
105
|
-
|
133
|
+
if s.name == :validate
|
134
|
+
s.validate!(memo)
|
135
|
+
else
|
136
|
+
s.implementation.call(memo, original_state, run_options)
|
137
|
+
end
|
106
138
|
end
|
107
139
|
rescue Operation::State::Invalid => e
|
108
140
|
e.state
|
@@ -48,6 +48,12 @@ module SmashTheState
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
+
def extend_validation_directives_block(state, &block)
|
52
|
+
state.tap do |s|
|
53
|
+
s.class_eval(&block)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
51
57
|
# for non-ActiveModel states we will just evaluate the block as a validator
|
52
58
|
def eval_custom_validator_block(state, original_state = nil)
|
53
59
|
yield(state, original_state)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module SmashTheState
|
2
|
+
class Operation
|
3
|
+
class ValidationStep < Step
|
4
|
+
attr_accessor :implementations
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
@name = :validate
|
8
|
+
@implementations = []
|
9
|
+
@options = {
|
10
|
+
side_effect_free: true
|
11
|
+
}.merge(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_implementation(&block)
|
15
|
+
tap do
|
16
|
+
@implementations << block
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def implementation
|
21
|
+
blocks = @implementations
|
22
|
+
|
23
|
+
proc do
|
24
|
+
# self here should be a state
|
25
|
+
blocks.reduce(self) do |memo, i|
|
26
|
+
memo.class_eval(&i)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def validate!(state)
|
32
|
+
state.tap do
|
33
|
+
SmashTheState::Operation::State.
|
34
|
+
eval_validation_directives_block(state, &implementation)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def dup
|
39
|
+
# it's not enough to duplicate the step, we should also duplicate our
|
40
|
+
# implementations. otherwise the list of implementations will be shared
|
41
|
+
super.tap do |s|
|
42
|
+
s.implementations = s.implementations.dup
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative 'operation/error'
|
2
2
|
require_relative 'operation/sequence'
|
3
3
|
require_relative 'operation/step'
|
4
|
+
require_relative 'operation/validation_step'
|
4
5
|
require_relative 'operation/state'
|
5
6
|
require_relative 'operation/dry_run'
|
6
7
|
require_relative 'operation/state_type'
|
@@ -38,6 +39,10 @@ module SmashTheState
|
|
38
39
|
sequence.add_step(step_name, options, &block)
|
39
40
|
end
|
40
41
|
|
42
|
+
def override_step(step_name, options = {}, &block)
|
43
|
+
sequence.override_step(step_name, options, &block)
|
44
|
+
end
|
45
|
+
|
41
46
|
def error(*steps, &block)
|
42
47
|
steps.each do |step_name|
|
43
48
|
sequence.add_error_handler_for_step(step_name, &block)
|
@@ -65,12 +70,12 @@ module SmashTheState
|
|
65
70
|
sequence.add_middleware_step(step_name, options)
|
66
71
|
end
|
67
72
|
|
68
|
-
def validate(&block)
|
73
|
+
def validate(options = {}, &block)
|
69
74
|
# when we add a validation step, all proceeding steps must not produce
|
70
75
|
# side-effects (subsequent steps are case-by-case)
|
71
76
|
sequence.mark_as_side_effect_free!
|
72
|
-
|
73
|
-
Operation::State.
|
77
|
+
sequence.add_validation_step(options) do |state|
|
78
|
+
Operation::State.extend_validation_directives_block(state, &block)
|
74
79
|
end
|
75
80
|
end
|
76
81
|
|
@@ -78,7 +83,7 @@ module SmashTheState
|
|
78
83
|
# when we add a validation step, all proceeding steps must not produce
|
79
84
|
# side-effects (subsequent steps are case-by-case)
|
80
85
|
sequence.mark_as_side_effect_free!
|
81
|
-
step :
|
86
|
+
step :custom_validation do |state, original_state|
|
82
87
|
Operation::State.eval_custom_validator_block(state, original_state, &block)
|
83
88
|
end
|
84
89
|
end
|
@@ -107,5 +112,16 @@ module SmashTheState
|
|
107
112
|
sequence_to_run.call(state || params)
|
108
113
|
end
|
109
114
|
end
|
115
|
+
|
116
|
+
def self.inherited(child_class)
|
117
|
+
# all steps from the parent first need to be cloned
|
118
|
+
new_steps = sequence.steps.map(&:dup)
|
119
|
+
|
120
|
+
# and then we add them to the child's empty sequence
|
121
|
+
child_class.sequence.steps.concat(new_steps)
|
122
|
+
|
123
|
+
# also copy the state class over
|
124
|
+
child_class.instance_variable_set(:@state_class, state_class && state_class.dup)
|
125
|
+
end
|
110
126
|
end
|
111
127
|
end
|
@@ -137,6 +137,63 @@ describe SmashTheState::Operation::Sequence do
|
|
137
137
|
end
|
138
138
|
end
|
139
139
|
|
140
|
+
describe "#override_step" do
|
141
|
+
let!(:instance) { subject.new }
|
142
|
+
|
143
|
+
before do
|
144
|
+
instance.add_step :one do |state|
|
145
|
+
state << :one
|
146
|
+
end
|
147
|
+
|
148
|
+
instance.add_step :two do |state|
|
149
|
+
state << :two
|
150
|
+
end
|
151
|
+
|
152
|
+
instance.add_step :three do |state|
|
153
|
+
state << :three
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
context "when no step by the given name exists" do
|
158
|
+
it "raises an exception" do
|
159
|
+
begin
|
160
|
+
instance.override_step :four do
|
161
|
+
end
|
162
|
+
|
163
|
+
raise "should not reach this"
|
164
|
+
rescue SmashTheState::Operation::Sequence::BadOverride => e
|
165
|
+
expect(
|
166
|
+
e.to_s
|
167
|
+
).to eq("overriding step :four failed because it does not exist")
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context "with a pre-existing step by that name" do
|
173
|
+
it "replaces the step in the position in which it originally appeared" do
|
174
|
+
instance.override_step :two do |state|
|
175
|
+
state << :pants
|
176
|
+
end
|
177
|
+
|
178
|
+
expect(instance.call([])).to eq(%i[one pants three])
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe "#add_validation_step" do
|
184
|
+
let!(:instance) { subject.new }
|
185
|
+
let!(:implementations) { 3.times.map { -> {} } }
|
186
|
+
|
187
|
+
before do
|
188
|
+
implementations.each { |i| instance.add_validation_step(&i) }
|
189
|
+
end
|
190
|
+
|
191
|
+
it "adds all the validation implementations to the validate step" do
|
192
|
+
validate_step = instance.steps_for_name(:validate).first
|
193
|
+
expect(validate_step.implementations).to eq(implementations)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
140
197
|
describe "#middleware_class" do
|
141
198
|
let!(:instance) { subject.new }
|
142
199
|
|
data/spec/unit/operation_spec.rb
CHANGED
@@ -103,6 +103,94 @@ describe SmashTheState::Operation do
|
|
103
103
|
end
|
104
104
|
end
|
105
105
|
|
106
|
+
describe "self#override_step" do
|
107
|
+
let!(:step_name) { :override_me }
|
108
|
+
let!(:step_options) { { some: :option } }
|
109
|
+
let!(:implementation) do
|
110
|
+
proc do
|
111
|
+
:success
|
112
|
+
end
|
113
|
+
end
|
114
|
+
let!(:token_result) { double }
|
115
|
+
|
116
|
+
it "delegates to the sequence implementation of :override_step" do
|
117
|
+
expect(
|
118
|
+
klass.sequence
|
119
|
+
).to receive(:override_step).with(
|
120
|
+
step_name,
|
121
|
+
step_options,
|
122
|
+
&implementation
|
123
|
+
).and_return(token_result)
|
124
|
+
|
125
|
+
expect(klass.override_step(step_name, step_options, &implementation)).to eq(token_result)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "inheritance" do
|
130
|
+
let!(:parent) do
|
131
|
+
Class.new(SmashTheState::Operation).tap do |k|
|
132
|
+
k.class_eval do
|
133
|
+
schema do
|
134
|
+
attribute :countup, :string
|
135
|
+
end
|
136
|
+
|
137
|
+
validate do
|
138
|
+
validates_presence_of :countup
|
139
|
+
end
|
140
|
+
|
141
|
+
step :one do |state|
|
142
|
+
state.countup += "one"
|
143
|
+
state
|
144
|
+
end
|
145
|
+
|
146
|
+
step :two do |state|
|
147
|
+
state.countup += "two"
|
148
|
+
state
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
let!(:child) do
|
155
|
+
Class.new(parent).tap do |k|
|
156
|
+
k.class_eval do
|
157
|
+
override_step :two do |state|
|
158
|
+
state.countup += "oneandahalf"
|
159
|
+
state
|
160
|
+
end
|
161
|
+
|
162
|
+
validate do
|
163
|
+
validates_length_of :countup, maximum: 5
|
164
|
+
end
|
165
|
+
|
166
|
+
step :three do |state|
|
167
|
+
state.countup += "three"
|
168
|
+
state
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
it "duplicates steps from the parent operation, allows overrides" do
|
175
|
+
expect(parent.sequence.steps.length).to eq(3)
|
176
|
+
expect(child.sequence.steps.length).to eq(4)
|
177
|
+
expect(child.call(countup: "zero").countup).to eq("zerooneoneandahalfthree")
|
178
|
+
end
|
179
|
+
|
180
|
+
it "duplicates the state class from the parent operation" do
|
181
|
+
expect(child.state_class).to_not eq(parent.state_class)
|
182
|
+
expect(child.state_class.attributes_registry).to eq(countup: [:string, {}])
|
183
|
+
end
|
184
|
+
|
185
|
+
it "merges the validation blocks" do
|
186
|
+
too_large = child.call(countup: "thisistoolarge")
|
187
|
+
expect(too_large.errors[:countup]).to eq(["is too long (maximum is 5 characters)"])
|
188
|
+
|
189
|
+
too_large = child.call(countup: "")
|
190
|
+
expect(too_large.errors[:countup]).to eq(["can't be blank"])
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
106
194
|
describe "self#dry_run_sequence" do
|
107
195
|
context "with a custom dry run sequence block" do
|
108
196
|
before do
|
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.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Connor
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-08-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: active_model_attributes
|
@@ -57,6 +57,7 @@ files:
|
|
57
57
|
- lib/smash_the_state/operation/state.rb
|
58
58
|
- lib/smash_the_state/operation/state_type.rb
|
59
59
|
- lib/smash_the_state/operation/step.rb
|
60
|
+
- lib/smash_the_state/operation/validation_step.rb
|
60
61
|
- lib/smash_the_state/version.rb
|
61
62
|
- spec/unit/operation/definition_spec.rb
|
62
63
|
- spec/unit/operation/sequence_spec.rb
|
@@ -82,7 +83,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
83
|
- !ruby/object:Gem::Version
|
83
84
|
version: '0'
|
84
85
|
requirements: []
|
85
|
-
|
86
|
+
rubyforge_project:
|
87
|
+
rubygems_version: 2.7.7
|
86
88
|
signing_key:
|
87
89
|
specification_version: 4
|
88
90
|
summary: A useful utility for transforming state that provides step sequencing, middleware,
|