statefully 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
1
+ # rubocop:disable Metrics/LineLength
2
+ module Statefully
3
+ module Errors
4
+ # StateMissing represents an error being thrown when a member of {State} is
5
+ # being accessed using an unsafe accessor (eg. #member!). It is technically
6
+ # a NoMethodError, but it is introduced to allow users to differentiate
7
+ # between failing state accessors and other code that may fail in a similar
8
+ # way.
9
+ class StateMissing < ::RuntimeError
10
+ # Stores the name of the missing {State} field
11
+ #
12
+ # @return [Symbol] the name of the field.
13
+ # @api public
14
+ # @example
15
+ # Statefully::Errors::StateMissing.new(:bacon).field
16
+ # => :bacon
17
+ attr_reader :field
18
+
19
+ # Error constructor for {StateMissing}
20
+ #
21
+ # @param field [Symbol] name of the missing field.
22
+ # @api public
23
+ # @example
24
+ # Statefully::Errors::StateMissing.new(:bacon)
25
+ # => #<Statefully::Errors::StateMissing: field 'bacon' missing from state>
26
+ def initialize(field)
27
+ @field = field
28
+ super("field '#{field}' missing from state")
29
+ end
30
+ end # class StateMissing
31
+ end # module Errors
32
+ end # module Statefully
@@ -1,10 +1,21 @@
1
1
  module Statefully
2
+ # {Inspect} provides helpers for human-readable object inspection.
2
3
  module Inspect
4
+ # Inspect a [Hash] of values in `key: val` format
5
+ # @param input [Hash] input values
6
+ #
7
+ # @return [String]
8
+ # @api private
3
9
  def from_hash(input)
4
10
  '{' + input.map { |key, val| "#{key}: #{val.inspect}" }.join(', ') + '}'
5
11
  end
6
12
  module_function :from_hash
7
13
 
14
+ # Inspect a [Hash] of values in `key=val` format
15
+ # @param input [Hash] input values
16
+ #
17
+ # @return [String]
18
+ # @api private
8
19
  def from_fields(input)
9
20
  input.map { |key, val| "#{key}=#{val.inspect}" }.join(', ')
10
21
  end
@@ -1,56 +1,214 @@
1
1
  require 'forwardable'
2
2
  require 'singleton'
3
3
 
4
+ # rubocop:disable Metrics/LineLength
4
5
  module Statefully
6
+ # {State} is an immutable collection of fields with some convenience methods.
7
+ # @abstract
5
8
  class State
6
9
  include Enumerable
7
10
  extend Forwardable
8
11
 
12
+ # Return the previous {State}
13
+ #
14
+ # @return [State]
15
+ # @api public
16
+ # @example
17
+ # Statefully::State.create.previous
18
+ # => #<Statefully::State::None>
19
+ #
20
+ # Statefully::State.create.succeed.previous
21
+ # => #<Statefully::State::Success>
9
22
  attr_reader :previous
10
- def_delegators :@_members, :each, :key?, :keys, :fetch
11
23
 
