hanami-utils 2.0.0.alpha2 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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