statefully 0.1.3 → 0.1.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.
- 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
|