24
+ # @!method each
25
+ # @return [Enumerator]
26
+ # @see https://docs.ruby-lang.org/en/2.0.0/Hash.html#method-i-each Hash#each
27
+ # @api public
28
+ # @example
29
+ # Statefully::State.create(key: 'val').each { |key, val| puts("#{key} => #{val}") }
30
+ # key => val
31
+ # @!method fetch
32
+ # @return [Object]
33
+ # @see https://docs.ruby-lang.org/en/2.0.0/Hash.html#method-i-fetch Hash#fetch
34
+ # @api public
35
+ # @example
36
+ # Statefully::State.create(key: 'val').fetch(:key)
37
+ # => 'val'
38
+ # @!method key?
39
+ # @return [Boolean]
40
+ # @see https://docs.ruby-lang.org/en/2.0.0/Hash.html#method-i-key-3F Hash#key?
41
+ # @api public
42
+ # @example
43
+ # state = Statefully::State.create(key: 'val')
44
+ # state.key?(:key)
45
+ # => true
46
+ # state.key?(:other)
47
+ # => false
48
+ # @!method keys
49
+ # @return [Array<Symbol>]
50
+ # @see https://docs.ruby-lang.org/en/2.0.0/Hash.html#method-i-keys Hash#keys
51
+ # @api public
52
+ # @example
53
+ # Statefully::State.create(key: 'val').keys
54
+ # => [:key]
55
+ def_delegators :@_members, :each, :fetch, :key?, :keys
56
+
57
+ # Create an instance of {State} object
58
+ #
59
+ # This is meant as the only valid way of creating {State} objects.
60
+ #
61
+ # @param values [Hash<Symbol, Object>] keyword arguments
62
+ #
63
+ # @return [type] [description]
64
+ # @api public
65
+ # @example
66
+ # Statefully::State.create(key: 'val')
67
+ # => #<Statefully::State::Success key="val">
12
68
  def self.create(**values)
13
69
  Success.send(:new, values, previous: None.instance).freeze
14
70
  end
15
71
 
72
+ # Return a {Diff} between current and previous {State}
73
+ #
74
+ # @return [Diff]
75
+ # @api public
76
+ # @example
77
+ # Statefully::State.create.succeed(key: 'val').diff
78
+ # => #<Statefully::Diff::Changed added={key: "val"}>
16
79
  def diff
17
- Diff.create(self, previous)
80
+ Diff.create(current: self, previous: previous)
18
81
  end
19
82
 
83
+ # Return all historical changes to this {State}
84
+ #
85
+ # @return [Array<Diff>]
86
+ # @api public
87
+ # @example
88
+ # Statefully::State.create.succeed(key: 'val').history
89
+ # => [#<Statefully::Diff::Changed added={key: "val"}>, #<Statefully::Diff::Created>]
20
90
  def history
21
91
  ([diff] + previous.history).freeze
22
92
  end
23
93
 
94
+ # Check if the current {State} is successful
95
+ #
96
+ # @return [Boolean]
97
+ # @api public
98
+ # @example
99
+ # state = Statefully::State.create
100
+ # state.successful?
101
+ # => true
102
+ #
103
+ # state.fail(RuntimeError.new('Boom!')).successful?
104
+ # => false
24
105
  def successful?
25
106
  true
26
107
  end
27
108
 
109
+ # Check if the current {State} is failed
110
+ #
111
+ # @return [Boolean]
112
+ # @api public
113
+ # @example
114
+ # state = Statefully::State.create
115
+ # state.failed?
116
+ # => false
117
+ #
118
+ # state.fail(RuntimeError.new('Boom!')).failed?
119
+ # => true
28
120
  def failed?
29
121
  !successful?
30
122
  end
31
123
 
124
+ # Check if the current {State} is finished
125
+ #
126
+ # @return [Boolean]
127
+ # @api public
128
+ # @example
129
+ # state = Statefully::State.create
130
+ # state.finished?
131
+ # => false
132
+ #
133
+ # state.finish.finished?
134
+ # => true
32
135
  def finished?
33
136
  false
34
137
  end
35
138
 
139
+ # Check if the current {State} is none (a null-object of {State})
140
+ #
141
+ # @return [Boolean]
142
+ # @api public
143
+ # @example
144
+ # state = Statefully::State.create
145
+ # state.none?
146
+ # => false
147
+ #
148
+ # state.previous.none?
149
+ # => true
150
+ def none?
151
+ false
152
+ end
153
+
154
+ # Resolve the current {State}
155
+ #
156
+ # Resolving will return the current {State} if successful, but raise an
157
+ # error wrapped in a {State::Failure}. This is a convenience method inspired
158
+ # by monadic composition from functional languages.
159
+ #
160
+ # @return [State] if the receiver is {#successful?}
161
+ # @raise [StandardError] if the receiver is {#failed?}
162
+ # @api public
163
+ # @example
164
+ # Statefully::State.create(key: 'val').resolve
165
+ # => #<Statefully::State::Success key="val">
166
+ #
167
+ # Statefully::State.create.fail(RuntimeError.new('Boom!')).resolve
168
+ # RuntimeError: Boom!
169
+ # [STACK TRACE]
36
170
  def resolve
