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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afd9065697aa4494f7ccf324fd1322f0a44c7dd99495c772766f7b1ad63070b2
4
- data.tar.gz: 7a58038b3b8f06b9d21871a5321d627224f7c56f65b3d3bbcb7d40b54af3b802
3
+ metadata.gz: ffbecb9f1fd850903e2f88e7576bf684f03e38d14e70c3e07e61c0db93621961
4
+ data.tar.gz: f259e6b012ff0bfca16320de3403967518de35e9c9b8e5d4baf53c5cd03ce6d2
5
5
  SHA512:
6
- metadata.gz: 6305468c38c4f46fdfda9e38134896b405e57f425afad55bada899b0e2071659eaa005eed4e0851708765d8da36c834ac97ba3e7698cf0c2855e8f699808dd4c
7
- data.tar.gz: 91cc65e7186915376b8990b2efc40093573a8dbc16039177786ba20b524a1c3b95713c9b0d0eb733cdbfe7ea66cdf22f5ce7dc22dcd61675aedb21855e005017
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
- dry_sequence.add_step(step_name, side_effect_free: true, &block)
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
- dry_sequence.add_step(
31
- step_name,
32
- side_effect_free: true,
33
- &referenced_step.implementation
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
- # mulitple validation steps are okay but otherwise step names need to be unique
46
- if step_name != :validate && !steps_for_name(step_name).empty?
47
- raise "an operation step named #{step_name.inspect} already exists"
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, step|
99
- current_step = 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
- step.implementation.call(memo, original_state, run_options)
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)
@@ -16,6 +16,10 @@ module SmashTheState
16
16
  def side_effect_free?
17
17
  options[:side_effect_free] == true
18
18
  end
19
+
20
+ def dup
21
+ super
22
+ end
19
23
  end
20
24
  end
21
25
  end
@@ -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
- step :validate, side_effect_free: true do |state|
73
- Operation::State.eval_validation_directives_block(state, &block)
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 :validate, side_effect_free: true do |state, original_state|
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
@@ -1,3 +1,3 @@
1
1
  module SmashTheState
2
- VERSION = "1.2.4".freeze
2
+ VERSION = "1.3.0".freeze
3
3
  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
 
@@ -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.2.4
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-05-13 00:00:00.000000000 Z
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
- rubygems_version: 3.0.3
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,