hanami-utils 2.0.0.alpha2 → 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,651 +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
- if RUBY_VERSION >= "3.0"
185
- def initialize(*args, **kwargs)
186
- super
187
- ensure
188
- @__result = ::Hanami::Interactor::Result.new
189
- end
190
- else
191
- def initialize(*args)
192
- super
193
- ensure
194
- @__result = ::Hanami::Interactor::Result.new
195
- end
196
- end
197
-
198
- # Triggers the operation and return a result.
199
- #
200
- # All the instance variables will be available in the result.
201
- #
202
- # ATTENTION: This must be implemented by the including class.
203
- #
204
- # @return [Hanami::Interactor::Result] the result of the operation
205
- #
206
- # @raise [NoMethodError] if this isn't implemented by the including class.
207
- #
208
- # @example Expose instance variables in result payload
209
- # require 'hanami/interactor'
210
- #
211
- # class Signup
212
- # include Hanami::Interactor
213
- # expose :user, :params
214
- #
215
- # def initialize(params)
216
- # @params = params
217
- # @foo = 'bar'
218
- # end
219
- #
220
- # def call
221
- # @user = UserRepository.new.create(@params)
222
- # end
223
- # end
224
- #
225
- # result = Signup.new(name: 'Luca').call
226
- # result.failure? # => false
227
- # result.successful? # => true
228
- #
229
- # result.user # => #<User:0x007fa311105778 @id=1 @name="Luca">
230
- # result.params # => { :name=>"Luca" }
231
- # result.foo # => raises NoMethodError
232
- #
233
- # @example Failed precondition
234
- # require 'hanami/interactor'
235
- #
236
- # class Signup
237
- # include Hanami::Interactor
238
- # expose :user
239
- #
240
- # def initialize(params)
241
- # @params = params
242
- # end
243
- #
244
- # # THIS WON'T BE INVOKED BECAUSE #valid? WILL RETURN false
245
- # def call
246
- # @user = UserRepository.new.create(@params)
247
- # end
248
- #
249
- # private
250
- # def valid?
251
- # @params.valid?
252
- # end
253
- # end
254
- #
255
- # result = Signup.new(name: nil).call
256
- # result.successful? # => false
257
- # result.failure? # => true
258
- #
259
- # result.user # => #<User:0x007fa311105778 @id=nil @name="Luca">
260
- #
261
- # @example Bad usage
262
- # require 'hanami/interactor'
263
- #
264
- # class Signup
265
- # include Hanami::Interactor
266
- #
267
- # # Method #call is not defined
268
- # end
269
- #
270
- # Signup.new.call # => NoMethodError
271
- def call
272
- _call { super }
273
- end
274
-
275
- private
276
-
277
- # @since 0.3.5
278
- # @api private
279
- def _call
280
- catch :fail do
281
- validate!
282
- yield
283
- end
284
-
285
- _prepare!
286
- end
287
-
288
- # @since 0.3.5
289
- def validate!
290
- fail! unless valid?
291
- end
292
- end
293
-
294
- # Interactor interface
295
- # @since 1.1.0
296
- module Interface
297
- # Triggers the operation and return a result.
298
- #
299
- # All the exposed instance variables will be available in the result.
300
- #
301
- # ATTENTION: This must be implemented by the including class.
302
- #
303
- # @return [Hanami::Interactor::Result] the result of the operation
304
- #
305
- # @raise [NoMethodError] if this isn't implemented by the including class.
306
- #
307
- # @example Expose instance variables in result payload
308
- # require 'hanami/interactor'
309
- #
310
- # class Signup
311
- # include Hanami::Interactor
312
- # expose :user, :params
313
- #
314
- # def call(params)
315
- # @params = params
316
- # @foo = 'bar'
317
- # @user = UserRepository.new.persist(User.new(params))
318
- # end
319
- # end
320
- #
321
- # result = Signup.new(name: 'Luca').call
322
- # result.failure? # => false
323
- # result.successful? # => true
324
- #
325
- # result.user # => #<User:0x007fa311105778 @id=1 @name="Luca">
326
- # result.params # => { :name=>"Luca" }
327
- # result.foo # => raises NoMethodError
328
- #
329
- # @example Failed precondition
330
- # require 'hanami/interactor'
331
- #
332
- # class Signup
333
- # include Hanami::Interactor
334
- # expose :user
335
- #
336
- # # THIS WON'T BE INVOKED BECAUSE #valid? WILL RETURN false
337
- # def call(params)
338
- # @user = User.new(params)
339
- # @user = UserRepository.new.persist(@user)
340
- # end
341
- #
342
- # private
343
- # def valid?(params)
344
- # params.valid?
345
- # end
346
- # end
347
- #
348
- # result = Signup.new.call(name: nil)
349
- # result.successful? # => false
350
- # result.failure? # => true
351
- #
352
- # result.user # => nil
353
- #
354
- # @example Bad usage
355
- # require 'hanami/interactor'
356
- #
357
- # class Signup
358
- # include Hanami::Interactor
359
- #
360
- # # Method #call is not defined
361
- # end
362
- #
363
- # Signup.new.call # => NoMethodError
364
- if RUBY_VERSION >= "3.0"
365
- def call(*args, **kwargs)
366
- @__result = ::Hanami::Interactor::Result.new
367
- _call(*args, **kwargs) { super }
368
- end
369
- else
370
- def call(*args)
371
- @__result = ::Hanami::Interactor::Result.new
372
- _call(*args) { super }
373
- end
374
- end
375
-
376
- private
377
-
378
- # @api private
379
- # @since 1.1.0
380
- if RUBY_VERSION >= "3.0"
381
- def _call(*args, **kwargs)
382
- catch :fail do
383
- validate!(*args, **kwargs)
384
- yield
385
- end
386
-
387
- _prepare!
388
- end
389
- else
390
- def _call(*args)
391
- catch :fail do
392
- validate!(*args)
393
- yield
394
- end
395
-
396
- _prepare!
397
- end
398
- end
399
-
400
- # @since 1.1.0
401
- if RUBY_VERSION >= "3.0"
402
- def validate!(*args, **kwargs)
403
- fail! unless valid?(*args, **kwargs)
404
- end
405
- else
406
- def validate!(*args)
407
- fail! unless valid?(*args)
408
- end
409
- end
410
- end
411
-
412
- private
413
-
414
- # Checks if proceed with <tt>#call</tt> invocation.
415
- # By default it returns <tt>true</tt>.
416
- #
417
- # Developers can override it.
418
- #
419
- # @return [TrueClass,FalseClass] the result of the check
420
- #
421
- # @since 0.3.5
422
- def valid?(*)
423
- true
424
- end
425
-
426
- # Fails and interrupts the current flow.
427
- #
428
- # @since 0.3.5
429
- #
430
- # @example
431
- # require 'hanami/interactor'
432
- #
433
- # class CreateEmailTest
434
- # include Hanami::Interactor
435
- #
436
- # def initialize(params)
437
- # @params = params
438
- # end
439
- #
440
- # def call
441
- # persist_email_test!
442
- # capture_screenshot!
443
- # end
444
- #
445
- # private
446
- # def persist_email_test!
447
- # @email_test = EmailTestRepository.new.create(@params)
448
- # end
449
- #
450
- # # IF THIS RAISES AN EXCEPTION WE FORCE A FAILURE
451
- # def capture_screenshot!
452
- # Screenshot.new(@email_test).capture!
453
- # rescue
454
- # fail!
455
- # end
456
- # end
457
- #
458
- # result = CreateEmailTest.new(account_id: 1).call
459
- # result.successful? # => false
460
- def fail!
461
- @__result.fail!
462
- throw :fail
463
- end
464
-
465
- # Logs an error without interrupting the flow.
466
- #
467
- # When used, the returned result won't be successful.
468
- #
469
- # @param message [String] the error message
470
- #
471
- # @return false
472
- #
473
- # @since 0.3.5
474
- #
475
- # @see Hanami::Interactor#error!
476
- #
477
- # @example
478
- # require 'hanami/interactor'
479
- #
480
- # class CreateRecord
481
- # include Hanami::Interactor
482
- # expose :logger
483
- #
484
- # def initialize
485
- # @logger = []
486
- # end
487
- #
488
- # def call
489
- # prepare_data!
490
- # persist!
491
- # sync!
492
- # end
493
- #
494
- # private
495
- # def prepare_data!
496
- # @logger << __method__
497
- # error "Prepare data error"
498
- # end
499
- #
500
- # def persist!
501
- # @logger << __method__
502
- # error "Persist error"
503
- # end
504
- #
505
- # def sync!
506
- # @logger << __method__
507
- # end
508
- # end
509
- #
510
- # result = CreateRecord.new.call
511
- # result.successful? # => false
512
- #
513
- # result.errors # => ["Prepare data error", "Persist error"]
514
- # result.logger # => [:prepare_data!, :persist!, :sync!]
515
- def error(message)
516
- @__result.add_error message
517
- false
518
- end
519
-
520
- # Logs an error and interrupts the flow.
521
- #
522
- # When used, the returned result won't be successful.
523
- #
524
- # @param message [String] the error message
525
- #
526
- # @since 0.3.5
527
- #
528
- # @see Hanami::Interactor#error
529
- #
530
- # @example
531
- # require 'hanami/interactor'
532
- #
533
- # class CreateRecord
534
- # include Hanami::Interactor
535
- # expose :logger
536
- #
537
- # def initialize
538
- # @logger = []
539
- # end
540
- #
541
- # def call
542
- # prepare_data!
543
- # persist!
544
- # sync!
545
- # end
546
- #
547
- # private
548
- # def prepare_data!
549
- # @logger << __method__
550
- # error "Prepare data error"
551
- # end
552
- #
553
- # def persist!
554
- # @logger << __method__
555
- # error! "Persist error"
556
- # end
557
- #
558
- # # THIS WILL NEVER BE INVOKED BECAUSE WE USE #error! IN #persist!
559
- # def sync!
560
- # @logger << __method__
561
- # end
562
- # end
563
- #
564
- # result = CreateRecord.new.call
565
- # result.successful? # => false
566
- #
567
- # result.errors # => ["Prepare data error", "Persist error"]
568
- # result.logger # => [:prepare_data!, :persist!]
569
- def error!(message)
570
- error(message)
571
- fail!
572
- end
573
-
574
- # @since 0.3.5
575
- # @api private
576
- def _prepare!
577
- @__result.prepare!(_exposures)
578
- end
579
-
580
- # @since 0.5.0
581
- # @api private
582
- def _exposures
583
- ::Hash[].tap do |result|
584
- self.class.exposures.each do |name, ivar|
585
- result[name] = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : nil
586
- end
587
- end
588
- end
589
- end
590
-
591
- # @since 0.5.0
592
- # @api private
593
- module ClassMethods
594
- # @since 0.5.0
595
- # @api private
596
- def self.extended(interactor)
597
- interactor.class_eval do
598
- include Utils::ClassAttribute
599
-
600
- class_attribute :exposures
601
- self.exposures = {}
602
- end
603
- end
604
-
605
- def method_added(method_name)
606
- super
607
- return unless method_name == :call
608
-
609
- if instance_method(:call).arity.zero?
610
- prepend Hanami::Interactor::LegacyInterface
611
- else
612
- prepend Hanami::Interactor::Interface
613
- end
614
- end
615
-
616
- # Exposes local instance variables into the returning value of <tt>#call</tt>
617
- #
618
- # @param instance_variable_names [Symbol,Array<Symbol>] one or more instance
619
- # variable names
620
- #
621
- # @since 0.5.0
622
- #
623
- # @see Hanami::Interactor::Result
624
- #
625
- # @example Exposes instance variable
626
- #
627
- # class Signup
628
- # include Hanami::Interactor
629
- # expose :user
630
- #
631
- # def initialize(params)
632
- # @params = params
633
- # @user = User.new(@params[:user])
634
- # end
635
- #
636
- # def call
637
- # # ...
638
- # end
639
- # end
640
- #
641
- # result = Signup.new(user: { name: "Luca" }).call
642
- #
643
- # result.user # => #<User:0x007fa85c58ccd8 @name="Luca">
644
- # result.params # => NoMethodError
645
- def expose(*instance_variable_names)
646
- instance_variable_names.each do |name|
647
- exposures[name.to_sym] = "@#{name}"
648
- end
649
- end
650
- end
651
- end