active_interaction 4.0.5 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +149 -6
  3. data/README.md +67 -32
  4. data/lib/active_interaction/array_input.rb +77 -0
  5. data/lib/active_interaction/base.rb +14 -98
  6. data/lib/active_interaction/concerns/active_recordable.rb +3 -3
  7. data/lib/active_interaction/concerns/missable.rb +2 -2
  8. data/lib/active_interaction/errors.rb +6 -88
  9. data/lib/active_interaction/exceptions.rb +47 -0
  10. data/lib/active_interaction/filter/column.rb +59 -0
  11. data/lib/active_interaction/filter/error.rb +40 -0
  12. data/lib/active_interaction/filter.rb +44 -53
  13. data/lib/active_interaction/filters/abstract_date_time_filter.rb +9 -6
  14. data/lib/active_interaction/filters/abstract_numeric_filter.rb +7 -3
  15. data/lib/active_interaction/filters/array_filter.rb +36 -10
  16. data/lib/active_interaction/filters/boolean_filter.rb +4 -3
  17. data/lib/active_interaction/filters/date_filter.rb +1 -1
  18. data/lib/active_interaction/filters/date_time_filter.rb +1 -1
  19. data/lib/active_interaction/filters/decimal_filter.rb +1 -1
  20. data/lib/active_interaction/filters/float_filter.rb +1 -1
  21. data/lib/active_interaction/filters/hash_filter.rb +23 -15
  22. data/lib/active_interaction/filters/integer_filter.rb +1 -1
  23. data/lib/active_interaction/filters/interface_filter.rb +12 -12
  24. data/lib/active_interaction/filters/object_filter.rb +9 -3
  25. data/lib/active_interaction/filters/record_filter.rb +21 -11
  26. data/lib/active_interaction/filters/string_filter.rb +1 -1
  27. data/lib/active_interaction/filters/symbol_filter.rb +1 -1
  28. data/lib/active_interaction/filters/time_filter.rb +4 -4
  29. data/lib/active_interaction/hash_input.rb +43 -0
  30. data/lib/active_interaction/input.rb +23 -0
  31. data/lib/active_interaction/inputs.rb +157 -46
  32. data/lib/active_interaction/locale/en.yml +0 -1
  33. data/lib/active_interaction/locale/fr.yml +0 -1
  34. data/lib/active_interaction/locale/it.yml +0 -1
  35. data/lib/active_interaction/locale/ja.yml +0 -1
  36. data/lib/active_interaction/locale/pt-BR.yml +0 -1
  37. data/lib/active_interaction/modules/validation.rb +6 -17
  38. data/lib/active_interaction/version.rb +1 -1
  39. data/lib/active_interaction.rb +43 -36
  40. data/spec/active_interaction/array_input_spec.rb +166 -0
  41. data/spec/active_interaction/base_spec.rb +15 -240
  42. data/spec/active_interaction/concerns/active_modelable_spec.rb +3 -3
  43. data/spec/active_interaction/concerns/active_recordable_spec.rb +7 -7
  44. data/spec/active_interaction/concerns/hashable_spec.rb +8 -8
  45. data/spec/active_interaction/concerns/missable_spec.rb +9 -9
  46. data/spec/active_interaction/concerns/runnable_spec.rb +34 -32
  47. data/spec/active_interaction/errors_spec.rb +60 -43
  48. data/spec/active_interaction/{filter_column_spec.rb → filter/column_spec.rb} +3 -10
  49. data/spec/active_interaction/filter_spec.rb +6 -6
  50. data/spec/active_interaction/filters/abstract_date_time_filter_spec.rb +2 -2
  51. data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +2 -2
  52. data/spec/active_interaction/filters/array_filter_spec.rb +99 -24
  53. data/spec/active_interaction/filters/boolean_filter_spec.rb +12 -11
  54. data/spec/active_interaction/filters/date_filter_spec.rb +32 -27
  55. data/spec/active_interaction/filters/date_time_filter_spec.rb +34 -29
  56. data/spec/active_interaction/filters/decimal_filter_spec.rb +20 -18
  57. data/spec/active_interaction/filters/file_filter_spec.rb +7 -7
  58. data/spec/active_interaction/filters/float_filter_spec.rb +19 -17
  59. data/spec/active_interaction/filters/hash_filter_spec.rb +16 -18
  60. data/spec/active_interaction/filters/integer_filter_spec.rb +24 -22
  61. data/spec/active_interaction/filters/interface_filter_spec.rb +105 -82
  62. data/spec/active_interaction/filters/object_filter_spec.rb +52 -36
  63. data/spec/active_interaction/filters/record_filter_spec.rb +61 -39
  64. data/spec/active_interaction/filters/string_filter_spec.rb +7 -7
  65. data/spec/active_interaction/filters/symbol_filter_spec.rb +6 -6
  66. data/spec/active_interaction/filters/time_filter_spec.rb +57 -34
  67. data/spec/active_interaction/hash_input_spec.rb +58 -0
  68. data/spec/active_interaction/i18n_spec.rb +22 -17
  69. data/spec/active_interaction/inputs_spec.rb +167 -23
  70. data/spec/active_interaction/integration/array_interaction_spec.rb +3 -7
  71. data/spec/active_interaction/modules/validation_spec.rb +8 -31
  72. data/spec/spec_helper.rb +8 -0
  73. data/spec/support/concerns.rb +2 -2
  74. data/spec/support/filters.rb +27 -51
  75. data/spec/support/interactions.rb +4 -4
  76. metadata +45 -95
  77. data/lib/active_interaction/filter_column.rb +0 -57
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc966bbcee80b29757efdf5bf0b6b0357b8d24610dca49017f4a8cdca64bfaa3
4
- data.tar.gz: fe255f995b77c90c935fb14ae05660fcc973a3e7ef3e26209d5e1e187624ca21
3
+ metadata.gz: 0cfed756a5463384a07d694ba40f14067dce231d2618af2f16e6fd429e31b97f
4
+ data.tar.gz: eb3b185d234762dec1799fbaa6e8acf290a47e0940b24c3da529f11172e6fde5
5
5
  SHA512:
6
- metadata.gz: 233b8b4334a5f4d34d12607c054a47aa4839f9c734cd251b682f30173aaa5115e0d8292c0074efd21137a966831b635d2a4e4f3c55f6cb282bb78ed6f795450e
7
- data.tar.gz: a5fef4055b48c4c72f3f297f52260f0fe4055a2299961d0fd70ebfd7fcfadc2814628a01cec947259e4b99620fc9a2ee313baaa373449aadeba62829eea7be00
6
+ metadata.gz: b2ce2d6ef28bc84e2cc614dd9e6dde57c14a92efcafe706e8cb532b1c0fa9c192db8548771b94198236f3ab3ec6baa9633c2625c3f8b73cbcc1291752078be6c
7
+ data.tar.gz: 3f5f3c15059c19c7fec77256a260fc67f98444ab5dba48048f2797a441a348f7deea31b7b558e0b1af334ffd8a6e1685756745ecebdc87ee658dafc42f8043c4
data/CHANGELOG.md CHANGED
@@ -1,32 +1,169 @@
1
+ # [5.0.0][] (2022-06-24)
2
+
3
+ ## Changed
4
+
5
+ - Drop support for JRuby.
6
+ - Drop support for Ruby 2.5 and 2.6, adding support for 3.1
7
+ - Drop support for Rails 5.0 and 5.1
8
+ - `ActiveInteraction::Inputs` no longer inherits from `Hash` though it still has most of the methods
9
+ provided by `Hash` (methods that write were removed).
10
+ - Removed `Filter#clean` (use `Filter#process` and call `#value` on the result)
11
+ - The `given?` method has been moved onto `inputs`. ([how to upgrade](#given))
12
+ - [#503][] - The record filter now treats blank strings value as `nil`. This was missed in the 4.0 update.
13
+ - The `type_check` callback has been renamed to `filter` to better match the reality of what it does.
14
+ ([how to upgrade](#filter-callback))
15
+ - `ActiveIneraction::FilterColumn` is now `ActiveInteraction::Filter::Column`
16
+ - Errors on the array filter will now be indexed if the Rails config `index_nested_attribute_errors`
17
+ is `true` or the `:index_errors` option is set to `true`. The `:index_errors` option always overrides
18
+ the Rails config.
19
+ - Invalid nested errors (`:invalid_nested`) are gone. Instead the nested errors will appear as they would
20
+ in Rails if they were a `has_many` relationship being assigned attributes through a parent.
21
+ ([how to upgrade](#nested-hash-errors))
22
+
23
+ ## Added
24
+
25
+ - `Filter#process` which returns an `Input`.
26
+
27
+ ## Fixed
28
+
29
+ - When passing an `ActiveRecord::Relation` in an array filter with no inner
30
+ filter, the value returned was an `ActiveRecord::Relation` instead of an
31
+ Array.
32
+
33
+ ## Upgrading
34
+
35
+ ### `given?`
36
+
37
+ The `given?` method can now be found on `inputs`. It works the same as before.
38
+
39
+ ```ruby
40
+ # 4.1
41
+ class Example < ActiveInteraction::Base
42
+ string :name, default: nil
43
+
44
+ def execute
45
+ given?(:name)
46
+ end
47
+ end
48
+
49
+ # 5.0
50
+ class Example < ActiveInteraction::Base
51
+ string :name, default: nil
52
+
53
+ def execute
54
+ inputs.given?(:name)
55
+ end
56
+ end
57
+ ```
58
+
59
+ ### Filter Callback
60
+
61
+ You'll need to rename any `:type_check` callbacks to `:filter`.
62
+
63
+ ```ruby
64
+ # 4.1
65
+ set_callback :type_check, :before, -> { puts 'before type check' }
66
+
67
+ # 5.0
68
+ set_callback :filter, :before, -> { puts 'before type check' }
69
+ ```
70
+
71
+ ### Nested Hash Errors
72
+
73
+ Nested hash errors no longer add an error as through it happened on the hash.
74
+ They now use the error in its original form and attach the name of the hash to
75
+ the error. It is also not limited to returning one error.
76
+
77
+ ```ruby
78
+ class HashInteraction < ActiveInteraction::Base
79
+ hash :mailing_lists do
80
+ boolean :marketing
81
+ boolean :product_updates
82
+ end
83
+
84
+ def execute
85
+ # ...
86
+ end
87
+ end
88
+
89
+ > outcome = HashInteraction.run(mailing_lists: {})
90
+
91
+ # 4.1
92
+ > outcome.errors.details
93
+ # => {:mailing_lists=>[{:error=>:invalid_nested, :name=>"\"marketing\"", :value=>"nil"}]},
94
+ > outcome.errors.messages
95
+ # => {:mailing_lists=>["has an invalid nested value (\"marketing\" => nil)"]}
96
+ > outcome.errors.full_messages
97
+ # => ["Mailing lists has an invalid nested value (\"marketing\" => nil)"]
98
+
99
+ # 5.0
100
+ > outcome.errors.details
101
+ # => {:"mailing_lists.marketing"=>[{:error=>:missing}], :"mailing_lists.product_updates"=>[{:error=>:missing}]}
102
+ > outcome.errors.messages
103
+ # => {:"mailing_lists.marketing"=>["is required"], :"mailing_lists.product_updates"=>["is required"]}
104
+ > outcome.errors.full_messages
105
+ # => ["Mailing lists marketing is required", "Mailing lists product updates is required"]
106
+ ```
107
+
108
+ I18n can handle these values the same as nested values in Rails:
109
+
110
+ ```yml
111
+ en:
112
+ active_interaction:
113
+ attributes:
114
+ hash_interaction/mailing_lists:
115
+ marketing: 'Mailing list "Marketing"'
116
+ product_updates: 'Mailing list "Product Updates"'
117
+ ```
118
+
119
+ Using the same example from above:
120
+
121
+ ```ruby
122
+ > outcome.errors.full_messages
123
+ # => ["Mailing list \"Marketing\" is required", "Mailing list \"Product Updates\" is required"]
124
+ ```
125
+
126
+ # [4.1.0][] (2021-12-30)
127
+
128
+ ## Added
129
+
130
+ - [#518][] - Add Rails 7 support
131
+
132
+ # [4.0.6][] (2021-10-13)
133
+
134
+ ## Fixed
135
+
136
+ - [#515][] - Filters nested in arrays should accept default values as indicated in the documentation.
137
+
1
138
  # [4.0.5][] (2021-07-11)
2
139
 
3
- ## Fix
140
+ ## Fixed
4
141
 
5
142
  - [#480][] - Interfaces used inside hashes failed to recognize `nil` as a non-value.
6
143
 
7
144
  # [4.0.4][] (2021-07-03)
8
145
 
9
- ## Fix
146
+ ## Fixed
10
147
 
11
148
  - [#510][] - Hash parameters failed when working outside of Rails.
12
149
  - [#511][] - Nested filters with options but no `:class` failed to have `:class` automatically added.
13
150
 
14
151
  # [4.0.3][] (2021-06-24)
15
152
 
16
- ## Fix
153
+ ## Fixed
17
154
 
18
155
  - [#499][] - `given?` now recognizes multi-part date inputs by their primary key name
19
156
  - [#493][] - `compose` now properly accepts `Inputs`
20
157
 
21
158
  # [4.0.2][] (2021-06-22)
22
159
 
23
- ## Fix
160
+ ## Fixed
24
161
 
25
162
  - [#505][] - Nested Interface filters using the `:methods` option threw an error.
26
163
 
27
164
  # [4.0.1][] (2021-05-26)
28
165
 
29
- ## Fix
166
+ ## Fixed
30
167
 
31
168
  - Fix regression of filter name relaxing.
32
169
  - [#495][] - Fix time filter ignoring time zones
@@ -56,7 +193,7 @@
56
193
 
57
194
  ## Added
58
195
 
59
- - Implicit coercion of types are now supported in filters (e.g. to_str, to_int,
196
+ - Implicit coercion of types are now supported in filters (e.g. `to_str`, `to_int`,
60
197
  etc).
61
198
  - The `interface` and `record` filters, when used as an inner filter for an
62
199
  `array`, will have their `from/class` option set to a singularized version of
@@ -964,6 +1101,9 @@ Example.run
964
1101
 
965
1102
  - Initial release.
966
1103
 
1104
+ [5.0.0]: https://github.com/AaronLasseigne/active_interaction/compare/v4.1.0...v5.0.0
1105
+ [4.1.0]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.6...v4.1.0
1106
+ [4.0.6]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.5...v4.0.6
967
1107
  [4.0.5]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.4...v4.0.5
968
1108
  [4.0.4]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.3...v4.0.4
969
1109
  [4.0.3]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.2...v4.0.3
@@ -1182,3 +1322,6 @@ Example.run
1182
1322
  [#511]: https://github.com/AaronLasseigne/active_interaction/issues/511
1183
1323
  [#412]: https://github.com/AaronLasseigne/active_interaction/issues/412
1184
1324
  [#480]: https://github.com/AaronLasseigne/active_interaction/issues/480
1325
+ [#515]: https://github.com/AaronLasseigne/active_interaction/issues/515
1326
+ [#518]: https://github.com/AaronLasseigne/active_interaction/issues/518
1327
+ [#503]: https://github.com/AaronLasseigne/active_interaction/issues/503
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
 
@@ -52,7 +51,7 @@ handles your verbs.
52
51
  - [Descriptions](#descriptions)
53
52
  - [Errors](#errors)
54
53
  - [Forms](#forms)
55
- - [Grouped inputs](#grouped-inputs)
54
+ - [Shared input options](#shared-input-options)
56
55
  - [Optional inputs](#optional-inputs)
57
56
  - [Translations](#translations)
58
57
  - [Credits](#credits)
@@ -64,18 +63,17 @@ handles your verbs.
64
63
  Add it to your Gemfile:
65
64
 
66
65
  ``` rb
67
- gem 'active_interaction', '~> 4.0'
66
+ gem 'active_interaction', '~> 5.0'
68
67
  ```
69
68
 
70
69
  Or install it manually:
71
70
 
72
71
  ``` sh
73
- $ gem install active_interaction --version '~> 4.0'
72
+ $ gem install active_interaction --version '~> 5.0'
74
73
  ```
75
74
 
76
75
  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][].
76
+ detailed list of changes.
79
77
 
80
78
  ## Basic usage
81
79
 
@@ -90,7 +88,7 @@ you need to do two things:
90
88
  2. **Define your business logic.** Do this by implementing the `#execute`
91
89
  method. Each input you defined will be available as the type you specified.
92
90
  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
91
+ responsible for checking your inputs. Check out [the validations
94
92
  section](#validations) if you need more than that.
95
93
 
96
94
  That covers the basics. Let's put it all together into a simple example that
@@ -142,7 +140,7 @@ Square.run!(x: 2.1)
142
140
 
143
141
  ### Validations
144
142
 
145
- ActiveInteraction type checks your inputs. Often you'll want more than that.
143
+ ActiveInteraction checks your inputs. Often you'll want more than that.
146
144
  For instance, you may want an input to be a string with at least one
147
145
  non-whitespace character. Instead of writing your own validation for that, you
148
146
  can use validations from ActiveModel.
@@ -165,7 +163,7 @@ end
165
163
  ```
166
164
 
167
165
  When you run this interaction, two things will happen. **First
168
- ActiveInteraction will type check your inputs. Then ActiveModel will validate
166
+ ActiveInteraction will check your inputs. Then ActiveModel will validate
169
167
  them.** If both of those are happy, it will be executed.
170
168
 
171
169
  ``` rb
@@ -262,6 +260,32 @@ array :managers do
262
260
  end
263
261
  ```
264
262
 
263
+ Errors that occur will be indexed based on the Rails configuration setting
264
+ `index_nested_attribute_errors`. You can also manually override this setting
265
+ with the `:index_errors` option. In this state is is possible to get multiple
266
+ errors from a single filter.
267
+
268
+ ```ruby
269
+ class ArrayInteraction < ActiveInteraction::Base
270
+ array :favorite_numbers, index_errors: true do
271
+ integer
272
+ end
273
+
274
+ def execute
275
+ favorite_numbers
276
+ end
277
+ end
278
+
279
+ ArrayInteraction.run(favorite_numbers: [8, 'bazillion']).errors.details
280
+ => {:"favorite_numbers[1]"=>[{:error=>:invalid_type, :type=>"array"}]}
281
+ ```
282
+
283
+ With `:index_errors` set to `false` the error would have been:
284
+
285
+ ```ruby
286
+ {:favorite_numbers=>[{:error=>:invalid_type, :type=>"array"}]}
287
+ ```
288
+
265
289
  ### Boolean
266
290
 
267
291
  Boolean filters convert the strings `"1"`, `"true"`, and `"on"`
@@ -465,8 +489,8 @@ not an instance of the class or one of its subclasses. The `converter` option
465
489
  accepts a symbol that specifies a class method on the object class or a proc.
466
490
  Both will be passed the value and any errors thrown inside the converter will
467
491
  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.
492
+ correct class will also be treated as invalid. Any `default` that is not an
493
+ instance of the class or subclass and is not `nil` will also be converted.
470
494
 
471
495
  ``` rb
472
496
  class ObjectInteraction < ActiveInteraction::Base
@@ -493,8 +517,9 @@ of its subclasses) or a value that can be used to locate an instance of the
493
517
  object. If the value does not match, it will call `find` on the class of the
494
518
  record. This is particularly useful when working with ActiveRecord objects.
495
519
  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.
520
+ specified with the `class` option. Any `default` that is not an instance of the
521
+ class or subclass and is not `nil` will also be found. Blank strings passed in
522
+ will be treated as `nil`.
498
523
 
499
524
  ``` rb
500
525
  class RecordInteraction < ActiveInteraction::Base
@@ -737,10 +762,7 @@ resourceful actions.
737
762
 
738
763
  We recommend putting your interactions in `app/interactions`. It's also very
739
764
  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`
765
+ `app/interactions/accounts` for all the ways you can interact with accounts.
744
766
 
745
767
  ```
746
768
  - app/
@@ -843,7 +865,7 @@ end
843
865
  ```
844
866
 
845
867
  Note that it's perfectly fine to add errors during execution. Not all errors
846
- have to come from type checking or validation.
868
+ have to come from checking or validation.
847
869
 
848
870
  #### New
849
871
 
@@ -1020,13 +1042,13 @@ end
1020
1042
 
1021
1043
  ### Callbacks
1022
1044
 
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.
1045
+ [ActiveSupport::Callbacks][] provides a powerful framework for defining callbacks.
1046
+ ActiveInteraction uses that framework to allow hooking into various parts of an
1047
+ interaction's lifecycle.
1026
1048
 
1027
1049
  ``` rb
1028
1050
  class Increment < ActiveInteraction::Base
1029
- set_callback :type_check, :before, -> { puts 'before type check' }
1051
+ set_callback :filter, :before, -> { puts 'before filter' }
1030
1052
 
1031
1053
  integer :x
1032
1054
 
@@ -1048,7 +1070,7 @@ class Increment < ActiveInteraction::Base
1048
1070
  end
1049
1071
 
1050
1072
  Increment.run!(x: 1)
1051
- # before type check
1073
+ # before filter
1052
1074
  # after validate
1053
1075
  # >>>
1054
1076
  # executing
@@ -1056,7 +1078,7 @@ Increment.run!(x: 1)
1056
1078
  # => 2
1057
1079
  ```
1058
1080
 
1059
- In order, the available callbacks are `type_check`, `validate`, and `execute`.
1081
+ In order, the available callbacks are `filter`, `validate`, and `execute`.
1060
1082
  You can set `before`, `after`, or `around` on any of them.
1061
1083
 
1062
1084
  ### Composition
@@ -1179,7 +1201,7 @@ errors.
1179
1201
  ``` rb
1180
1202
  outcome = BuyItem.run(item: 'Thing', options: { gift_wrapped: 'yes' })
1181
1203
  outcome.errors.messages
1182
- # => {:credit_card=>["is required"], :item=>["is not a valid object"], :options=>["has an invalid nested value (\"gift_wrapped\" => \"yes\")"]}
1204
+ # => {:credit_card=>["is required"], :item=>["is not a valid object"], :"options.gift_wrapped"=>["is not a valid boolean"]}
1183
1205
  ```
1184
1206
 
1185
1207
  Determining the type of error based on the string is difficult if not
@@ -1188,7 +1210,7 @@ the same list of errors with a testable label representing the error.
1188
1210
 
1189
1211
  ``` rb
1190
1212
  outcome.errors.details
1191
- # => {:credit_card=>[{:error=>:missing}], :item=>[{:type=>"object", :error=>:invalid_type}], :options=>[{:name=>"\"gift_wrapped\"", :value=>"\"yes\"", :error=>:invalid_nested}]}
1213
+ # => {:credit_card=>[{:error=>:missing}], :item=>[{:error=>:invalid_type, :type=>"object"}], :"options.gift_wrapped"=>[{:error=>:invalid_type, :type=>"boolean"}]}
1192
1214
  ```
1193
1215
 
1194
1216
  Detailed errors can also be manually added during the execute call by passing a
@@ -1338,7 +1360,7 @@ used to define the inputs on your interaction will relay type information to
1338
1360
  these gems. As a result, form fields will automatically use the appropriate
1339
1361
  input type.
1340
1362
 
1341
- ### Grouped inputs
1363
+ ### Shared input options
1342
1364
 
1343
1365
  It can be convenient to apply the same options to a bunch of inputs. One common
1344
1366
  use case is making many inputs optional. Instead of setting `default: nil` on
@@ -1359,8 +1381,8 @@ Optional inputs can be defined by using the `:default` option as described in
1359
1381
  are merged to create `inputs`. There are times where it is useful to know
1360
1382
  whether a value was passed to `run` or the result of a filter default. In
1361
1383
  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
1384
+ may optionally track your users' birthdays. You can use the `inputs.given?` predicate
1385
+ to see if an input was even passed to `run`. With `inputs.given?` you can also check
1364
1386
  the input of a hash or array filter by passing a series of keys or indexes to
1365
1387
  check.
1366
1388
 
@@ -1371,7 +1393,7 @@ class UpdateUser < ActiveInteraction::Base
1371
1393
  default: nil
1372
1394
 
1373
1395
  def execute
1374
- user.birthday = birthday if given?(:birthday)
1396
+ user.birthday = birthday if inputs.given?(:birthday)
1375
1397
  errors.merge!(user.errors) unless user.save
1376
1398
  user
1377
1399
  end
@@ -1425,7 +1447,6 @@ hsilgne:
1425
1447
  errors:
1426
1448
  messages:
1427
1449
  invalid: dilavni si
1428
- invalid_nested: (%{value} <= %{name}) eulav detsen dilavni na sah
1429
1450
  invalid_type: '%{type} dilav a ton si'
1430
1451
  missing: deriuqer si
1431
1452
  ```
@@ -1445,6 +1466,19 @@ I18nInteraction.run(name: false).errors.messages[:name]
1445
1466
  # => ["gnirts dilav a ton si"]
1446
1467
  ```
1447
1468
 
1469
+ Everything else works like an `activerecord` entry. For example, to rename an
1470
+ attribute you can use `attributes`.
1471
+
1472
+ Here we'll rename the `num` attribute on an interaction named `product`:
1473
+
1474
+ ``` yml
1475
+ en:
1476
+ active_interaction:
1477
+ attributes:
1478
+ product:
1479
+ num: 'Number'
1480
+ ```
1481
+
1448
1482
  ## Credits
1449
1483
 
1450
1484
  ActiveInteraction is brought to you by [Aaron Lasseigne][].
@@ -1471,3 +1505,4 @@ ActiveInteraction is licensed under [the MIT License][].
1471
1505
  [the errors section]: #errors
1472
1506
  [the optional inputs section]: #optional-inputs
1473
1507
  [`with_options`]: http://api.rubyonrails.org/classes/Object.html#method-i-with_options
1508
+ [ActiveSupport::Callbacks]: https://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteraction
4
+ # Represents a processed array input.
5
+ class ArrayInput < Input
6
+ # @private
7
+ def initialize(filter, value: nil, error: nil, index_errors: false, children: [])
8
+ super(filter, value: value, error: error)
9
+
10
+ @filter = filter
11
+ @index_errors = index_errors
12
+ @children = children
13
+ end
14
+
15
+ # @overload children
16
+ # Child inputs if a nested filter is used.
17
+ #
18
+ # @return [Array<Input, ArrayInput, HashInput>]
19
+ attr_reader :children
20
+
21
+ # Any errors that occurred during processing.
22
+ #
23
+ # @return [Filter::Error]
24
+ def errors
25
+ return @errors if defined?(@errors)
26
+
27
+ return @errors = super if @error
28
+
29
+ child_errors = get_errors_by_index(children)
30
+
31
+ return @errors = super if child_errors.empty?
32
+
33
+ @errors ||=
34
+ if @index_errors
35
+ child_errors.map do |(error, i)|
36
+ name = attach_child_name(:"#{@filter.name}[#{i}]", error)
37
+ Filter::Error.new(error.filter, error.type, name: name)
38
+ end.freeze
39
+ else
40
+ error, = child_errors.first
41
+ [Filter::Error.new(@filter, error.type)].freeze
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def get_errors_by_index(children)
48
+ children.flat_map.with_index do |child, i|
49
+ child.errors.map do |error|
50
+ [error, i]
51
+ end
52
+ end
53
+ end
54
+
55
+ def attach_child_name(name, error)
56
+ return name unless error.name.present?
57
+
58
+ if children_are_arrays?(children)
59
+ :"#{name}#{error.name.to_s.sub(/\A[^\[]*/, '')}"
60
+ elsif children_are_hashes?(children)
61
+ :"#{name}.#{error.name.to_s[1..]}"
62
+ end
63
+ end
64
+
65
+ def children_are_arrays?(children)
66
+ return @children_are_arrays if defined?(@children_are_arrays)
67
+
68
+ @children_are_arrays = children.first&.is_a?(ArrayInput)
69
+ end
70
+
71
+ def children_are_hashes?(children)
72
+ return @children_are_hashes if defined?(@children_are_hashes)
73
+
74
+ @children_are_hashes = children.first&.is_a?(HashInput)
75
+ end
76
+ end
77
+ end