active_interaction 4.1.0 → 5.1.1

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 +150 -1
  3. data/CONTRIBUTING.md +11 -3
  4. data/README.md +256 -215
  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 +40 -52
  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 +27 -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 +43 -44
  79. data/lib/active_interaction/filter_column.rb +0 -57
data/README.md CHANGED
@@ -1,7 +1,7 @@
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)
@@ -17,23 +17,25 @@ handles your verbs.
17
17
  - [Basic usage](#basic-usage)
18
18
  - [Validations](#validations)
19
19
  - [Filters](#filters)
20
- - [Array](#array)
21
- - [Boolean](#boolean)
22
- - [File](#file)
23
- - [Hash](#hash)
24
- - [Interface](#interface)
25
- - [Object](#object)
26
- - [Record](#record)
27
- - [String](#string)
28
- - [Symbol](#symbol)
29
- - [Dates and times](#dates-and-times)
30
- - [Date](#date)
31
- - [DateTime](#datetime)
32
- - [Time](#time)
33
- - [Numbers](#numbers)
34
- - [Decimal](#decimal)
35
- - [Float](#float)
36
- - [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)
37
39
  - [Rails](#rails)
38
40
  - [Setup](#setup)
39
41
  - [Controller](#controller)
@@ -51,7 +53,7 @@ handles your verbs.
51
53
  - [Descriptions](#descriptions)
52
54
  - [Errors](#errors)
53
55
  - [Forms](#forms)
54
- - [Grouped inputs](#grouped-inputs)
56
+ - [Shared input options](#shared-input-options)
55
57
  - [Optional inputs](#optional-inputs)
56
58
  - [Translations](#translations)
57
59
  - [Credits](#credits)
@@ -63,18 +65,17 @@ handles your verbs.
63
65
  Add it to your Gemfile:
64
66
 
65
67
  ``` rb
66
- gem 'active_interaction', '~> 4.1'
68
+ gem 'active_interaction', '~> 5.1'
67
69
  ```
68
70
 
69
71
  Or install it manually:
70
72
 
71
73
  ``` sh
72
- $ gem install active_interaction --version '~> 4.1'
74
+ $ gem install active_interaction --version '~> 5.1'
73
75
  ```
74
76
 
75
77
  This project uses [Semantic Versioning][]. Check out [GitHub releases][] for a
76
- detailed list of changes. For help upgrading to version 2, please read [the
77
- announcement post][].
78
+ detailed list of changes.
78
79
 
79
80
  ## Basic usage
80
81
 
@@ -89,7 +90,7 @@ you need to do two things:
89
90
  2. **Define your business logic.** Do this by implementing the `#execute`
90
91
  method. Each input you defined will be available as the type you specified.
91
92
  If any of the inputs are invalid, `#execute` won't be run. Filters are
92
- responsible for type checking your inputs. Check out [the validations
93
+ responsible for checking your inputs. Check out [the validations
93
94
  section](#validations) if you need more than that.
94
95
 
95
96
  That covers the basics. Let's put it all together into a simple example that
@@ -141,7 +142,7 @@ Square.run!(x: 2.1)
141
142
 
142
143
  ### Validations
143
144
 
144
- ActiveInteraction type checks your inputs. Often you'll want more than that.
145
+ ActiveInteraction checks your inputs. Often you'll want more than that.
145
146
  For instance, you may want an input to be a string with at least one
146
147
  non-whitespace character. Instead of writing your own validation for that, you
147
148
  can use validations from ActiveModel.
@@ -164,7 +165,7 @@ end
164
165
  ```
165
166
 
166
167
  When you run this interaction, two things will happen. **First
167
- ActiveInteraction will type check your inputs. Then ActiveModel will validate
168
+ ActiveInteraction will check your inputs. Then ActiveModel will validate
168
169
  them.** If both of those are happy, it will be executed.
169
170
 
170
171
  ``` rb
@@ -214,7 +215,9 @@ alternatives that can be reasonably coerced. Typically the coercions come from
214
215
  Rails, so `"1"` can be interpreted as the boolean value `true`, the string
215
216
  `"1"`, or the number `1`.
216
217
 
217
- ### Array
218
+ ### Basic Filters
219
+
220
+ #### Array
218
221
 
219
222
  In addition to accepting arrays, array inputs will convert
220
223
  `ActiveRecord::Relation`s into arrays.
@@ -261,7 +264,33 @@ array :managers do
261
264
  end
262
265
  ```
263
266
 
264
- ### 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
265
294
 
266
295
  Boolean filters convert the strings `"1"`, `"true"`, and `"on"`
267
296
  (case-insensitive) into `true`. They also convert `"0"`, `"false"`, and `"off"`
@@ -282,7 +311,7 @@ BooleanInteraction.run!(kool_aid: true)
282
311
  # => "Oh yeah!"
283
312
  ```
284
313
 
285
- ### File
314
+ #### File
286
315
 
287
316
  File filters also accept `TempFile`s and anything that responds to `#rewind`.
288
317
  That means that you can pass the `params` from uploading files via forms in
@@ -303,7 +332,7 @@ FileInteraction.run!(readme: File.open('README.md'))
303
332
  # => 21563
304
333
  ```
305
334
 
306
- ### Hash
335
+ #### Hash
307
336
 
308
337
  Hash filters accept hashes. The expected value types are given by passing a
309
338
  block and nesting other filters. You can have any number of filters inside a
@@ -355,165 +384,7 @@ hash :stuff,
355
384
  strip: false
356
385
  ```
357
386
 
358
- ### Interface
359
-
360
- Interface filters allow you to specify an interface that the passed value must
361
- meet in order to pass. The name of the interface is used to look for a constant
362
- inside the ancestor listing for the passed value. This allows for a variety of
363
- checks depending on what's passed. Class instances are checked for an included
364
- module or an inherited ancestor class. Classes are checked for an extended
365
- module or an inherited ancestor class. Modules are checked for an extended
366
- module.
367
-
368
- ``` rb
369
- class InterfaceInteraction < ActiveInteraction::Base
370
- interface :exception
371
-
372
- def execute
373
- exception
374
- end
375
- end
376
-
377
- InterfaceInteraction.run!(exception: Exception)
378
- # ActiveInteraction::InvalidInteractionError: Exception is not a valid interface
379
- InterfaceInteraction.run!(exception: NameError) # a subclass of Exception
380
- # => NameError
381
- ```
382
-
383
- You can use `:from` to specify a class or module. This would be the equivalent
384
- of what's above.
385
-
386
- ```rb
387
- class InterfaceInteraction < ActiveInteraction::Base
388
- interface :error,
389
- from: Exception
390
-
391
- def execute
392
- error
393
- end
394
- end
395
- ```
396
-
397
- You can also create an anonymous interface on the fly by passing the `methods`
398
- option.
399
-
400
- ``` rb
401
- class InterfaceInteraction < ActiveInteraction::Base
402
- interface :serializer,
403
- methods: %i[dump load]
404
-
405
- def execute
406
- input = '{ "is_json" : true }'
407
- object = serializer.load(input)
408
- output = serializer.dump(object)
409
-
410
- output
411
- end
412
- end
413
-
414
- require 'json'
415
-
416
- InterfaceInteraction.run!(serializer: Object.new)
417
- # ActiveInteraction::InvalidInteractionError: Serializer is not a valid interface
418
- InterfaceInteraction.run!(serializer: JSON)
419
- # => "{\"is_json\":true}"
420
- ```
421
-
422
- ### Object
423
-
424
- Object filters allow you to require an instance of a particular class or one of
425
- its subclasses.
426
-
427
- ``` rb
428
- class Cow
429
- def moo
430
- 'Moo!'
431
- end
432
- end
433
-
434
- class ObjectInteraction < ActiveInteraction::Base
435
- object :cow
436
-
437
- def execute
438
- cow.moo
439
- end
440
- end
441
-
442
- ObjectInteraction.run!(cow: Object.new)
443
- # ActiveInteraction::InvalidInteractionError: Cow is not a valid object
444
- ObjectInteraction.run!(cow: Cow.new)
445
- # => "Moo!"
446
- ```
447
-
448
- The class name is automatically determined by the filter name. If your filter
449
- name is different than your class name, use the `class` option. It can be
450
- either the class, a string, or a symbol.
451
-
452
- ``` rb
453
- object :dolly1,
454
- class: Sheep
455
- object :dolly2,
456
- class: 'Sheep'
457
- object :dolly3,
458
- class: :Sheep
459
- ```
460
-
461
- If you have value objects or you would like to build one object from another,
462
- you can use the `converter` option. It is only called if the value provided is
463
- not an instance of the class or one of its subclasses. The `converter` option
464
- accepts a symbol that specifies a class method on the object class or a proc.
465
- Both will be passed the value and any errors thrown inside the converter will
466
- cause the value to be considered invalid. Any returned value that is not the
467
- correct class will also be treated as invalid. The value given to the `default`
468
- option will also be converted.
469
-
470
- ``` rb
471
- class ObjectInteraction < ActiveInteraction::Base
472
- object :ip_address,
473
- class: IPAddr,
474
- converter: :new
475
-
476
- def execute
477
- ip_address
478
- end
479
- end
480
-
481
- ObjectInteraction.run!(ip_address: '192.168.1.1')
482
- # #<IPAddr: IPv4:192.168.1.1/255.255.255.255>
483
-
484
- ObjectInteraction.run!(ip_address: 1)
485
- # ActiveInteraction::InvalidInteractionError: Ip address is not a valid object
486
- ```
487
-
488
- ### Record
489
-
490
- Record filters allow you to require an instance of a particular class (or one
491
- of its subclasses) or a value that can be used to locate an instance of the
492
- object. If the value does not match, it will call `find` on the class of the
493
- record. This is particularly useful when working with ActiveRecord objects.
494
- Like an object filter, the class is derived from the name passed but can be
495
- specified with the `class` option. The value given to the `default` option will
496
- also be found.
497
-
498
- ``` rb
499
- class RecordInteraction < ActiveInteraction::Base
500
- record :encoding
501
-
502
- def execute
503
- encoding
504
- end
505
- end
506
-
507
- > RecordInteraction.run!(encoding: Encoding::US_ASCII)
508
- => #<Encoding:US-ASCII>
509
-
510
- > RecordInteraction.run!(encoding: 'ascii')
511
- => #<Encoding:US-ASCII>
512
- ```
513
-
514
- A different method can be specified by providing a symbol to the `finder` option.
515
-
516
- ### String
387
+ #### String
517
388
 
518
389
  String filters define inputs that only accept strings.
519
390
 
@@ -540,7 +411,7 @@ string :comment,
540
411
  strip: false
541
412
  ```
542
413
 
543
- ### Symbol
414
+ #### Symbol
544
415
 
545
416
  Symbol filters define inputs that accept symbols. Strings will be converted
546
417
  into symbols.
@@ -560,7 +431,7 @@ SymbolInteraction.run!(method: :object_id)
560
431
  # => #<Proc:0x007fdc9ba94118>
561
432
  ```
562
433
 
563
- ### Dates and times
434
+ #### Dates and times
564
435
 
565
436
  Filters that work with dates and times behave similarly. By default, they all
566
437
  convert strings into their expected data types using `.parse`. Blank strings
@@ -568,7 +439,7 @@ will be treated as `nil`. If you give the `format` option, they will instead
568
439
  convert strings using `.strptime`. Note that formats won't work with `DateTime`
569
440
  and `Time` filters if a time zone is set.
570
441
 
571
- #### Date
442
+ ##### Date
572
443
 
573
444
  ``` rb
574
445
  class DateInteraction < ActiveInteraction::Base
@@ -590,7 +461,7 @@ date :birthday,
590
461
  format: '%Y-%m-%d'
591
462
  ```
592
463
 
593
- #### DateTime
464
+ ##### DateTime
594
465
 
595
466
  ``` rb
596
467
  class DateTimeInteraction < ActiveInteraction::Base
@@ -612,7 +483,7 @@ date_time :start,
612
483
  format: '%Y-%m-%dT%H:%M:%S'
613
484
  ```
614
485
 
615
- #### Time
486
+ ##### Time
616
487
 
617
488
  In addition to converting strings with `.parse` (or `.strptime`), time filters
618
489
  convert numbers with `.at`.
@@ -637,13 +508,13 @@ time :start,
637
508
  format: '%Y-%m-%dT%H:%M:%S'
638
509
  ```
639
510
 
640
- ### Numbers
511
+ #### Numbers
641
512
 
642
513
  All numeric filters accept numeric input. They will also convert strings using
643
514
  the appropriate method from `Kernel` (like `.Float`). Blank strings will be
644
515
  treated as `nil`.
645
516
 
646
- #### Decimal
517
+ ##### Decimal
647
518
 
648
519
  ``` rb
649
520
  class DecimalInteraction < ActiveInteraction::Base
@@ -667,7 +538,7 @@ decimal :dollars,
667
538
  digits: 2
668
539
  ```
669
540
 
670
- #### Float
541
+ ##### Float
671
542
 
672
543
  ``` rb
673
544
  class FloatInteraction < ActiveInteraction::Base
@@ -684,7 +555,7 @@ FloatInteraction.run!(x: 2.1)
684
555
  # => 4.41
685
556
  ```
686
557
 
687
- #### Integer
558
+ ##### Integer
688
559
 
689
560
  ``` rb
690
561
  class IntegerInteraction < ActiveInteraction::Base
@@ -725,6 +596,167 @@ IntegerInteraction.run!(limit1: "08", limit2: "08", limit3: "08")
725
596
  ActiveInteraction::InvalidInteractionError: Limit2 is not a valid integer, Limit3 is not a valid integer
726
597
  ```
727
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
+
728
760
  ## Rails
729
761
 
730
762
  ActiveInteraction plays nicely with Rails. You can use interactions to handle
@@ -736,10 +768,7 @@ resourceful actions.
736
768
 
737
769
  We recommend putting your interactions in `app/interactions`. It's also very
738
770
  helpful to group them by model. That way you can look in
739
- `app/interactions/accounts` for all the ways you can interact with accounts. In
740
- order to use this structure add
741
- `config.autoload_paths += Dir.glob("#{config.root}/app/interactions/*")` in
742
- your `application.rb`
771
+ `app/interactions/accounts` for all the ways you can interact with accounts.
743
772
 
744
773
  ```
745
774
  - app/
@@ -842,7 +871,7 @@ end
842
871
  ```
843
872
 
844
873
  Note that it's perfectly fine to add errors during execution. Not all errors
845
- have to come from type checking or validation.
874
+ have to come from checking or validation.
846
875
 
847
876
  #### New
848
877
 
@@ -1025,7 +1054,7 @@ interaction's lifecycle.
1025
1054
 
1026
1055
  ``` rb
1027
1056
  class Increment < ActiveInteraction::Base
1028
- set_callback :type_check, :before, -> { puts 'before type check' }
1057
+ set_callback :filter, :before, -> { puts 'before filter' }
1029
1058
 
1030
1059
  integer :x
1031
1060
 
@@ -1047,7 +1076,7 @@ class Increment < ActiveInteraction::Base
1047
1076
  end
1048
1077
 
1049
1078
  Increment.run!(x: 1)
1050
- # before type check
1079
+ # before filter
1051
1080
  # after validate
1052
1081
  # >>>
1053
1082
  # executing
@@ -1055,7 +1084,7 @@ Increment.run!(x: 1)
1055
1084
  # => 2
1056
1085
  ```
1057
1086
 
1058
- In order, the available callbacks are `type_check`, `validate`, and `execute`.
1087
+ In order, the available callbacks are `filter`, `validate`, and `execute`.
1059
1088
  You can set `before`, `after`, or `around` on any of them.
1060
1089
 
1061
1090
  ### Composition
@@ -1178,7 +1207,7 @@ errors.
1178
1207
  ``` rb
1179
1208
  outcome = BuyItem.run(item: 'Thing', options: { gift_wrapped: 'yes' })
1180
1209
  outcome.errors.messages
1181
- # => {: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"]}
1182
1211
  ```
1183
1212
 
1184
1213
  Determining the type of error based on the string is difficult if not
@@ -1187,7 +1216,7 @@ the same list of errors with a testable label representing the error.
1187
1216
 
1188
1217
  ``` rb
1189
1218
  outcome.errors.details
1190
- # => {: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"}]}
1191
1220
  ```
1192
1221
 
1193
1222
  Detailed errors can also be manually added during the execute call by passing a
@@ -1337,7 +1366,7 @@ used to define the inputs on your interaction will relay type information to
1337
1366
  these gems. As a result, form fields will automatically use the appropriate
1338
1367
  input type.
1339
1368
 
1340
- ### Grouped inputs
1369
+ ### Shared input options
1341
1370
 
1342
1371
  It can be convenient to apply the same options to a bunch of inputs. One common
1343
1372
  use case is making many inputs optional. Instead of setting `default: nil` on
@@ -1358,8 +1387,8 @@ Optional inputs can be defined by using the `:default` option as described in
1358
1387
  are merged to create `inputs`. There are times where it is useful to know
1359
1388
  whether a value was passed to `run` or the result of a filter default. In
1360
1389
  particular, it is useful when `nil` is an acceptable value. For example, you
1361
- may optionally track your users' birthdays. You can use the `given?` predicate
1362
- 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
1363
1392
  the input of a hash or array filter by passing a series of keys or indexes to
1364
1393
  check.
1365
1394
 
@@ -1370,7 +1399,7 @@ class UpdateUser < ActiveInteraction::Base
1370
1399
  default: nil
1371
1400
 
1372
1401
  def execute
1373
- user.birthday = birthday if given?(:birthday)
1402
+ user.birthday = birthday if inputs.given?(:birthday)
1374
1403
  errors.merge!(user.errors) unless user.save
1375
1404
  user
1376
1405
  end
@@ -1424,7 +1453,6 @@ hsilgne:
1424
1453
  errors:
1425
1454
  messages:
1426
1455
  invalid: dilavni si
1427
- invalid_nested: (%{value} <= %{name}) eulav detsen dilavni na sah
1428
1456
  invalid_type: '%{type} dilav a ton si'
1429
1457
  missing: deriuqer si
1430
1458
  ```
@@ -1444,6 +1472,19 @@ I18nInteraction.run(name: false).errors.messages[:name]
1444
1472
  # => ["gnirts dilav a ton si"]
1445
1473
  ```
1446
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
+
1447
1488
  ## Credits
1448
1489
 
1449
1490
  ActiveInteraction is brought to you by [Aaron Lasseigne][].
@@ -1457,13 +1498,13 @@ ActiveInteraction is licensed under [the MIT License][].
1457
1498
 
1458
1499
  [activeinteraction]: https://github.com/AaronLasseigne/active_interaction
1459
1500
  [API Documentation]: http://rubydoc.info/github/AaronLasseigne/active_interaction
1460
- [semantic versioning]: http://semver.org/spec/v2.0.0.html
1501
+ [Semantic Versioning]: http://semver.org/spec/v2.0.0.html
1461
1502
  [GitHub releases]: https://github.com/AaronLasseigne/active_interaction/releases
1462
1503
  [aaron lasseigne]: https://github.com/AaronLasseigne
1463
1504
  [taylor fausak]: https://github.com/tfausak
1464
1505
  [our contribution guidelines]: CONTRIBUTING.md
1465
1506
  [complete list of contributors]: https://github.com/AaronLasseigne/active_interaction/graphs/contributors
1466
- [the mit license]: LICENSE.md
1507
+ [the MIT License]: LICENSE.md
1467
1508
  [formtastic]: https://rubygems.org/gems/formtastic
1468
1509
  [simple_form]: https://rubygems.org/gems/simple_form
1469
1510
  [the filters section]: #filters