37
171
  self
38
172
  end
39
173
 
174
+ # Show the current {State} in a human-readable form
175
+ #
176
+ # @return [String]
177
+ # @api public
178
+ # @example
179
+ # Statefully::State.create(key: 'val')
180
+ # => #<Statefully::State::Success key="val">
40
181
  def inspect
41
182
  _inspect_details({})
42
183
  end
43
184
 
44
185
  private
45
186
 
187
+ # State fields
188
+ #
189
+ # @return [Hash]
190
+ # @api private
46
191
  attr_reader :_members
47
192
 
193
+ # Constructor for the {State} object
194
+ #
195
+ # @param values [Hash<Symbol, Object>] values to store
196
+ # @param previous [State] previous {State}
197
+ #
198
+ # @return [State]
199
+ # @api private
48
200
  def initialize(values, previous:)
49
201
  @_members = values.freeze
50
202
  @previous = previous
51
203
  end
52
204
  private_class_method :new
53
205
 
206
+ # Inspect {State} fields, with extras
207
+ #
208
+ # @param extras [Hash] Non-member values to include
209
+ #
210
+ # @return [String]
211
+ # @api private
54
212
  def _inspect_details(extras)
55
213
  details = [self.class.name]
56
214
  fields = _members.merge(extras)
@@ -58,7 +216,47 @@ module Statefully
58
216
  "#<#{details.join(' ')}>"
59
217
  end
60
218
 
219
+ # Dynamically pass unknown messages to the underlying state storage
220
+ #
221
+ # State fields become accessible through readers, like in an
222
+ # {http://ruby-doc.org/stdlib-2.0.0/libdoc/ostruct/rdoc/OpenStruct.html OpenStruct}.
223
+ # A single state field can be questioned for existence by having its name
224
+ # followed by a question mark - eg. bacon?.
225
+ # A single state field can be force-accessed by having its name followed by
226
+ # an exclamation mark - eg. bacon!.
227
+ #
61
228
  # This method reeks of :reek:TooManyStatements.
229
+ #
230
+ # @param name [Symbol|String]
231
+ # @param args [Array<Object>]
232
+ # @param block [Proc]
233
+ #
234
+ # @return [Object]
235
+ # @raise [NoMethodError]
236
+ # @raise [Errors::StateMissing]
237
+ # @api private
238
+ # @example
239
+ # state = Statefully::State.create(bacon: 'tasty')
240
+ #
241
+ # state.bacon
242
+ # => "tasty"
243
+ #
244
+ # state.bacon?
245
+ # => true
246
+ #
247
+ # state.bacon!
248
+ # => "tasty"
249
+ #
250
+ # state.cabbage
251
+ # NoMethodError: undefined method `cabbage' for #<Statefully::State::Success bacon="tasty">
252
+ # [STACK TRACE]
253
+ #
254
+ # state.cabbage?
255
+ # => false
256
+ #
257
+ # state.cabbage!
258
+ # Statefully::Errors::StateMissing: field 'cabbage' missing from state
259
+ # [STACK TRACE]
62
260
  def method_missing(name, *args, &block)
63
261
  sym_name = name.to_sym
64
262
  return fetch(sym_name) if key?(sym_name)
@@ -69,95 +267,215 @@ module Statefully
69
267
  known = key?(base)
70
268
  return known if modifier == '?'
71
269
  return fetch(base) if known
72
- raise Missing, base
270
+ raise Errors::StateMissing, base
73
271
  end
74
272
 
