smash_the_state 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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