active_interaction 4.0.6 → 5.1.0

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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +156 -7
  3. data/CONTRIBUTING.md +11 -3
  4. data/README.md +260 -219
  5. data/lib/active_interaction/array_input.rb +77 -0
  6. data/lib/active_interaction/base.rb +14 -98
  7. data/lib/active_interaction/concerns/active_recordable.rb +3 -3
  8. data/lib/active_interaction/concerns/missable.rb +2 -2
  9. data/lib/active_interaction/errors.rb +6 -88
  10. data/lib/active_interaction/exceptions.rb +47 -0
  11. data/lib/active_interaction/filter/column.rb +59 -0
  12. data/lib/active_interaction/filter/error.rb +40 -0
  13. data/lib/active_interaction/filter.rb +44 -53
  14. data/lib/active_interaction/filters/abstract_date_time_filter.rb +9 -6
  15. data/lib/active_interaction/filters/abstract_numeric_filter.rb +7 -3
  16. data/lib/active_interaction/filters/array_filter.rb +40 -6
  17. data/lib/active_interaction/filters/boolean_filter.rb +4 -3
  18. data/lib/active_interaction/filters/date_filter.rb +1 -1
  19. data/lib/active_interaction/filters/date_time_filter.rb +1 -1
  20. data/lib/active_interaction/filters/decimal_filter.rb +1 -1
  21. data/lib/active_interaction/filters/float_filter.rb +1 -1
  22. data/lib/active_interaction/filters/hash_filter.rb +23 -15
  23. data/lib/active_interaction/filters/integer_filter.rb +1 -1
  24. data/lib/active_interaction/filters/interface_filter.rb +12 -12
  25. data/lib/active_interaction/filters/object_filter.rb +9 -3
  26. data/lib/active_interaction/filters/record_filter.rb +21 -11
  27. data/lib/active_interaction/filters/string_filter.rb +1 -1
  28. data/lib/active_interaction/filters/symbol_filter.rb +1 -1
  29. data/lib/active_interaction/filters/time_filter.rb +4 -4
  30. data/lib/active_interaction/hash_input.rb +43 -0
  31. data/lib/active_interaction/input.rb +23 -0
  32. data/lib/active_interaction/inputs.rb +161 -46
  33. data/lib/active_interaction/locale/en.yml +0 -1
  34. data/lib/active_interaction/locale/fr.yml +0 -1
  35. data/lib/active_interaction/locale/it.yml +0 -1
  36. data/lib/active_interaction/locale/ja.yml +0 -1
  37. data/lib/active_interaction/locale/pt-BR.yml +0 -1
  38. data/lib/active_interaction/modules/validation.rb +6 -17
  39. data/lib/active_interaction/version.rb +1 -1
  40. data/lib/active_interaction.rb +41 -36
  41. data/spec/active_interaction/array_input_spec.rb +166 -0
  42. data/spec/active_interaction/base_spec.rb +34 -248
  43. data/spec/active_interaction/concerns/active_modelable_spec.rb +3 -3
  44. data/spec/active_interaction/concerns/active_recordable_spec.rb +7 -7
  45. data/spec/active_interaction/concerns/hashable_spec.rb +8 -8
  46. data/spec/active_interaction/concerns/missable_spec.rb +9 -9
  47. data/spec/active_interaction/concerns/runnable_spec.rb +34 -32
  48. data/spec/active_interaction/errors_spec.rb +60 -43
  49. data/spec/active_interaction/{filter_column_spec.rb → filter/column_spec.rb} +3 -10
  50. data/spec/active_interaction/filter_spec.rb +6 -6
  51. data/spec/active_interaction/filters/abstract_date_time_filter_spec.rb +2 -2
  52. data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +2 -2
  53. data/spec/active_interaction/filters/array_filter_spec.rb +109 -16
  54. data/spec/active_interaction/filters/boolean_filter_spec.rb +12 -11
  55. data/spec/active_interaction/filters/date_filter_spec.rb +32 -27
  56. data/spec/active_interaction/filters/date_time_filter_spec.rb +34 -29
  57. data/spec/active_interaction/filters/decimal_filter_spec.rb +20 -18
  58. data/spec/active_interaction/filters/file_filter_spec.rb +7 -7
  59. data/spec/active_interaction/filters/float_filter_spec.rb +19 -17
  60. data/spec/active_interaction/filters/hash_filter_spec.rb +16 -18
  61. data/spec/active_interaction/filters/integer_filter_spec.rb +24 -22
  62. data/spec/active_interaction/filters/interface_filter_spec.rb +105 -82
  63. data/spec/active_interaction/filters/object_filter_spec.rb +52 -36
  64. data/spec/active_interaction/filters/record_filter_spec.rb +61 -39
  65. data/spec/active_interaction/filters/string_filter_spec.rb +7 -7
  66. data/spec/active_interaction/filters/symbol_filter_spec.rb +6 -6
  67. data/spec/active_interaction/filters/time_filter_spec.rb +57 -34
  68. data/spec/active_interaction/hash_input_spec.rb +58 -0
  69. data/spec/active_interaction/i18n_spec.rb +22 -17
  70. data/spec/active_interaction/inputs_spec.rb +170 -18
  71. data/spec/active_interaction/integration/array_interaction_spec.rb +3 -7
  72. data/spec/active_interaction/integration/record_integration_spec.rb +5 -0
  73. data/spec/active_interaction/modules/validation_spec.rb +8 -31
  74. data/spec/spec_helper.rb +9 -0
  75. data/spec/support/concerns.rb +2 -2
  76. data/spec/support/filters.rb +27 -51
  77. data/spec/support/interactions.rb +4 -4
  78. metadata +50 -50
  79. data/lib/active_interaction/filter_column.rb +0 -57
