hanami-utils 2.0.0.alpha6 → 2.0.0.beta1

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.
@@ -1,619 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "hanami/utils/basic_object"
4
- require "hanami/utils/class_attribute"
5
- require "hanami/utils/hash"
6
-
7
- module Hanami
8
- # Hanami Interactor
9
- #
10
- # @since 0.3.5
11
- module Interactor
12
- # Result of an operation
13
- #
14
- # @since 0.3.5
15
- class Result < Utils::BasicObject
16
- # Concrete methods
17
- #
18
- # @since 0.3.5
19
- # @api private
20
- #
21
- # @see Hanami::Interactor::Result#respond_to_missing?
22
- METHODS = ::Hash[initialize: true,
23
- success?: true,
24
- successful?: true,
25
- failure?: true,
26
- fail!: true,
27
- prepare!: true,
28
- errors: true,
29
- error: true].freeze
30
-
31
- # Initialize a new result
32
- #
33
- # @param payload [Hash] a payload to carry on
34
- #
35
- # @return [Hanami::Interactor::Result]
36
- #
37
- # @since 0.3.5
38
- # @api private
39
- def initialize(payload = {})
40
- @payload = payload
41
- @errors = []
42
- @success = true
43
- end
44
-
45
- # Checks if the current status is successful
46
- #
47
- # @return [TrueClass,FalseClass] the result of the check
48
- #
49
- # @since 0.8.1
50
- def successful?
51
- @success && errors.empty?
52
- end
53
-
54
- # @since 0.3.5
55
- alias_method :success?, :successful?
56
-
57
- # Checks if the current status is not successful
58
- #
59
- # @return [TrueClass,FalseClass] the result of the check
60
- #
61
- # @since 0.9.2
62
- def failure?
63
- !successful?
64
- end
65
-
66
- # Forces the status to be a failure
67
- #
68
- # @since 0.3.5
69
- def fail!
70
- @success = false
71
- end
72
-
73
- # Returns all the errors collected during an operation
74
- #
75
- # @return [Array] the errors
76
- #
77
- # @since 0.3.5
78
- #
79
- # @see Hanami::Interactor::Result#error
80
- # @see Hanami::Interactor#call
81
- # @see Hanami::Interactor#error
82
- # @see Hanami::Interactor#error!
83
- def errors
84
- @errors.dup
85
- end
86
-
87
- # @since 0.5.0
88
- # @api private
89
- def add_error(*errors)
90
- @errors << errors
91
- @errors.flatten!
92
- nil
93
- end
94
-
95
- # Returns the first errors collected during an operation
96
- #
97
- # @return [nil,String] the error, if present
98
- #
99
- # @since 0.3.5
100
- #
101
- # @see Hanami::Interactor::Result#errors
102
- # @see Hanami::Interactor#call
103
- # @see Hanami::Interactor#error
104
- # @see Hanami::Interactor#error!
105
- def error
106
- errors.first
107
- end
108
-
109
- # Prepares the result before to be returned
110
- #
111
- # @param payload [Hash] an updated payload
112
- #
113
- # @since 0.3.5
114
- # @api private
115
- def prepare!(payload)
116
- @payload.merge!(payload)
117
- self
118
- end
119
-
120
- protected
121
-
122
- # @since 0.3.5
123
- # @api private
124
- def method_missing(method_name, *)
125
- @payload.fetch(method_name) { super }
126
- end
127
-
128
- # @since 0.3.5
129
- # @api private
130
- def respond_to_missing?(method_name, _include_all)
131
- method_name = method_name.to_sym
132
- METHODS[method_name] || @payload.key?(method_name)
133
- end
134
-
135
- # @since 0.3.5
136
- # @api private
137
- def __inspect
138
- " @success=#{@success} @payload=#{@payload.inspect}"
139
- end
140
- end
141
-
142
- # Override for <tt>Module#included</tt>.
143
- #
144
- # @since 0.3.5
145
- # @api private
146
- def self.included(base)
147
- super
148
-
149
- base.class_eval do
150
- extend ClassMethods
151
- end
152
- end
153
-
154
- # Interactor legacy interface
155
- #
156
- # @since 0.3.5
157
- module LegacyInterface
158
- # Initialize an interactor
159
- #
160
- # It accepts arbitrary number of arguments.
161
- # Developers can override it.
162
- #
163
- # @param args [Array<Object>] arbitrary number of arguments
164
- #
165
- # @return [Hanami::Interactor] the interactor
166
- #
167
- # @since 0.3.5
168
- #
169
- # @example Override #initialize
170
- # require 'hanami/interactor'
171
- #
172
- # class UpdateProfile
173
- # include Hanami::Interactor
174
- #
175
- # def initialize(user, params)
176
- # @user = user
177
- # @params = params
178
- # end
179
- #
180
- # def call
181
- # # ...
182
- # end
183
- # end
184
- def initialize(*args, **kwargs)
185
- super
186
- ensure
187
- @__result = ::Hanami::Interactor::Result.new
188
- end
189
-
190
- # Triggers the operation and return a result.
191
- #
192
- # All the instance variables will be available in the result.
193
- #
194
- # ATTENTION: This must be implemented by the including class.
195
- #
196
- # @return [Hanami::Interactor::Result] the result of the operation
197
- #
198
- # @raise [NoMethodError] if this isn't implemented by the including class.
199
- #
200
- # @example Expose instance variables in result payload
201
- # require 'hanami/interactor'
202
- #
203
- # class Signup
204
- # include Hanami::Interactor
205
- # expose :user, :params
206
- #
207
- # def initialize(params)
208
- # @params = params
209
- # @foo = 'bar'
210
- # end
211
- #
212
- # def call
213
- # @user = UserRepository.new.create(@params)
214
- # end
215
- # end
216
- #
217
- # result = Signup.new(name: 'Luca').call
218
- # result.failure? # => false
219
- # result.successful? # => true
220
- #
221
- # result.user # => #<User:0x007fa311105778 @id=1 @name="Luca">
222
- # result.params # => { :name=>"Luca" }
223
- # result.foo # => raises NoMethodError
224
- #
225
- # @example Failed precondition
226
- # require 'hanami/interactor'
227
- #
228
- # class Signup
229
- # include Hanami::Interactor
230
- # expose :user
231
- #
232
- # def initialize(params)
233
- # @params = params
234
- # end
235
- #
236
- # # THIS WON'T BE INVOKED BECAUSE #valid? WILL RETURN false
237
- # def call
238
- # @user = UserRepository.new.create(@params)
239
- # end
240
- #
241
- # private
242
- # def valid?
243
- # @params.valid?
244
- # end
245
- # end
246
- #
247
- # result = Signup.new(name: nil).call
248
- # result.successful? # => false
249
- # result.failure? # => true
250
- #
251
- # result.user # => #<User:0x007fa311105778 @id=nil @name="Luca">
252
- #
253
- # @example Bad usage
254
- # require 'hanami/interactor'
255
- #
256
- # class Signup
257
- # include Hanami::Interactor
258
- #
259
- # # Method #call is not defined
260
- # end
261
- #
262
- # Signup.new.call # => NoMethodError
263
- def call
264
- _call { super }
265
- end
266
-
267
- private
268
-
269
- # @since 0.3.5
270
- # @api private
271
- def _call
272
- catch :fail do
273
- validate!
274
- yield
275
- end
276
-
277
- _prepare!
278
- end
279
-
280
- # @since 0.3.5
281
- def validate!
282
- fail! unless valid?
283
- end
284
- end
285
-
286
- # Interactor interface
287
- # @since 1.1.0
288
- module Interface
289
- # Triggers the operation and return a result.
290
- #
291
- # All the exposed instance variables will be available in the result.
292
- #
293
- # ATTENTION: This must be implemented by the including class.
294
- #
295
- # @return [Hanami::Interactor::Result] the result of the operation
296
- #
297
- # @raise [NoMethodError] if this isn't implemented by the including class.
298
- #
299
- # @example Expose instance variables in result payload
300
- # require 'hanami/interactor'
301
- #
302
- # class Signup
303
- # include Hanami::Interactor
304
- # expose :user, :params
305
- #
306
- # def call(params)
307
- # @params = params
308
- # @foo = 'bar'
309
- # @user = UserRepository.new.persist(User.new(params))
310
- # end
311
- # end
312
- #
313
- # result = Signup.new(name: 'Luca').call
314
- # result.failure? # => false
315
- # result.successful? # => true
316
- #
317
- # result.user # => #<User:0x007fa311105778 @id=1 @name="Luca">
318
- # result.params # => { :name=>"Luca" }
319
- # result.foo # => raises NoMethodError
320
- #
321
- # @example Failed precondition
322
- # require 'hanami/interactor'
323
- #
324
- # class Signup
325
- # include Hanami::Interactor
326
- # expose :user
327
- #
328
- # # THIS WON'T BE INVOKED BECAUSE #valid? WILL RETURN false
329
- # def call(params)
330
- # @user = User.new(params)
331
- # @user = UserRepository.new.persist(@user)
332
- # end
333
- #
334
- # private
335
- # def valid?(params)
336
- # params.valid?
337
- # end
338
- # end
339
- #
340
- # result = Signup.new.call(name: nil)
341
- # result.successful? # => false
342
- # result.failure? # => true
343
- #
344
- # result.user # => nil
345
- #
346
- # @example Bad usage
347
- # require 'hanami/interactor'
348
- #
349
- # class Signup
350
- # include Hanami::Interactor
351
- #
352
- # # Method #call is not defined
353
- # end
354
- #
355
- # Signup.new.call # => NoMethodError
356
- def call(*args, **kwargs)
357
- @__result = ::Hanami::Interactor::Result.new
358
- _call(*args, **kwargs) { super }
359
- end
360
-
361
- private
362
-
363
- # @api private
364
- # @since 1.1.0
365
- def _call(*args, **kwargs)
366
- catch :fail do
367
- validate!(*args, **kwargs)
368
- yield
369
- end
370
-
371
- _prepare!
372
- end
373
-
374
- # @since 1.1.0
375
- def validate!(*args, **kwargs)
376
- fail! unless valid?(*args, **kwargs)
377
- end
378
- end
379
-
380
- private
381
-
382
- # Checks if proceed with <tt>#call</tt> invocation.
383
- # By default it returns <tt>true</tt>.
384
- #
385
- # Developers can override it.
386
- #
387
- # @return [TrueClass,FalseClass] the result of the check
388
- #
389
- # @since 0.3.5
390
- def valid?(*)
391
- true
392
- end
393
-
394
- # Fails and interrupts the current flow.
395
- #
396
- # @since 0.3.5
397
- #
398
- # @example
399
- # require 'hanami/interactor'
400
- #
401
- # class CreateEmailTest
402
- # include Hanami::Interactor
403
- #
404
- # def initialize(params)
405
- # @params = params
406
- # end
407
- #
408
- # def call
409
- # persist_email_test!
410
- # capture_screenshot!
411
- # end
412
- #
413
- # private
414
- # def persist_email_test!
415
- # @email_test = EmailTestRepository.new.create(@params)
416
- # end
417
- #
418
- # # IF THIS RAISES AN EXCEPTION WE FORCE A FAILURE
419
- # def capture_screenshot!
420
- # Screenshot.new(@email_test).capture!
421
- # rescue
422
- # fail!
423
- # end
424
- # end
425
- #
426
- # result = CreateEmailTest.new(account_id: 1).call
427
- # result.successful? # => false
428
- def fail!
429
- @__result.fail!
430
- throw :fail
431
- end
432
-
433
- # Logs an error without interrupting the flow.
434
- #
435
- # When used, the returned result won't be successful.
436
- #
437
- # @param message [String] the error message
438
- #
439
- # @return false
440
- #
441
- # @since 0.3.5
442
- #
443
- # @see Hanami::Interactor#error!
444
- #
445
- # @example
446
- # require 'hanami/interactor'
447
- #
448
- # class CreateRecord
449
- # include Hanami::Interactor
450
- # expose :logger
451
- #
452
- # def initialize
453
- # @logger = []
454
- # end
455
- #
456
- # def call
457
- # prepare_data!
458
- # persist!
459
- # sync!
460
- # end
461
- #
462
- # private
463
- # def prepare_data!
464
- # @logger << __method__
465
- # error "Prepare data error"
466
- # end
467
- #
468
- # def persist!
469
- # @logger << __method__
470
- # error "Persist error"
471
- # end
472
- #
473
- # def sync!
474
- # @logger << __method__
475
- # end
476
- # end
477
- #
478
- # result = CreateRecord.new.call
479
- # result.successful? # => false
480
- #
481
- # result.errors # => ["Prepare data error", "Persist error"]
482
- # result.logger # => [:prepare_data!, :persist!, :sync!]
483
- def error(message)
484
- @__result.add_error message
485
- false
486
- end
487
-
488
- # Logs an error and interrupts the flow.
489
- #
490
- # When used, the returned result won't be successful.
491
- #
492
- # @param message [String] the error message
493
- #
494
- # @since 0.3.5
495
- #
496
- # @see Hanami::Interactor#error
497
- #
498
- # @example
499
- # require 'hanami/interactor'
500
- #
501
- # class CreateRecord
502
- # include Hanami::Interactor
503
- # expose :logger
504
- #
505
- # def initialize
506
- # @logger = []
507
- # end
508
- #
509
- # def call
510
- # prepare_data!
511
- # persist!
512
- # sync!
513
- # end
514
- #
515
- # private
516
- # def prepare_data!
517
- # @logger << __method__
518
- # error "Prepare data error"
519
- # end
520
- #
521
- # def persist!
522
- # @logger << __method__
523
- # error! "Persist error"
524
- # end
525
- #
526
- # # THIS WILL NEVER BE INVOKED BECAUSE WE USE #error! IN #persist!
527
- # def sync!
528
- # @logger << __method__
529
- # end
530
- # end
531
- #
532
- # result = CreateRecord.new.call
533
- # result.successful? # => false
534
- #
535
- # result.errors # => ["Prepare data error", "Persist error"]
536
- # result.logger # => [:prepare_data!, :persist!]
537
- def error!(message)
538
- error(message)
539
- fail!
540
- end
541
-
542
- # @since 0.3.5
543
- # @api private
544
- def _prepare!
545
- @__result.prepare!(_exposures)
546
- end
547
-
548
- # @since 0.5.0
549
- # @api private
550
- def _exposures
551
- ::Hash[].tap do |result|
552
- self.class.exposures.each do |name, ivar|
553
- result[name] = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : nil
554
- end
555
- end
556
- end
557
- end
558
-
559
- # @since 0.5.0
560
- # @api private
561
- module ClassMethods
562
- # @since 0.5.0
563
- # @api private
564
- def self.extended(interactor)
565
- interactor.class_eval do
566
- include Utils::ClassAttribute
567
-
568
- class_attribute :exposures
569
- self.exposures = {}
570
- end
571
- end
572
-
573
- def method_added(method_name)
574
- super
575
- return unless method_name == :call
576
-
577
- if instance_method(:call).arity.zero?
578
- prepend Hanami::Interactor::LegacyInterface
579
- else
580
- prepend Hanami::Interactor::Interface
581
- end
582
- end
583
-
584
- # Exposes local instance variables into the returning value of <tt>#call</tt>
585
- #
586
- # @param instance_variable_names [Symbol,Array<Symbol>] one or more instance
587
- # variable names
588
- #
589
- # @since 0.5.0
590
- #
591
- # @see Hanami::Interactor::Result
592
- #
593
- # @example Exposes instance variable
594
- #
595
- # class Signup
596
- # include Hanami::Interactor
597
- # expose :user
598
- #
599
- # def initialize(params)
600
- # @params = params
601
- # @user = User.new(@params[:user])
602
- # end
603
- #
604
- # def call
605
- # # ...
606
- # end
607
- # end
608
- #
609
- # result = Signup.new(user: { name: "Luca" }).call
610
- #
611
- # result.user # => #<User:0x007fa85c58ccd8 @name="Luca">
612
- # result.params # => NoMethodError
613
- def expose(*instance_variable_names)
614
- instance_variable_names.each do |name|
615
- exposures[name.to_sym] = "@#{name}"
616
- end
617
- end
618
- end
619
- end