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.
@@ -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