active_interaction 4.0.6 → 5.1.0

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