273
+ # Companion to `method_missing`
274
+ #
75
275
  # This method reeks of :reek:BooleanParameter.
276
+ #
277
+ # @param name [Symbol|String]
278
+ # @param _include_private [Boolean]
279
+ #
280
+ # @return [Boolean]
281
+ # @api private
76
282
  def respond_to_missing?(name, _include_private = false)
77
283
  str_name = name.to_s
78
284
  key?(name.to_sym) || %w[? !].any?(&str_name.method(:end_with?)) || super
79
285
  end
80
286
 
81
- class Missing < RuntimeError
82
- attr_reader :field
83
-
84
- def initialize(field)
85
- @field = field
86
- super("field '#{field}' missing from state")
87
- end
88
- end # class Missing
89
-
287
+ # {None} is a null-value of {State}
90
288
  class None < State
91
289
  include Singleton
92
290
 
291
+ # Return all historical changes to this {State}
292
+ #
293
+ # @return [Array<Diff>]
294
+ # @api public
295
+ # @example
296
+ # Statefully::State.create.succeed(key: 'val').history
297
+ # => [#<Statefully::Diff::Changed added={key: "val"}>, #<Statefully::Diff::Created>]
93
298
  def history
94
299
  []
95
300
  end
96
301
 
302
+ # Check if the current {State} is none (a null-object of {State})
303
+ #
304
+ # @return [Boolean]
305
+ # @api public
306
+ # @example
307
+ # state = Statefully::State.create
308
+ # state.none?
309
+ # => false
310
+ #
311
+ # state.previous.none?
312
+ # => true
313
+ def none?
314
+ true
315
+ end
316
+
97
317
  private
98
318
 
319
+ # Constructor for the {None} object
320
+ # @api private
99
321
  def initialize
100
322
  @_members = {}.freeze
101
323
  @previous = self
102
324
  end
103
325
  end # class None
104
- private_constant :None
105
326
 
106
- # Success is a not-yet failed State.
327
+ # {Success} is a not-yet failed {State}.
107
328
  class Success < State
329
+ # Return the next, successful {State} with new values merged in (if any)
330
+ #
331
+ # @param values [Hash<Symbol, Object>] New values of the {State}
332
+ #
333
+ # @return [State::Success] new successful {State}
334
+ # @api public
335
+ # @example
336
+ # Statefully::State.create.succeed(key: 'val')
337
+ # => #<Statefully::State::Success key="val">
108
338
  def succeed(**values)
109
339
  self.class.send(:new, _members.merge(values).freeze, previous: self)
110
340
  end
111
341
 
342
+ # Return the next, failed {State} with a stored error
343
+ #
344
+ # @param error [StandardError] error to store
345
+ #
346
+ # @return [State::Failure] new failed {State}
347
+ # @api public
348
+ # @example
349
+ # Statefully::State.create(key: 'val').fail(RuntimeError.new('Boom!'))
350
+ # => #<Statefully::State::Failure key="val", error="#<RuntimeError: Boom!>">
112
351
  def fail(error)
113
352
  Failure.send(:new, _members, error, previous: self).freeze
114
353
  end
115
354
 
355
+ # Return the next, finished? {State}
356
+ #
357
+ # @return [State::State] new finished {State}
358
+ # @api public
359
+ # @example
360
+ # Statefully::State.create(key: 'val').finish
361
+ # => #<Statefully::State::Finished key="val">
116
362
  def finish
117
363
  Finished.send(:new, _members, previous: self).freeze
118
364
  end
119
365
  end # class Success
120
- private_constant :Success
121
366
 
122
- # Failure is a failed State.
367
+ # {Failure} is a failed {State}.
123
368
  class Failure < State
369
+ # Error stored in the current {State}
370
+ #
371
+ # @return [StandardError]
372
+ # @api public
373
+ # @example
374
+ # state = Statefully::State.create(key: 'val').fail(RuntimeError.new('Boom!'))
375
+ # state.error
376
+ # => #<RuntimeError: Boom!>
124
377
  attr_reader :error
