statefully 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/statefully.rb +2 -0
- data/lib/statefully/change.rb +52 -0
- data/lib/statefully/diff.rb +261 -61
- data/lib/statefully/errors.rb +32 -0
- data/lib/statefully/inspect.rb +11 -0
- data/lib/statefully/state.rb +338 -20
- data/spec/diff_spec.rb +31 -23
- data/spec/errors_spec.rb +13 -0
- data/spec/spec_helper.rb +6 -4
- data/spec/state_spec.rb +58 -31
- metadata +204 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f804d2bf7a4e93cce2e444f0bf61ecbf93fcc40
|
4
|
+
data.tar.gz: 9fdf666b486671a6a45cab5e5c4bb885ffcb2baf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d4d379b599bf47f4cfb48ec9a5d7450be76e07f8debacc0c1eb18d603fe0a846d2103b69e8cc53951350e12ed17a8ea9bac4a030728365113001b1007a0c6ac2
|
7
|
+
data.tar.gz: 9ed5595f56f566543fdc0bd69d71ef917ba674d2abf49502670c3d92d1fe021780c3304d8056ca87da4681443bf112491d3b2138b098ce16eec16533ebc7a9e8
|
data/lib/statefully.rb
CHANGED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Statefully
|
2
|
+
# Change is a tuple of current and previous value of a field in a {Diff}.
|
3
|
+
class Change
|
4
|
+
# Returns the current {State} field value
|
5
|
+
#
|
6
|
+
# @return [Object] current {State} field value
|
7
|
+
# @api public
|
8
|
+
# @example
|
9
|
+
# Statefully::Change.new(current: 7, previous: 8).current
|
10
|
+
# => 7
|
11
|
+
attr_reader :current
|
12
|
+
|
13
|
+
# Returns the previous {State} field value
|
14
|
+
#
|
15
|
+
# @return [Object] previous {State} field value
|
16
|
+
# @api public
|
17
|
+
# @example
|
18
|
+
# Statefully::Change.new(current: 7, previous: 8).previous
|
19
|
+
# => 8
|
20
|
+
attr_reader :previous
|
21
|
+
|
22
|
+
# Constructor for the {Change} object
|
23
|
+
# @param current [Object] current {State} field value
|
24
|
+
# @param previous [Object] previous {State} field value
|
25
|
+
# @api public
|
26
|
+
# @example
|
27
|
+
# Statefully::Change.new(current: 7, previous: 8)
|
28
|
+
# => #<Statefully::Change current=7, previous=8>
|
29
|
+
def initialize(current:, previous:)
|
30
|
+
@current = current
|
31
|
+
@previous = previous
|
32
|
+
end
|
33
|
+
|
34
|
+
# Internal-only method used to determine whether there was any change
|
35
|
+
# @api private
|
36
|
+
def none?
|
37
|
+
@current == @previous
|
38
|
+
end
|
39
|
+
|
40
|
+
# Human-readable representation of the {Change} for console inspection
|
41
|
+
#
|
42
|
+
# @return [String]
|
43
|
+
# @api semipublic
|
44
|
+
# @example
|
45
|
+
# Statefully::Change.new(current: 7, previous: 8)
|
46
|
+
# => #<Statefully::Change current=7, previous=8>
|
47
|
+
def inspect
|
48
|
+
"#<#{self.class.name} " \
|
49
|
+
"#{Inspect.from_fields(current: current, previous: previous)}>"
|
50
|
+
end
|
51
|
+
end # class Change
|
52
|
+
end # module Statefully
|
data/lib/statefully/diff.rb
CHANGED
@@ -2,124 +2,299 @@ require 'set'
|
|
2
2
|
require 'singleton'
|
3
3
|
|
4
4
|
module Statefully
|
5
|
-
|
5
|
+
# {Diff} is a difference between two neighboring instances of {State}.
|
6
|
+
#
|
7
|
+
# @abstract
|
8
|
+
class Diff
|
9
|
+
# Create is the only public interface to the Diff class
|
10
|
+
#
|
11
|
+
# @param current [Statefully::State] current state
|
12
|
+
# @param previous [Statefully::State] previous state
|
13
|
+
#
|
14
|
+
# @return [Statefully::Diff] Difference between states.
|
15
|
+
# @api public
|
16
|
+
# @example
|
17
|
+
# previous = Statefully::State.create
|
18
|
+
# current = previus.succeed(key: 'val')
|
19
|
+
# Statefully::Diff.create(current, previous)
|
20
|
+
# => #<Statefully::Diff::Changed added={key: "val"}>
|
21
|
+
#
|
6
22
|
# This method reeks of :reek:FeatureEnvy (of current).
|
7
|
-
def create(current
|
23
|
+
def self.create(current:, previous:)
|
8
24
|
return current.diff if current.failed? || current.finished?
|
9
|
-
changes = Builder.new(current, previous).build
|
25
|
+
changes = Builder.new(current: current, previous: previous).build
|
26
|
+
return Created.new(**changes).freeze if previous.none?
|
10
27
|
changes.empty? ? Unchanged.instance : Changed.new(**changes).freeze
|
11
28
|
end
|
12
|
-
module_function :create
|
13
29
|
|
14
|
-
|
15
|
-
|
30
|
+
# Check if a {Diff} is empty
|
31
|
+
#
|
32
|
+
# An empty {Diff} means that is there are no changes in properties between
|
33
|
+
# current and previous {State}.
|
34
|
+
#
|
35
|
+
# @return [Boolean]
|
36
|
+
# @api public
|
37
|
+
# @example
|
38
|
+
# Statefully::Diff::Unchanged.instance.empty?
|
39
|
+
# => true
|
40
|
+
def empty?
|
41
|
+
true
|
42
|
+
end
|
16
43
|
|
17
|
-
|
18
|
-
|
44
|
+
# Hash of added properties and their values
|
45
|
+
#
|
46
|
+
# @return [Hash<Symbol, Object>]
|
47
|
+
# @api public
|
48
|
+
# @example
|
49
|
+
# Statefully::Diff::Unchanged.instance.added
|
50
|
+
# => {}
|
51
|
+
def added
|
52
|
+
{}
|
53
|
+
end
|
54
|
+
|
55
|
+
# Hash of changed properties and their current and previous values
|
56
|
+
#
|
57
|
+
# @return [Hash<Symbol, Statefully::Diff::Change>]
|
58
|
+
# @api public
|
59
|
+
# @example
|
60
|
+
# Statefully::Diff::Unchanged.instance.added.changed
|
61
|
+
# => {}
|
62
|
+
def changed
|
63
|
+
{}
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check if a key has been added
|
67
|
+
#
|
68
|
+
# @param key [Symbol]
|
69
|
+
# @return [Boolean]
|
70
|
+
# @api public
|
71
|
+
# @example
|
72
|
+
# diff = Statefully::Diff::Changed.new(added: {key: 7})
|
73
|
+
# diff.added?(:key)
|
74
|
+
# => true
|
75
|
+
# diff.added?(:other)
|
76
|
+
# => false
|
77
|
+
def added?(key)
|
78
|
+
added.key?(key)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Check if a key has been changed
|
82
|
+
#
|
83
|
+
# @param key [Symbol]
|
84
|
+
# @return [Boolean]
|
85
|
+
# @api public
|
86
|
+
# @example
|
87
|
+
# diff = Statefully::Diff::Changed.new(
|
88
|
+
# changed: {key: Statefully::Change.new(current: 7, previous: 8)},
|
89
|
+
# )
|
90
|
+
# diff.changed?(:key)
|
91
|
+
# => true
|
92
|
+
# diff.changed?(:other)
|
93
|
+
# => false
|
94
|
+
def changed?(key)
|
95
|
+
changed.key?(key)
|
96
|
+
end
|
97
|
+
|
98
|
+
# {Changed} is a {Diff} which contains changes between two successful
|
99
|
+
# {State}s.
|
100
|
+
class Changed < Diff
|
101
|
+
# Hash of added properties and their values
|
102
|
+
#
|
103
|
+
# @return [Hash<Symbol, Object>]
|
104
|
+
# @api public
|
105
|
+
# @example
|
106
|
+
# Statefully::Diff::Changed.new(added: {key: 7}).added
|
107
|
+
# => {:key => 7}
|
108
|
+
attr_reader :added
|
109
|
+
|
110
|
+
# Hash of changed properties and their current and previous values
|
111
|
+
#
|
112
|
+
# @return [Hash<Symbol, Change>]
|
113
|
+
# @api public
|
114
|
+
# @example
|
115
|
+
# Statefully::Diff::Changed.new(
|
116
|
+
# changed: {key: Statefully::Change.new(current: 7, previous: 8)},
|
117
|
+
# )
|
118
|
+
# => {:key=>#<Statefully::Change current=7, previous=8>}
|
119
|
+
attr_reader :changed
|
120
|
+
|
121
|
+
# Constructor for {Diff::Changed}
|
122
|
+
#
|
123
|
+
# @param added [Hash<Symbol, Object>] added fields
|
124
|
+
# @param changed [Hash<Symbol, Change>] [changed fields]
|
125
|
+
# @api public
|
126
|
+
# @example
|
127
|
+
# Statefully::Diff::Changed.new(added: {key: 7})
|
128
|
+
# => #<Statefully::Diff::Changed added={key: 7}>
|
129
|
+
def initialize(added: {}, changed: {})
|
130
|
+
@added = added.freeze
|
131
|
+
@changed = changed.freeze
|
19
132
|
end
|
20
133
|
|
21
|
-
|
22
|
-
|
134
|
+
# Check if a {Diff} resulted from creating a {State}
|
135
|
+
#
|
136
|
+
# @return [Boolean]
|
137
|
+
# @api public
|
138
|
+
# @example
|
139
|
+
# Stateful::State.created.created?
|
140
|
+
# => true
|
141
|
+
#
|
142
|
+
# Stateful::State.created.succeed.created?
|
143
|
+
# => false
|
144
|
+
def created?
|
145
|
+
false
|
23
146
|
end
|
24
147
|
|
25
|
-
|
26
|
-
|
148
|
+
# Check if a {Diff} is empty
|
149
|
+
#
|
150
|
+
# An empty {Diff} means that there are no changes in properties between
|
151
|
+
# current and previous {State}.
|
152
|
+
#
|
153
|
+
# @return [Boolean]
|
154
|
+
# @api public
|
155
|
+
# @example
|
156
|
+
# Statefully::Diff::Changed.new(added: {key: 7}).empty?
|
157
|
+
# => false
|
158
|
+
def empty?
|
159
|
+
added.empty? && changed.empty?
|
27
160
|
end
|
28
161
|
|
29
|
-
|
30
|
-
|
162
|
+
# Human-readable representation of the {Change} for console inspection
|
163
|
+
#
|
164
|
+
# @return [String]
|
165
|
+
# @api semipublic
|
166
|
+
# @example
|
167
|
+
# Statefully::Diff::Changed.new(added: {key: 7})
|
168
|
+
# => #<Statefully::Diff::Changed added={key: 7}>
|
169
|
+
def inspect
|
170
|
+
details = [self.class.name]
|
171
|
+
details << inspect_details unless empty?
|
172
|
+
"#<#{details.join(' ')}>"
|
31
173
|
end
|
32
174
|
|
33
175
|
private
|
34
176
|
|
177
|
+
# Helper method to print out added and changed fields
|
178
|
+
# @return [String]
|
179
|
+
# @api private
|
35
180
|
def inspect_details
|
36
181
|
[inspect_added, inspect_changed].compact.join(', ')
|
37
182
|
end
|
38
183
|
|
184
|
+
# Helper method to print out added fields
|
185
|
+
# @return [String]
|
186
|
+
# @api private
|
39
187
|
def inspect_added
|
40
188
|
added.empty? ? nil : "added=#{Inspect.from_hash(added)}"
|
41
189
|
end
|
42
190
|
|
191
|
+
# Helper method to print out changed fields
|
192
|
+
# @return [String]
|
193
|
+
# @api private
|
43
194
|
def inspect_changed
|
44
195
|
changed.empty? ? nil : "changed=#{Inspect.from_hash(changed)}"
|
45
196
|
end
|
46
|
-
|
47
|
-
def initialize(added:, changed:)
|
48
|
-
@added = added.freeze
|
49
|
-
@changed = changed.freeze
|
50
|
-
end
|
51
197
|
end # class Changed
|
52
198
|
|
53
|
-
module
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
def
|
59
|
-
{}
|
199
|
+
module SingletonInspect
|
200
|
+
# Human-readable representation of the {Diff} singleton
|
201
|
+
#
|
202
|
+
# @return [String]
|
203
|
+
# @api private
|
204
|
+
def inspect
|
205
|
+
"#<#{self.class.name}>"
|
60
206
|
end
|
61
|
-
|
62
|
-
|
63
|
-
|
207
|
+
end # module SingletonInspect
|
208
|
+
private_constant :SingletonInspect
|
209
|
+
|
210
|
+
# {Created} represents a difference between a null and non-null {State}.
|
211
|
+
class Created < Changed
|
212
|
+
# Check if a {Diff} resulted from creating a {State}
|
213
|
+
#
|
214
|
+
# @return [Boolean]
|
215
|
+
# @api public
|
216
|
+
# @example
|
217
|
+
# Stateful::State.created.created?
|
218
|
+
# => true
|
219
|
+
#
|
220
|
+
# Stateful::State.created.succeed.created?
|
221
|
+
# => false
|
222
|
+
def created?
|
223
|
+
true
|
64
224
|
end
|
65
|
-
end #
|
66
|
-
private_constant :NoChanges
|
225
|
+
end # class Created
|
67
226
|
|
68
|
-
|
227
|
+
# {Unchanged} represents a lack of difference between two {State}s.
|
228
|
+
class Unchanged < Diff
|
69
229
|
include Singleton
|
70
|
-
include
|
71
|
-
|
72
|
-
def inspect
|
73
|
-
"#<#{self.class.name}>"
|
74
|
-
end
|
230
|
+
include SingletonInspect
|
75
231
|
end # class Unchanged
|
76
232
|
|
77
|
-
|
78
|
-
|
233
|
+
# {Failed} represents a difference between a succesful and failed {State}.
|
234
|
+
class Failed < Diff
|
235
|
+
# Error that caused the {State} to fail
|
236
|
+
#
|
237
|
+
# @return [StandardError]
|
238
|
+
# @api public
|
239
|
+
# @example
|
240
|
+
# Statefully::Diff::Failed.new(RuntimeError.new('Boom!')).error
|
241
|
+
# => #<RuntimeError: Boom!>
|
79
242
|
attr_reader :error
|
80
243
|
|
244
|
+
# Constructor for {Diff::Failed}
|
245
|
+
#
|
246
|
+
# @param error [StandardError] error that caused the {State} to fail
|
247
|
+
# @api semipublic
|
248
|
+
# @example
|
249
|
+
# Statefully::Diff::Failed.new(RuntimeError.new('Boom!'))
|
250
|
+
# => #<Statefully::Diff::Failed error=#<RuntimeError: Boom!>>
|
81
251
|
def initialize(error)
|
82
252
|
@error = error
|
83
253
|
end
|
84
254
|
|
255
|
+
# Human-readable representation of the {Diff::Failed}
|
256
|
+
#
|
257
|
+
# @return [String]
|
258
|
+
# @api semipublic
|
259
|
+
# @example
|
260
|
+
# Statefully::Diff::Failed.new(RuntimeError.new('Boom!'))
|
261
|
+
# => #<Statefully::Diff::Failed error=#<RuntimeError: Boom!>>
|
85
262
|
def inspect
|
86
263
|
"#<#{self.class.name} error=#{error.inspect}>"
|
87
264
|
end
|
88
265
|
end # class Failed
|
89
266
|
|
90
|
-
|
267
|
+
# {Failed} represents a difference between a succesful and finished {State}.
|
268
|
+
class Finished < Diff
|
269
|
+
include Singleton
|
270
|
+
include SingletonInspect
|
91
271
|
end # class Finished
|
92
272
|
|
93
|
-
class Change
|
94
|
-
attr_reader :current, :previous
|
95
|
-
|
96
|
-
def initialize(current, previous)
|
97
|
-
@current = current
|
98
|
-
@previous = previous
|
99
|
-
end
|
100
|
-
|
101
|
-
def none?
|
102
|
-
@current == @previous
|
103
|
-
end
|
104
|
-
|
105
|
-
def inspect
|
106
|
-
"#<#{self.class.name} " \
|
107
|
-
"#{Inspect.from_fields(current: current, previous: previous)}>"
|
108
|
-
end
|
109
|
-
end # class Change
|
110
|
-
|
111
273
|
class Builder
|
112
|
-
|
274
|
+
# Constructor for the {Builder} object
|
275
|
+
#
|
276
|
+
# @param current [State] current {State}
|
277
|
+
# @param previous [State] previous {State}
|
278
|
+
# @api private
|
279
|
+
def initialize(current:, previous:)
|
113
280
|
@current = current
|
114
281
|
@previous = previous
|
115
282
|
end
|
116
283
|
|
284
|
+
# Build a Hash of added and changed {State} fields
|
285
|
+
#
|
286
|
+
# @return [Hash]
|
287
|
+
# @api private
|
117
288
|
def build
|
118
289
|
empty? ? {} : { added: added, changed: changed }
|
119
290
|
end
|
120
291
|
|
121
292
|
private
|
122
293
|
|
294
|
+
# List added fields
|
295
|
+
#
|
296
|
+
# @return [Hash]
|
297
|
+
# @api private
|
123
298
|
def added
|
124
299
|
@added ||=
|
125
300
|
(current_keys - previous_keys)
|
@@ -127,6 +302,10 @@ module Statefully
|
|
127
302
|
.to_h
|
128
303
|
end
|
129
304
|
|
305
|
+
# List changed fields
|
306
|
+
#
|
307
|
+
# @return [Hash]
|
308
|
+
# @api private
|
130
309
|
def changed
|
131
310
|
@changed ||=
|
132
311
|
(current_keys & previous_keys)
|
@@ -135,22 +314,43 @@ module Statefully
|
|
135
314
|
.reject { |_, val| val.none? }
|
136
315
|
end
|
137
316
|
|
317
|
+
# Change for individual key
|
318
|
+
#
|
319
|
+
# @param [Symbol] key name
|
320
|
+
#
|
321
|
+
# @return [Change]
|
322
|
+
# @api private
|
138
323
|
def change_for(key)
|
139
|
-
Change.new(
|
324
|
+
Change.new(
|
325
|
+
current: @current.fetch(key),
|
326
|
+
previous: @previous.fetch(key),
|
327
|
+
).freeze
|
140
328
|
end
|
141
329
|
|
330
|
+
# Check if the nothing has changed
|
331
|
+
#
|
332
|
+
# @return [Boolean]
|
333
|
+
# @api private
|
142
334
|
def empty?
|
143
335
|
added.empty? && changed.empty?
|
144
336
|
end
|
145
337
|
|
338
|
+
# Return the set of keys for the current {State}
|
339
|
+
#
|
340
|
+
# @return [Set<Symbol>]
|
341
|
+
# @api private
|
146
342
|
def current_keys
|
147
343
|
Set.new(@current.keys)
|
148
344
|
end
|
149
345
|
|
346
|
+
# Return the set of keys for previous {State}
|
347
|
+
#
|
348
|
+
# @return [Set<Symbol>]
|
349
|
+
# @api private
|
150
350
|
def previous_keys
|
151
351
|
Set.new(@previous.keys)
|
152
352
|
end
|
153
353
|
end # class Builder
|
154
354
|
private_constant :Builder
|
155
|
-
end #
|
355
|
+
end # class Diff
|
156
356
|
end # module Statefully
|