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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffbecb9f1fd850903e2f88e7576bf684f03e38d14e70c3e07e61c0db93621961
4
- data.tar.gz: f259e6b012ff0bfca16320de3403967518de35e9c9b8e5d4baf53c5cd03ce6d2
3
+ metadata.gz: 9a4407b73af1d296ca49dc0e0d112e3fd4a497d37e66cc97026bcf75835e8d48
4
+ data.tar.gz: 19c36a89864c088ff0ff068768049dc68c798b57b3b074dd4cd4721f46386abb
5
5
  SHA512:
6
- metadata.gz: 143ca4fdccc3fd34300f133dae21d70e55ceff9121523a456ad499839403901d56d086059913defe0fddc6ba612046489065d8c9160e13b189da0a6a1cfe0719
7
- data.tar.gz: 2c40f91ac88831ea57b0ab44b24e4b514eea3524b5445c3cfb246dc12f1d5047731ed7620145f7a4d57478bf292fc93d540bb38916bc5c23a7e513aad83385db
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 State Classes (built at runtime)
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 omit a schema block and the raw params will be passed in as the initial state. From there you can create whatever state class you desire from inside the first step.
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 MyOperation < SmashTheState::Operation
105
- step :custom_state_class do |params|
106
- c = Operation::State.build do
107
- # create whatever state class you need at runtime
108
- attribute :some_name, :some_type
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, params|
115
- # params will be the initial params
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 are functional, so they don't support inheritance. In lieu of inheritance, you may use `continues_from`, 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
+ 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).constantize
100
- rescue NameError, NoMethodError
101
- nil
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.dup
145
+ original_state = make_original_state(state)
124
146
  current_step = nil
125
147
 
126
148
  steps_to_run.reduce(state) do |memo, s|
@@ -17,9 +17,9 @@ module SmashTheState
17
17
  class << self
18
18
  attr_accessor :representer
19
19
 
20
- def build(&block)
20
+ def build(params = nil, &block)
21
21
  Class.new(self).tap do |k|
22
- k.class_eval(&block)
22
+ k.class_exec(params, &block)
23
23
  end
24
24
  end
25
25
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module SmashTheState
2
- VERSION = "1.3.0".freeze
2
+ VERSION = "1.4.4".freeze
3
3
  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
- it "runs the middleware_class_block, passes in the state, constantizes" do
208
- expect(instance.middleware_class("String")).to eq(String)
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
- context "with a NameError" do
213
- it "returns nil" do
214
- expect(instance.middleware_class("Whazzat")).to eq(nil)
215
- end
216
- end
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
- context "with a NoMethodError" do
219
- let!(:state) { double }
220
+ context "that is a class" do
221
+ let!(:klass) { Class.new }
220
222
 
221
- before do
222
- allow(state).to receive(:to_s) do
223
- raise NoMethodError
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(state)).to eq(nil)
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")
@@ -120,9 +120,9 @@ describe SmashTheState::Operation do
120
120
  step_name,
121
121
  step_options,
122
122
  &implementation
123
- ).and_return(token_result)
123
+ )
124
124
 
125
- expect(klass.override_step(step_name, step_options, &implementation)).to eq(token_result)
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" do
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.3.0
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: 2019-08-15 00:00:00.000000000 Z
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: 3.0.0
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: 3.0.0
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
- rubyforge_project:
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,