125
378
 
379
+ # Constructor for the {Failure} object
380
+ #
381
+ # @param values [Hash<Symbol, Object>] fields to be stored
382
+ # @param error [StandardError] error to be wrapped
383
+ # @param previous [State] previous state
384
+ # @api private
126
385
  def initialize(values, error, previous:)
127
386
  super(values, previous: previous)
128
387
  @error = error
129
388
  end
130
389
 
390
+ # Return a {Diff} between current and previous {State}
391
+ #
392
+ # @return [Diff::Failed]
393
+ # @api public
394
+ # @example
395
+ # state = Statefully::State.create(key: 'val').fail(RuntimeError.new('Boom!'))
396
+ # state.diff
397
+ # => #<Statefully::Diff::Failed error=#<RuntimeError: Boom!>>
131
398
  def diff
132
399
  Diff::Failed.new(error).freeze
133
400
  end
134
401
 
402
+ # Check if the current {State} is successful
403
+ #
404
+ # @return [Boolean]
405
+ # @api public
406
+ # @example
407
+ # state = Statefully::State.create
408
+ # state.successful?
409
+ # => true
410
+ #
411
+ # state.fail(RuntimeError.new('Boom!')).successful?
412
+ # => false
135
413
  def successful?
136
414
  false
137
415
  end
138
416
 
417
+ # Resolve the current {State}
418
+ #
419
+ # Resolving will return the current {State} if successful, but raise an
420
+ # error wrapped in a {State::Failure}. This is a convenience method inspired
421
+ # by monadic composition from functional languages.
422
+ #
423
+ # @return [State] if the receiver is {#successful?}
424
+ # @raise [StandardError] if the receiver is {#failed?}
425
+ # @api public
426
+ # @example
427
+ # Statefully::State.create(key: 'val').resolve
428
+ # => #<Statefully::State::Success key="val">
429
+ #
430
+ # Statefully::State.create.fail(RuntimeError.new('Boom!')).resolve
431
+ # RuntimeError: Boom!
432
+ # [STACK TRACE]
139
433
  def resolve
140
434
  raise error
141
435
  end
142
436
 
437
+ # Show the current {State} in a human-readable form
438
+ #
439
+ # @return [String]
440
+ # @api public
441
+ # @example
442
+ # Statefully::State.create.fail(RuntimeError.new('Boom!'))
443
+ # => #<Statefully::State::Failure error="#<RuntimeError: Boom!>">
143
444
  def inspect
144
445
  _inspect_details(error: error.inspect)
145
446
  end
146
447
  end # class Failure
147
- private_constant :Failure
148
448
 
149
- # Finished state is a state which is successful, but should not be processed
150
- # any further. This could be useful for things like early returns.
449
+ # {Finished} state is a state which is successful, but should not be
450
+ # processed any further. This could be useful for things like early returns.
151
451
  class Finished < State
452
+ # Return a {Diff} between current and previous {State}
453
+ #
152
454
  # This method reeks of :reek:UtilityFunction - just implementing an API.
455
+ #
456
+ # @return [Diff::Finished]
457
+ # @api public
458
+ # @example
459
+ # Statefully::State.create(key: 'val').finish.diff
460
+ # => #<Statefully::Diff::Finished>
153
461
  def diff
154
462
  Diff::Finished.instance
155
463
  end
156
464
 
465
+ # Check if the current {State} is finished
466
+ #
467
+ # @return [Boolean]
468
+ # @api public
469
+ # @example
470
+ # state = Statefully::State.create
471
+ # state.finished?
472
+ # => false
473
+ #
474
+ # state.finish.finished?
475
+ # => true
157
476
  def finished?
158
477
  true
159
478
  end
160
479
  end # class Finished
161
- private_constant :Finished
162
480
  end # class State
163
481
  end # module Statefully