data/README.md CHANGED
@@ -1,11 +1,10 @@
1
1
  # [ActiveInteraction][]
2
2
 
3
3
  ActiveInteraction manages application-specific business logic.
4
- It's an implementation of the command pattern in Ruby.
4
+ It's an implementation of service objects designed to blend seamlessly into Rails.
5
5
 
6
6
  [![Version](https://img.shields.io/gem/v/active_interaction.svg?style=flat-square)](https://rubygems.org/gems/active_interaction)
7
7
  [![Test](https://img.shields.io/github/workflow/status/AaronLasseigne/active_interaction/Test?label=Test&style=flat-square)](https://github.com/AaronLasseigne/active_interaction/actions?query=workflow%3ATest)
8
- [![Climate](https://img.shields.io/codeclimate/maintainability/orgsync/active_interaction.svg?style=flat-square)](https://codeclimate.com/github/orgsync/active_interaction)
9
8
 
10
9
  ---
11
10
 
@@ -18,23 +17,25 @@ handles your verbs.
18
17
  - [Basic usage](#basic-usage)
19
18
  - [Validations](#validations)
20
19
  - [Filters](#filters)
21
- - [Array](#array)
22
- - [Boolean](#boolean)
23
- - [File](#file)
24
- - [Hash](#hash)
25
- - [Interface](#interface)
26
- - [Object](#object)
27
- - [Record](#record)
28
- - [String](#string)
29
- - [Symbol](#symbol)
30
- - [Dates and times](#dates-and-times)
31
- - [Date](#date)
32
- - [DateTime](#datetime)
33
- - [Time](#time)
34
- - [Numbers](#numbers)
35
- - [Decimal](#decimal)
36
- - [Float](#float)
37
- - [Integer](#integer)
20
+ - [Basic Filters](#basic-filters)
21
+ - [Array](#array)
22
+ - [Boolean](#boolean)
23
+ - [File](#file)
24
+ - [Hash](#hash)
25
+ - [String](#string)
26
+ - [Symbol](#symbol)
27
+ - [Dates and times](#dates-and-times)
28
+ - [Date](#date)
29
+ - [DateTime](#datetime)
30
+ - [Time](#time)
31
+ - [Numbers](#numbers)
32
+ - [Decimal](#decimal)
33
+ - [Float](#float)
34
+ - [Integer](#integer)
35
+ - [Advanced Filters](#advanced-filters)
36
+ - [Interface](#interface)
37
+ - [Object](#object)
38
+ - [Record](#record)
38
39
  - [Rails](#rails)
39
40
  - [Setup](#setup)
40
41
  - [Controller](#controller)
@@ -52,7 +53,7 @@ handles your verbs.
52
53
  - [Descriptions](#descriptions)
53
54
  - [Errors](#errors)
54
55
  - [Forms](#forms)
55
- - [Grouped inputs](#grouped-inputs)
56
+ - [Shared input options](#shared-input-options)
56
57
  - [Optional inputs](#optional-inputs)
57
58
  - [Translations](#translations)
58
59
  - [Credits](#credits)
@@ -64,18 +65,17 @@ handles your verbs.
64
65
  Add it to your Gemfile:
65
66
 
66
67
  ``` rb
67
- gem 'active_interaction', '~> 4.0'
68
+ gem 'active_interaction', '~> 5.1'
68
69
  ```
69
70
 
70
71
  Or install it manually:
71
72
 
72
73
  ``` sh
73
- $ gem install active_interaction --version '~> 4.0'
74
+ $ gem install active_interaction --version '~> 5.1'
74
75
  ```
75
76
 
76
77
  This project uses [Semantic Versioning][]. Check out [GitHub releases][] for a
77
- detailed list of changes. For help upgrading to version 2, please read [the
78
- announcement post][].
78
+ detailed list of changes.
79
79
 
80
80
  ## Basic usage
81
81
 
@@ -90,7 +90,7 @@ you need to do two things:
90
90
  2. **Define your business logic.** Do this by implementing the `#execute`
91
91
  method. Each input you defined will be available as the type you specified.
92
92
  If any of the inputs are invalid, `#execute` won't be run. Filters are
93
- responsible for type checking your inputs. Check out [the validations
93
+ responsible for checking your inputs. Check out [the validations
94
94
  section](#validations) if you need more than that.
95
95
 
96
96
  That covers the basics. Let's put it all together into a simple example that
@@ -142,7 +142,7 @@ Square.run!(x: 2.1)
142
142
 
143
143
  ### Validations
144
144
 
145
- ActiveInteraction type checks your inputs. Often you'll want more than that.
145
+ ActiveInteraction checks your inputs. Often you'll want more than that.
146
146
  For instance, you may want an input to be a string with at least one
147
147
  non-whitespace character. Instead of writing your own validation for that, you
148
148
  can use validations from ActiveModel.
@@ -165,7 +165,7 @@ end
165
165
  ```
166
166
 
167
167
  When you run this interaction, two things will happen. **First
168
- ActiveInteraction will type check your inputs. Then ActiveModel will validate
168
+ ActiveInteraction will check your inputs. Then ActiveModel will validate
169
169
  them.** If both of those are happy, it will be executed.
170
170
 
171
171
  ``` rb
@@ -215,7 +215,9 @@ alternatives that can be reasonably coerced. Typically the coercions come from
215
215
  Rails, so `"1"` can be interpreted as the boolean value `true`, the string
216
216
  `"1"`, or the number `1`.
217
217
 
218
- ### Array
218
+ ### Basic Filters
219
+
220
+ #### Array
219
221
 
220
222
  In addition to accepting arrays, array inputs will convert
221
223
  `ActiveRecord::Relation`s into arrays.
@@ -262,7 +264,33 @@ array :managers do
262
264
  end
263
265
  ```
264
266
 
265
- ### Boolean
267
+ Errors that occur will be indexed based on the Rails configuration setting
268
+ `index_nested_attribute_errors`. You can also manually override this setting
269
+ with the `:index_errors` option. In this state is is possible to get multiple
270
+ errors from a single filter.
271
+
272
+ ```ruby
273
+ class ArrayInteraction < ActiveInteraction::Base
274
+ array :favorite_numbers, index_errors: true do
275
+ integer
276
+ end
277
+
278
+ def execute
279
+ favorite_numbers
280
+ end
281
+ end
282
+
283
+ ArrayInteraction.run(favorite_numbers: [8, 'bazillion']).errors.details
284
+ => {:"favorite_numbers[1]"=>[{:error=>:invalid_type, :type=>"array"}]}
285
+ ```
286
+
287
+ With `:index_errors` set to `false` the error would have been:
288
+
289
+ ```ruby
290
+ {:favorite_numbers=>[{:error=>:invalid_type, :type=>"array"}]}
291
+ ```
292
+
293
+ #### Boolean
266
294
 
267
295
  Boolean filters convert the strings `"1"`, `"true"`, and `"on"`
268
296
  (case-insensitive) into `true`. They also convert `"0"`, `"false"`, and `"off"`
@@ -283,7 +311,7 @@ BooleanInteraction.run!(kool_aid: true)
283
311
  # => "Oh yeah!"
284
312
  ```
285
313
 
286
- ### File
314
+ #### File
287
315
 
288
316
  File filters also accept `TempFile`s and anything that responds to `#rewind`.
289
317
  That means that you can pass the `params` from uploading files via forms in
@@ -304,7 +332,7 @@ FileInteraction.run!(readme: File.open('README.md'))
304
332
  # => 21563
305
333
  ```
306
334
 
307
- ### Hash
335
+ #### Hash
308
336
 
309
337
  Hash filters accept hashes. The expected value types are given by passing a
310
338
  block and nesting other filters. You can have any number of filters inside a
@@ -356,165 +384,7 @@ hash :stuff,
356
384
  strip: false
357
385
  ```
358
386
 
359
- ### Interface
360
-
361
- Interface filters allow you to specify an interface that the passed value must
362
- meet in order to pass. The name of the interface is used to look for a constant
363
- inside the ancestor listing for the passed value. This allows for a variety of
364
- checks depending on what's passed. Class instances are checked for an included
365
- module or an inherited ancestor class. Classes are checked for an extended
366
- module or an inherited ancestor class. Modules are checked for an extended
367
- module.
368
-
369
- ``` rb
370
- class InterfaceInteraction < ActiveInteraction::Base
371
- interface :exception
372
-
373
- def execute
374
- exception
375
- end
376
- end
377
-
378
- InterfaceInteraction.run!(exception: Exception)
379
- # ActiveInteraction::InvalidInteractionError: Exception is not a valid interface
380
- InterfaceInteraction.run!(exception: NameError) # a subclass of Exception
381
- # => NameError
382
- ```
383
-
384
- You can use `:from` to specify a class or module. This would be the equivalent
385
- of what's above.
386
-
387
- ```rb
388
- class InterfaceInteraction < ActiveInteraction::Base
389
- interface :error,
390
- from: Exception
391
-
392
- def execute
393
- error
394
- end
395
- end
396
- ```
397
-
398
- You can also create an anonymous interface on the fly by passing the `methods`
399
- option.
400
-
401
- ``` rb
402
- class InterfaceInteraction < ActiveInteraction::Base
403
- interface :serializer,
404
- methods: %i[dump load]
405
-
406
- def execute
407
- input = '{ "is_json" : true }'
408
- object = serializer.load(input)
409
- output = serializer.dump(object)
410
-
411
- output
412
- end
413
- end
414
-
415
- require 'json'
416
-
417
- InterfaceInteraction.run!(serializer: Object.new)
418
- # ActiveInteraction::InvalidInteractionError: Serializer is not a valid interface
419
- InterfaceInteraction.run!(serializer: JSON)
420
- # => "{\"is_json\":true}"
421
- ```
422
-
423
- ### Object
424
-
425
- Object filters allow you to require an instance of a particular class or one of
426
- its subclasses.
427
-
428
- ``` rb
429
- class Cow
430
- def moo
431
- 'Moo!'
432
- end
433
- end
434
-
435
- class ObjectInteraction < ActiveInteraction::Base
436
- object :cow
437
-
438
- def execute
439
- cow.moo
440
- end
441
- end
442
-
443
- ObjectInteraction.run!(cow: Object.new)
444
- # ActiveInteraction::InvalidInteractionError: Cow is not a valid object
445
- ObjectInteraction.run!(cow: Cow.new)
446
- # => "Moo!"
447
- ```
448
-
449
- The class name is automatically determined by the filter name. If your filter
450
- name is different than your class name, use the `class` option. It can be
451
- either the class, a string, or a symbol.
452
-
453
- ``` rb
454
- object :dolly1,
455
- class: Sheep
456
- object :dolly2,
457
- class: 'Sheep'
458
- object :dolly3,
459
- class: :Sheep
460
- ```
461
-
462
- If you have value objects or you would like to build one object from another,
463
- you can use the `converter` option. It is only called if the value provided is
464
- not an instance of the class or one of its subclasses. The `converter` option
465
- accepts a symbol that specifies a class method on the object class or a proc.
466
- Both will be passed the value and any errors thrown inside the converter will
467
- cause the value to be considered invalid. Any returned value that is not the
468
- correct class will also be treated as invalid. The value given to the `default`
469
- option will also be converted.
470
-
471
- ``` rb
472
- class ObjectInteraction < ActiveInteraction::Base
473
- object :ip_address,
474
- class: IPAddr,
475
- converter: :new
476
-
477
- def execute
478
- ip_address
479
- end
480
- end
481
-
482
- ObjectInteraction.run!(ip_address: '192.168.1.1')
483
- # #<IPAddr: IPv4:192.168.1.1/255.255.255.255>
484
-
485
- ObjectInteraction.run!(ip_address: 1)
486
- # ActiveInteraction::InvalidInteractionError: Ip address is not a valid object
487
- ```
488
-
489
- ### Record
490
-
491
- Record filters allow you to require an instance of a particular class (or one
492
- of its subclasses) or a value that can be used to locate an instance of the
493
- object. If the value does not match, it will call `find` on the class of the
494
- record. This is particularly useful when working with ActiveRecord objects.
495
- Like an object filter, the class is derived from the name passed but can be
496
- specified with the `class` option. The value given to the `default` option will
497
- also be found.
498
-
499
- ``` rb
500
- class RecordInteraction < ActiveInteraction::Base
501
- record :encoding
502
-
503
- def execute
504
- encoding
505
- end
506
- end
507
-
508
- > RecordInteraction.run!(encoding: Encoding::US_ASCII)
509
- => #<Encoding:US-ASCII>
510
-
511
- > RecordInteraction.run!(encoding: 'ascii')
512
- => #<Encoding:US-ASCII>
513
- ```
514
-
515
- A different method can be specified by providing a symbol to the `finder` option.
516
-
517
- ### String
387
+ #### String
518
388
 
519
389
  String filters define inputs that only accept strings.
520
390
 
@@ -541,7 +411,7 @@ string :comment,
541
411
  strip: false
542
412
  ```
543
413
 
544
- ### Symbol
414
+ #### Symbol
545
415
 
546
416
  Symbol filters define inputs that accept symbols. Strings will be converted
547
417
  into symbols.
@@ -561,7 +431,7 @@ SymbolInteraction.run!(method: :object_id)
561
431
  # => #<Proc:0x007fdc9ba94118>
562
432
  ```
563
433
 
564
- ### Dates and times
434
+ #### Dates and times
565
435
 
566
436
  Filters that work with dates and times behave similarly. By default, they all
567
437
  convert strings into their expected data types using `.parse`. Blank strings
@@ -569,7 +439,7 @@ will be treated as `nil`. If you give the `format` option, they will instead
569
439
  convert strings using `.strptime`. Note that formats won't work with `DateTime`
570
440
  and `Time` filters if a time zone is set.
571
441
 
572
- #### Date
442
+ ##### Date
573
443
 
574
444
  ``` rb
575
445
  class DateInteraction < ActiveInteraction::Base
@@ -591,7 +461,7 @@ date :birthday,
591
461
  format: '%Y-%m-%d'
592
462
  ```
593
463
 
594
- #### DateTime
464
+ ##### DateTime
595
465
 
596
466
  ``` rb
597
467
  class DateTimeInteraction < ActiveInteraction::Base
@@ -613,7 +483,7 @@ date_time :start,
613
483
  format: '%Y-%m-%dT%H:%M:%S'
614
484
  ```
615
485
 
616
- #### Time
486
+ ##### Time
617
487
 
618
488
  In addition to converting strings with `.parse` (or `.strptime`), time filters
619
489
  convert numbers with `.at`.
@@ -638,13 +508,13 @@ time :start,
638
508
  format: '%Y-%m-%dT%H:%M:%S'
639
509
  ```
640
510
 
641
- ### Numbers
511
+ #### Numbers
642
512
 
643
513
  All numeric filters accept numeric input. They will also convert strings using
644
514
  the appropriate method from `Kernel` (like `.Float`). Blank strings will be
645
515
  treated as `nil`.
646
516
 
647
- #### Decimal
517
+ ##### Decimal
648
518
 
649
519
  ``` rb
650
520
  class DecimalInteraction < ActiveInteraction::Base
@@ -668,7 +538,7 @@ decimal :dollars,
668
538
  digits: 2
669
539
  ```
670
540
 
671
- #### Float
541
+ ##### Float
672
542
 
673
543
  ``` rb
674
544
  class FloatInteraction < ActiveInteraction::Base
@@ -685,7 +555,7 @@ FloatInteraction.run!(x: 2.1)
685
555
  # => 4.41
686
556
  ```
687
557
 
688
- #### Integer
558
+ ##### Integer
689
559
 
690
560
  ``` rb
691
561
  class IntegerInteraction < ActiveInteraction::Base
@@ -726,6 +596,167 @@ IntegerInteraction.run!(limit1: "08", limit2: "08", limit3: "08")
726
596
  ActiveInteraction::InvalidInteractionError: Limit2 is not a valid integer, Limit3 is not a valid integer
727
597
  ```
728
598
 
599
+ ### Advanced Filters
600
+
601
+ #### Interface
602
+
603
+ Interface filters allow you to specify an interface that the passed value must
604
+ meet in order to pass. The name of the interface is used to look for a constant
605
+ inside the ancestor listing for the passed value. This allows for a variety of
606
+ checks depending on what's passed. Class instances are checked for an included
607
+ module or an inherited ancestor class. Classes are checked for an extended
608
+ module or an inherited ancestor class. Modules are checked for an extended
609
+ module.
610
+
611
+ ``` rb
612
+ class InterfaceInteraction < ActiveInteraction::Base
613
+ interface :exception
614
+
615
+ def execute
616
+ exception
617
+ end
618
+ end
619
+
620
+ InterfaceInteraction.run!(exception: Exception)
621
+ # ActiveInteraction::InvalidInteractionError: Exception is not a valid interface
622
+ InterfaceInteraction.run!(exception: NameError) # a subclass of Exception
623
+ # => NameError
624
+ ```
625
+
626
+ You can use `:from` to specify a class or module. This would be the equivalent
627
+ of what's above.
628
+
629
+ ```rb
630
+ class InterfaceInteraction < ActiveInteraction::Base
631
+ interface :error,
632
+ from: Exception
633
+
634
+ def execute
635
+ error
636
+ end
637
+ end
638
+ ```
639
+
640
+ You can also create an anonymous interface on the fly by passing the `methods`
641
+ option.
642
+
643
+ ``` rb
644
+ class InterfaceInteraction < ActiveInteraction::Base
645
+ interface :serializer,
646
+ methods: %i[dump load]
647
+
648
+ def execute
649
+ input = '{ "is_json" : true }'
650
+ object = serializer.load(input)
651
+ output = serializer.dump(object)
652
+
653
+ output
654
+ end
655
+ end
656
+
657
+ require 'json'
658
+
659
+ InterfaceInteraction.run!(serializer: Object.new)
660
+ # ActiveInteraction::InvalidInteractionError: Serializer is not a valid interface
661
+ InterfaceInteraction.run!(serializer: JSON)
662
+ # => "{\"is_json\":true}"
663
+ ```
664
+
665
+ #### Object
666
+
667
+ Object filters allow you to require an instance of a particular class or one of
668
+ its subclasses.
669
+
670
+ ``` rb
671
+ class Cow
672
+ def moo
673
+ 'Moo!'
674
+ end
675
+ end
676
+
677
+ class ObjectInteraction < ActiveInteraction::Base
678
+ object :cow
679
+
680
+ def execute
681
+ cow.moo
682
+ end
683
+ end
684
+
685
+ ObjectInteraction.run!(cow: Object.new)
686
+ # ActiveInteraction::InvalidInteractionError: Cow is not a valid object
687
+ ObjectInteraction.run!(cow: Cow.new)
688
+ # => "Moo!"
689
+ ```
690
+
691
+ The class name is automatically determined by the filter name. If your filter
692
+ name is different than your class name, use the `class` option. It can be
693
+ either the class, a string, or a symbol.
694
+
695
+ ``` rb
696
+ object :dolly1,
697
+ class: Sheep
698
+ object :dolly2,
699
+ class: 'Sheep'
700
+ object :dolly3,
701
+ class: :Sheep
702
+ ```
703
+
704
+ If you have value objects or you would like to build one object from another,
705
+ you can use the `converter` option. It is only called if the value provided is
706
+ not an instance of the class or one of its subclasses. The `converter` option
707
+ accepts a symbol that specifies a class method on the object class or a proc.
708
+ Both will be passed the value and any errors thrown inside the converter will
709
+ cause the value to be considered invalid. Any returned value that is not the
710
+ correct class will also be treated as invalid. Any `default` that is not an
711
+ instance of the class or subclass and is not `nil` will also be converted.
712
+
713
+ ``` rb
714
+ class ObjectInteraction < ActiveInteraction::Base
715
+ object :ip_address,
716
+ class: IPAddr,
717
+ converter: :new
718
+
719
+ def execute
720
+ ip_address
721
+ end
722
+ end
723
+
724
+ ObjectInteraction.run!(ip_address: '192.168.1.1')
725
+ # #<IPAddr: IPv4:192.168.1.1/255.255.255.255>
726
+
727
+ ObjectInteraction.run!(ip_address: 1)
728
+ # ActiveInteraction::InvalidInteractionError: Ip address is not a valid object
729
+ ```
730
+
731
+ #### Record
732
+
733
+ Record filters allow you to require an instance of a particular class (or one
734
+ of its subclasses) or a value that can be used to locate an instance of the
735
+ object. If the value does not match, it will call `find` on the class of the
736
+ record. This is particularly useful when working with ActiveRecord objects.
737
+ Like an object filter, the class is derived from the name passed but can be
738
+ specified with the `class` option. Any `default` that is not an instance of the
739
+ class or subclass and is not `nil` will also be found. Blank strings passed in
740
+ will be treated as `nil`.
741
+
742
+ ``` rb
743
+ class RecordInteraction < ActiveInteraction::Base
744
+ record :encoding
745
+
746
+ def execute
747
+ encoding
748
+ end
749
+ end
750
+
751
+ > RecordInteraction.run!(encoding: Encoding::US_ASCII)
752
+ => #<Encoding:US-ASCII>
753
+
754
+ > RecordInteraction.run!(encoding: 'ascii')
755
+ => #<Encoding:US-ASCII>
756
+ ```
757
+
758
+ A different method can be specified by providing a symbol to the `finder` option.
759
+
729
760
  ## Rails
730
761
 
731
762
  ActiveInteraction plays nicely with Rails. You can use interactions to handle
@@ -737,10 +768,7 @@ resourceful actions.
737
768
 
738
769
  We recommend putting your interactions in `app/interactions`. It's also very
739
770
  helpful to group them by model. That way you can look in
740
- `app/interactions/accounts` for all the ways you can interact with accounts. In
741
- order to use this structure add
742
- `config.autoload_paths += Dir.glob("#{config.root}/app/interactions/*")` in
743
- your `application.rb`
771
+ `app/interactions/accounts` for all the ways you can interact with accounts.
744
772
 
745
773
  ```
746
774
  - app/
@@ -843,7 +871,7 @@ end
843
871
  ```
844
872
 
845
873
  Note that it's perfectly fine to add errors during execution. Not all errors
846
- have to come from type checking or validation.
874
+ have to come from checking or validation.
847
875
 
848
876
  #### New
849
877
 
@@ -1020,13 +1048,13 @@ end
1020
1048
 
1021
1049
  ### Callbacks
1022
1050
 
1023
- ActiveModel provides a powerful framework for defining callbacks.
1024
- ActiveInteraction hooks into that framework to allow hooking into various parts
1025
- of an interaction's lifecycle.
1051
+ [ActiveSupport::Callbacks][] provides a powerful framework for defining callbacks.
1052
+ ActiveInteraction uses that framework to allow hooking into various parts of an
1053
+ interaction's lifecycle.
1026
1054
 
1027
1055
  ``` rb
1028
1056
  class Increment < ActiveInteraction::Base
1029
- set_callback :type_check, :before, -> { puts 'before type check' }
1057
+ set_callback :filter, :before, -> { puts 'before filter' }
1030
1058
 
1031
1059
  integer :x
1032
1060
 
@@ -1048,7 +1076,7 @@ class Increment < ActiveInteraction::Base
1048
1076
  end
1049
1077
 
1050
1078
  Increment.run!(x: 1)
1051
- # before type check
1079
+ # before filter
1052
1080
  # after validate
1053
1081
  # >>>
1054
1082
  # executing
@@ -1056,7 +1084,7 @@ Increment.run!(x: 1)
1056
1084
  # => 2
1057
1085
  ```
1058
1086
 
1059
- In order, the available callbacks are `type_check`, `validate`, and `execute`.
1087
+ In order, the available callbacks are `filter`, `validate`, and `execute`.
1060
1088
  You can set `before`, `after`, or `around` on any of them.
1061
1089
 
1062
1090
  ### Composition
@@ -1179,7 +1207,7 @@ errors.
1179
1207
  ``` rb
1180
1208
  outcome = BuyItem.run(item: 'Thing', options: { gift_wrapped: 'yes' })
1181
1209
  outcome.errors.messages
1182
- # => {:credit_card=>["is required"], :item=>["is not a valid object"], :options=>["has an invalid nested value (\"gift_wrapped\" => \"yes\")"]}
1210
+ # => {:credit_card=>["is required"], :item=>["is not a valid object"], :"options.gift_wrapped"=>["is not a valid boolean"]}
1183
1211
  ```
1184
1212
 
1185
1213
  Determining the type of error based on the string is difficult if not
@@ -1188,7 +1216,7 @@ the same list of errors with a testable label representing the error.
1188
1216
 
1189
1217
  ``` rb
1190
1218
  outcome.errors.details
1191
- # => {:credit_card=>[{:error=>:missing}], :item=>[{:type=>"object", :error=>:invalid_type}], :options=>[{:name=>"\"gift_wrapped\"", :value=>"\"yes\"", :error=>:invalid_nested}]}
1219
+ # => {:credit_card=>[{:error=>:missing}], :item=>[{:error=>:invalid_type, :type=>"object"}], :"options.gift_wrapped"=>[{:error=>:invalid_type, :type=>"boolean"}]}
1192
1220
  ```
1193
1221
 
1194
1222
  Detailed errors can also be manually added during the execute call by passing a
@@ -1338,7 +1366,7 @@ used to define the inputs on your interaction will relay type information to
1338
1366
  these gems. As a result, form fields will automatically use the appropriate
1339
1367
  input type.
1340
1368
 
1341
- ### Grouped inputs
1369
+ ### Shared input options
1342
1370
 
1343
1371
  It can be convenient to apply the same options to a bunch of inputs. One common
1344
1372
  use case is making many inputs optional. Instead of setting `default: nil` on
@@ -1359,8 +1387,8 @@ Optional inputs can be defined by using the `:default` option as described in
1359
1387
  are merged to create `inputs`. There are times where it is useful to know
1360
1388
  whether a value was passed to `run` or the result of a filter default. In
1361
1389
  particular, it is useful when `nil` is an acceptable value. For example, you
1362
- may optionally track your users' birthdays. You can use the `given?` predicate
1363
- to see if an input was even passed to `run`. With `given?` you can also check
1390
+ may optionally track your users' birthdays. You can use the `inputs.given?` predicate
1391
+ to see if an input was even passed to `run`. With `inputs.given?` you can also check
1364
1392
  the input of a hash or array filter by passing a series of keys or indexes to
1365
1393
  check.
1366
1394
 
@@ -1371,7 +1399,7 @@ class UpdateUser < ActiveInteraction::Base
1371
1399
  default: nil
1372
1400
 
1373
1401
  def execute
1374
- user.birthday = birthday if given?(:birthday)
1402
+ user.birthday = birthday if inputs.given?(:birthday)
1375
1403
  errors.merge!(user.errors) unless user.save
1376
1404
  user
1377
1405
  end
@@ -1425,7 +1453,6 @@ hsilgne:
1425
1453
  errors:
1426
1454
  messages:
1427
1455
  invalid: dilavni si
1428
- invalid_nested: (%{value} <= %{name}) eulav detsen dilavni na sah
1429
1456
  invalid_type: '%{type} dilav a ton si'
1430
1457
  missing: deriuqer si
1431
1458
  ```
@@ -1445,6 +1472,19 @@ I18nInteraction.run(name: false).errors.messages[:name]
1445
1472
  # => ["gnirts dilav a ton si"]
1446
1473
  ```
1447
1474
 
1475
+ Everything else works like an `activerecord` entry. For example, to rename an
1476
+ attribute you can use `attributes`.
1477
+
1478
+ Here we'll rename the `num` attribute on an interaction named `product`:
1479
+
1480
+ ``` yml
1481
+ en:
1482
+ active_interaction:
1483
+ attributes:
1484
+ product:
1485
+ num: 'Number'
1486
+ ```
1487
+
1448
1488
  ## Credits
1449
1489
 
1450
1490
  ActiveInteraction is brought to you by [Aaron Lasseigne][].
@@ -1458,16 +1498,17 @@ ActiveInteraction is licensed under [the MIT License][].
1458
1498
 
1459
1499
  [activeinteraction]: https://github.com/AaronLasseigne/active_interaction
1460
1500
  [API Documentation]: http://rubydoc.info/github/AaronLasseigne/active_interaction
1461
- [semantic versioning]: http://semver.org/spec/v2.0.0.html
1501
+ [Semantic Versioning]: http://semver.org/spec/v2.0.0.html
1462
1502
  [GitHub releases]: https://github.com/AaronLasseigne/active_interaction/releases
1463
1503
  [aaron lasseigne]: https://github.com/AaronLasseigne
1464
1504
  [taylor fausak]: https://github.com/tfausak
1465
1505
  [our contribution guidelines]: CONTRIBUTING.md
1466
1506
  [complete list of contributors]: https://github.com/AaronLasseigne/active_interaction/graphs/contributors
1467
- [the mit license]: LICENSE.md
1507
+ [the MIT License]: LICENSE.md
1468
1508
  [formtastic]: https://rubygems.org/gems/formtastic
1469
1509
  [simple_form]: https://rubygems.org/gems/simple_form
1470
1510
  [the filters section]: #filters
1471
1511
  [the errors section]: #errors
1472
1512
  [the optional inputs section]: #optional-inputs
1473
1513
  [`with_options`]: http://api.rubyonrails.org/classes/Object.html#method-i-with_options
1514
+ [ActiveSupport::Callbacks]: https://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html