smash_the_state 1.2.4

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.
@@ -0,0 +1,19 @@
1
+ module SmashTheState
2
+ class Operation
3
+ class StateType < ActiveModel::Type::Value
4
+ def initialize(block)
5
+ @schema_class = Operation::State.build(&block)
6
+ end
7
+
8
+ private
9
+
10
+ def cast_value(attributes)
11
+ @schema_class.new(attributes)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ ActiveModel::Type.register(:state_for_smashing) do |_name, options|
18
+ SmashTheState::Operation::StateType.new(options[:schema])
19
+ end
@@ -0,0 +1,21 @@
1
+ module SmashTheState
2
+ class Operation
3
+ class Step
4
+ attr_accessor :error_handler
5
+ attr_reader :name, :implementation, :options
6
+
7
+ def initialize(step_name, options = {}, &block)
8
+ @name = step_name
9
+ @implementation = block
10
+ @options = {
11
+ # defaults
12
+ side_effect_free: nil # nil roughly implies unknown
13
+ }.merge(options)
14
+ end
15
+
16
+ def side_effect_free?
17
+ options[:side_effect_free] == true
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module SmashTheState
2
+ VERSION = "1.2.4".freeze
3
+ end
@@ -0,0 +1,57 @@
1
+ require "spec_helper"
2
+
3
+ # not sure why getting a `NoMethodError: undefined method `empty?' for nil:NilClass` error
4
+ # when the literal module is referenced here as opposed to as a string
5
+ describe "SmashTheState::Operation::Definition" do
6
+ let!(:subject) do
7
+ location_definition = Class.new(SmashTheState::Operation::Definition).tap do |k|
8
+ k.class_eval do
9
+ definition "Location"
10
+
11
+ schema do
12
+ attribute :postal_code, :string
13
+ attribute :lat, :float
14
+ attribute :lon, :float
15
+ end
16
+ end
17
+ end
18
+
19
+ Class.new(SmashTheState::Operation::Definition).tap do |k|
20
+ k.class_eval do
21
+ definition "Syndicate"
22
+
23
+ schema do
24
+ # smoke test nested definitions
25
+ schema :location, ref: location_definition
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "inheritance" do
32
+ it "inherits from State" do
33
+ expect(subject.ancestors).to include(SmashTheState::Operation::State)
34
+ end
35
+ end
36
+
37
+ describe "#ref" do
38
+ it "returns the definition name" do
39
+ expect(subject.ref).to eq("Syndicate")
40
+ end
41
+ end
42
+
43
+ describe "#to_s" do
44
+ it "returns the ref" do
45
+ expect(subject.to_s).to eq(subject.ref)
46
+ end
47
+ end
48
+
49
+ describe "#schema with a referenced definition" do
50
+ it "pulls in the schema from the referened definition" do
51
+ expect(
52
+ # wheeeeeee
53
+ subject.attributes_registry[:location][1][:ref].attributes_registry.keys
54
+ ).to eq(%i[postal_code lat lon])
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,256 @@
1
+ require "spec_helper"
2
+ require "json"
3
+
4
+ describe SmashTheState::Operation::Sequence do
5
+ let!(:subject) { SmashTheState::Operation::Sequence }
6
+
7
+ describe "#initialize" do
8
+ it "initializes steps as an array" do
9
+ expect(subject.new.steps).to be_a(Array)
10
+ end
11
+ end
12
+
13
+ describe "#call" do
14
+ let!(:instance) { subject.new }
15
+
16
+ context "in general" do
17
+ before do
18
+ instance.add_step(:one) do |state|
19
+ state << 1
20
+ end
21
+
22
+ instance.add_step(:two) do |state|
23
+ state << 2
24
+ end
25
+ end
26
+
27
+ it "runs each steps' implementation block, passing each steps' return " \
28
+ "value to the next step" do
29
+ expect(instance.call([])).to eq([1, 2])
30
+ end
31
+ end
32
+
33
+ context "with an Invalid exception" do
34
+ before do
35
+ instance.instance_eval do |i|
36
+ i.add_step(:one) do |state|
37
+ state << :invalid
38
+ raise SmashTheState::Operation::State::Invalid, state
39
+ end
40
+ end
41
+
42
+ instance.add_step(:two) do |_state|
43
+ raise "should not hit this"
44
+ end
45
+ end
46
+
47
+ it "returns the state of the step, skips subsequent steps" do
48
+ expect(instance.call([])).to eq([:invalid])
49
+ end
50
+ end
51
+
52
+ context "with an Error exception" do
53
+ before do
54
+ instance.instance_eval do |i|
55
+ i.add_step(:one) do |_state|
56
+ # test that we can pass a different state to the error handler
57
+ raise SmashTheState::Operation::Error, :custom_state
58
+ end
59
+ end
60
+
61
+ instance.add_step(:two) do |_state|
62
+ raise "should not hit this"
63
+ end
64
+ end
65
+
66
+ context "with an error handler" do
67
+ before do
68
+ instance.add_error_handler_for_step :one do |custom_state, original_state|
69
+ [custom_state, original_state]
70
+ end
71
+ end
72
+
73
+ it "runs the error handler" do
74
+ expect(instance.call([])).to eq([:custom_state, []])
75
+ end
76
+ end
77
+
78
+ context "without an error handler" do
79
+ it "re-raises the Error exception" do
80
+ expect do
81
+ begin
82
+ instance.call([])
83
+ raise "should not hit this"
84
+ rescue SmashTheState::Operation::Error => e
85
+ expect(e.state).to eq([:foo])
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ describe "steps" do
94
+ let!(:instance) { subject.new }
95
+
96
+ before do
97
+ instance.add_step :new_step do |state|
98
+ state << :step_added
99
+ end
100
+ end
101
+
102
+ let!(:step) { instance.steps.first }
103
+
104
+ describe "#add_step" do
105
+ it "adds a Step instance with the step name and block" do
106
+ expect(instance.steps.length).to eq(1)
107
+ expect(step).to be_a(SmashTheState::Operation::Step)
108
+ expect(step.name).to eq(:new_step)
109
+ expect(step.implementation.call(%i[existing])).to eq(%i[existing step_added])
110
+ end
111
+ end
112
+
113
+ describe "#add_error_handler_for_step" do
114
+ before do
115
+ instance.add_error_handler_for_step :new_step do |state|
116
+ state.tap do
117
+ state << :handled
118
+ end
119
+ end
120
+ end
121
+
122
+ it "the error handler block is added to the named step" do
123
+ expect(step.error_handler.call([])).to eq([:handled])
124
+ end
125
+
126
+ context "error handlers for missing steps do nothing" do
127
+ before do
128
+ instance.add_error_handler_for_step :not_there do |_state|
129
+ raise "nope"
130
+ end
131
+ end
132
+
133
+ it "is allowed but does nothing" do
134
+ expect(instance.steps.map(&:name)).to eq([:new_step])
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ describe "#middleware_class" do
141
+ let!(:instance) { subject.new }
142
+
143
+ before do
144
+ instance.middleware_class_block = proc do |state|
145
+ state.to_s
146
+ end
147
+ end
148
+
149
+ context "with a middleware_class_block" do
150
+ it "runs the middleware_class_block, passes in the state, constantizes" do
151
+ expect(instance.middleware_class("String")).to eq(String)
152
+ end
153
+ end
154
+
155
+ context "with a NameError" do
156
+ it "returns nil" do
157
+ expect(instance.middleware_class("Whazzat")).to eq(nil)
158
+ end
159
+ end
160
+
161
+ context "with a NoMethodError" do
162
+ let!(:state) { double }
163
+
164
+ before do
165
+ allow(state).to receive(:to_s) do
166
+ raise NoMethodError
167
+ end
168
+ end
169
+
170
+ it "returns nil" do
171
+ expect(instance.middleware_class(state)).to eq(nil)
172
+ end
173
+ end
174
+ end
175
+
176
+ describe "#add_middleware_step" do
177
+ let!(:instance) { subject.new }
178
+
179
+ context "with a middleware class defined" do
180
+ let!(:middleware_class) do
181
+ class SequenceSpecMiddleware
182
+ def self.extra_step(state, original_state)
183
+ state.merge(original_state)
184
+ end
185
+ end
186
+ end
187
+
188
+ before do
189
+ instance.middleware_class_block = proc do |_state, _original_state|
190
+ "SequenceSpecMiddleware"
191
+ end
192
+
193
+ instance.add_step :inline_step do |_state|
194
+ { baz: "bing" }
195
+ end
196
+
197
+ instance.add_middleware_step :extra_step
198
+ end
199
+
200
+ it "block receives both the state and original state" do
201
+ instance.middleware_class_block = proc do |state, original_state|
202
+ expect(state).to eq(baz: "bing")
203
+ expect(original_state).to eq(foo: "bar")
204
+ end
205
+
206
+ instance.call(foo: "bar")
207
+ end
208
+
209
+ it "delegates the step to the middleware class" do
210
+ expect(instance.call(foo: "bar")).to eq(baz: "bing", foo: "bar")
211
+ end
212
+ end
213
+
214
+ context "with no middleware class defined" do
215
+ it "just returns the state" do
216
+ expect(instance.call(foo: "bar")).to eq(foo: "bar")
217
+ end
218
+ end
219
+ end
220
+
221
+ describe "#slice" do
222
+ let!(:instance) { subject.new }
223
+
224
+ before do
225
+ instance.add_step(:one) do |state|
226
+ state << 1
227
+ end
228
+
229
+ instance.add_step(:two) do |state|
230
+ state << 2
231
+ end
232
+
233
+ instance.add_step(:three) do |state|
234
+ state << 3
235
+ end
236
+
237
+ instance.add_step(:four) do |state|
238
+ state << 4
239
+ end
240
+
241
+ instance.add_step(:five) do |state|
242
+ state << 5
243
+ end
244
+ end
245
+
246
+ it "returns a new sequence cut to the specified length from the " \
247
+ "specified start" do
248
+ sliced = instance.slice(1, 3)
249
+ expect(sliced.steps.map(&:name)).to eq(%i[two three four])
250
+
251
+ # doesn't mutate the original
252
+ expect(instance.steps.length).to eq(5)
253
+ expect(instance.object_id).to_not eq(sliced.object_id)
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,143 @@
1
+ require "spec_helper"
2
+ require "json"
3
+
4
+ describe SmashTheState::Operation::State do
5
+ let!(:subject) { SmashTheState::Operation::State }
6
+
7
+ describe "self#build" do
8
+ let!(:built) do
9
+ subject.build do
10
+ attr_accessor :bread
11
+ end
12
+ end
13
+
14
+ it "creates a new inherited class and class_evals the block" do
15
+ expect(built < SmashTheState::Operation::State).to eq(true)
16
+ expect(built.new.respond_to?(:bread)).to eq(true)
17
+ end
18
+ end
19
+
20
+ describe "self#schema" do
21
+ jam_definition = Class.new(SmashTheState::Operation::Definition).tap do |k|
22
+ k.class_eval do
23
+ definition "Jam"
24
+
25
+ schema do
26
+ attribute :sweetened, :boolean
27
+ end
28
+ end
29
+ end
30
+
31
+ let!(:built) do
32
+ subject.build do
33
+ schema :bread do
34
+ attribute :loaves, :integer
35
+
36
+ # inline
37
+ schema :butter do
38
+ attribute :salted, :boolean
39
+ end
40
+
41
+ # by reference
42
+ schema :jam, ref: jam_definition
43
+ end
44
+ end
45
+ end
46
+
47
+ let!(:instance) do
48
+ built.new(
49
+ bread: {
50
+ loaves: 3,
51
+ butter: {
52
+ salted: true
53
+ },
54
+ jam: {
55
+ sweetened: false
56
+ }
57
+ }
58
+ )
59
+ end
60
+
61
+ it "allows for inline nesting of schemas" do
62
+ expect(instance.bread.loaves).to eq(3)
63
+ expect(instance.bread.butter.salted).to eq(true)
64
+ end
65
+
66
+ it "allows for reference of type definitions" do
67
+ expect(instance.bread.jam.sweetened).to eq(false)
68
+ end
69
+ end
70
+
71
+ describe "self#eval_validation_directives_block" do
72
+ let!(:built) do
73
+ subject.build do
74
+ attribute :bread, :string
75
+ end
76
+ end
77
+
78
+ let!(:instance) { built.new }
79
+
80
+ it "clears the validators, evals the block, runs validate" do
81
+ expect(built).to receive(:clear_validators!)
82
+
83
+ begin
84
+ SmashTheState::Operation::State.eval_validation_directives_block(instance) do
85
+ validates_presence_of :bread
86
+ end
87
+ rescue SmashTheState::Operation::State::Invalid => e
88
+ expect(e.state.errors[:bread]).to include("can't be blank")
89
+ end
90
+ end
91
+
92
+ context "when validate returns true" do
93
+ let!(:instance) { built.new(bread: "rye") }
94
+
95
+ it "returns the state" do
96
+ state = SmashTheState::Operation::State.eval_validation_directives_block(instance) do
97
+ validates_presence_of :bread
98
+ end
99
+
100
+ expect(state.bread).to eq(state.bread)
101
+ end
102
+ end
103
+ end
104
+
105
+ describe "#eval_custom_validator_block" do
106
+ let!(:built) do
107
+ subject.build do
108
+ attribute :bread, :string
109
+ end
110
+ end
111
+
112
+ let!(:instance) { built.new }
113
+
114
+ it "calls the block" do
115
+ begin
116
+ SmashTheState::Operation::State.eval_custom_validator_block(instance) do |i|
117
+ i.errors.add(:bread, "is moldy")
118
+ end
119
+ rescue SmashTheState::Operation::State::Invalid => e
120
+ expect(e.state.errors[:bread]).to include("is moldy")
121
+ end
122
+ end
123
+
124
+ it "only raises an error for the current state" do
125
+ state = SmashTheState::Operation::State.
126
+ eval_custom_validator_block(instance, built.new) do |_i, original_state|
127
+ original_state.errors.add(:bread, "is moldy")
128
+ end
129
+
130
+ expect(state).to eq(instance)
131
+ end
132
+
133
+ context "with no errors present" do
134
+ it "returns the state" do
135
+ state = SmashTheState::Operation::State.eval_custom_validator_block(instance) do
136
+ :noop
137
+ end
138
+
139
+ expect(state).to eq(instance)
140
+ end
141
+ end
142
+ end
143
+ end