hanami-utils 2.0.0.alpha6 → 2.0.0.beta1

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