active_interaction 4.1.0 → 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 +128 -1
  3. data/README.md +63 -28
  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 +34 -6
  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 -16
  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 +40 -91
  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: 557de518b6e1a33937d70d31e6c29dd6179384372e082595f8087b3efcdae784
4
- data.tar.gz: 2ba312b6ddc61043acbc8bcb48dbbc4a60010be6701a5a368325d0a14fc4c557
3
+ metadata.gz: 0cfed756a5463384a07d694ba40f14067dce231d2618af2f16e6fd429e31b97f
4
+ data.tar.gz: eb3b185d234762dec1799fbaa6e8acf290a47e0940b24c3da529f11172e6fde5
5
5
  SHA512:
6
- metadata.gz: 28674a0400c7bd8dafaae7430f1609fdbd3cd93e3ca5c60a07d2df2970b825a7e218d2482026f0a6365ba11fdd6518830fdfb96f496909ee7d8cb63154c5b8e7
7
- data.tar.gz: e527210922d3e9af79ff3c61f2e5d1ba873fe9eb31be543ffd640d255f69de19b40d0621ff9900db19e3cf3f553c8cfbcf583954385d2c8a3db2f24a747037f7
6
+ metadata.gz: b2ce2d6ef28bc84e2cc614dd9e6dde57c14a92efcafe706e8cb532b1c0fa9c192db8548771b94198236f3ab3ec6baa9633c2625c3f8b73cbcc1291752078be6c
7
+ data.tar.gz: 3f5f3c15059c19c7fec77256a260fc67f98444ab5dba48048f2797a441a348f7deea31b7b558e0b1af334ffd8a6e1685756745ecebdc87ee658dafc42f8043c4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,128 @@
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
+
1
126
  # [4.1.0][] (2021-12-30)
2
127
 
3
128
  ## Added
@@ -68,7 +193,7 @@
68
193
 
69
194
  ## Added
70
195
 
71
- - 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`,
72
197
  etc).
73
198
  - The `interface` and `record` filters, when used as an inner filter for an
74
199
  `array`, will have their `from/class` option set to a singularized version of
@@ -976,6 +1101,7 @@ Example.run
976
1101
 
977
1102
  - Initial release.
978
1103
 
1104
+ [5.0.0]: https://github.com/AaronLasseigne/active_interaction/compare/v4.1.0...v5.0.0
979
1105
  [4.1.0]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.6...v4.1.0
980
1106
  [4.0.6]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.5...v4.0.6
981
1107
  [4.0.5]: https://github.com/AaronLasseigne/active_interaction/compare/v4.0.4...v4.0.5
@@ -1198,3 +1324,4 @@ Example.run
1198
1324
  [#480]: https://github.com/AaronLasseigne/active_interaction/issues/480
1199
1325
  [#515]: https://github.com/AaronLasseigne/active_interaction/issues/515
1200
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,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)
@@ -51,7 +51,7 @@ handles your verbs.
51
51
  - [Descriptions](#descriptions)
52
52
  - [Errors](#errors)
53
53
  - [Forms](#forms)
54
- - [Grouped inputs](#grouped-inputs)
54
+ - [Shared input options](#shared-input-options)
55
55
  - [Optional inputs](#optional-inputs)
56
56
  - [Translations](#translations)
57
57
  - [Credits](#credits)
@@ -63,18 +63,17 @@ handles your verbs.
63
63
  Add it to your Gemfile:
64
64
 
65
65
  ``` rb
66
- gem 'active_interaction', '~> 4.1'
66
+ gem 'active_interaction', '~> 5.0'
67
67
  ```
68
68
 
69
69
  Or install it manually:
70
70
 
71
71
  ``` sh
72
- $ gem install active_interaction --version '~> 4.1'
72
+ $ gem install active_interaction --version '~> 5.0'
73
73
  ```
74
74
 
75
75
  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][].
76
+ detailed list of changes.
78
77
 
79
78
  ## Basic usage
80
79
 
@@ -89,7 +88,7 @@ you need to do two things:
89
88
  2. **Define your business logic.** Do this by implementing the `#execute`
90
89
  method. Each input you defined will be available as the type you specified.
91
90
  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
91
+ responsible for checking your inputs. Check out [the validations
93
92
  section](#validations) if you need more than that.
94
93
 
95
94
  That covers the basics. Let's put it all together into a simple example that
@@ -141,7 +140,7 @@ Square.run!(x: 2.1)
141
140
 
142
141
  ### Validations
143
142
 
144
- ActiveInteraction type checks your inputs. Often you'll want more than that.
143
+ ActiveInteraction checks your inputs. Often you'll want more than that.
145
144
  For instance, you may want an input to be a string with at least one
146
145
  non-whitespace character. Instead of writing your own validation for that, you
147
146
  can use validations from ActiveModel.
@@ -164,7 +163,7 @@ end
164
163
  ```
165
164
 
166
165
  When you run this interaction, two things will happen. **First
167
- ActiveInteraction will type check your inputs. Then ActiveModel will validate
166
+ ActiveInteraction will check your inputs. Then ActiveModel will validate
168
167
  them.** If both of those are happy, it will be executed.
169
168
 
170
169
  ``` rb
@@ -261,6 +260,32 @@ array :managers do
261
260
  end
262
261
  ```
263
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
+
264
289
  ### Boolean
265
290
 
266
291
  Boolean filters convert the strings `"1"`, `"true"`, and `"on"`
@@ -464,8 +489,8 @@ not an instance of the class or one of its subclasses. The `converter` option
464
489
  accepts a symbol that specifies a class method on the object class or a proc.
465
490
  Both will be passed the value and any errors thrown inside the converter will
466
491
  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.
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.
469
494
 
470
495
  ``` rb
471
496
  class ObjectInteraction < ActiveInteraction::Base
@@ -492,8 +517,9 @@ of its subclasses) or a value that can be used to locate an instance of the
492
517
  object. If the value does not match, it will call `find` on the class of the
493
518
  record. This is particularly useful when working with ActiveRecord objects.
494
519
  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.
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`.
497
523
 
498
524
  ``` rb
499
525
  class RecordInteraction < ActiveInteraction::Base
@@ -736,10 +762,7 @@ resourceful actions.
736
762
 
737
763
  We recommend putting your interactions in `app/interactions`. It's also very
738
764
  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`
765
+ `app/interactions/accounts` for all the ways you can interact with accounts.
743
766
 
744
767
  ```
745
768
  - app/
@@ -842,7 +865,7 @@ end
842
865
  ```
843
866
 
844
867
  Note that it's perfectly fine to add errors during execution. Not all errors
845
- have to come from type checking or validation.
868
+ have to come from checking or validation.
846
869
 
847
870
  #### New
848
871
 
@@ -1025,7 +1048,7 @@ interaction's lifecycle.
1025
1048
 
1026
1049
  ``` rb
1027
1050
  class Increment < ActiveInteraction::Base
1028
- set_callback :type_check, :before, -> { puts 'before type check' }
1051
+ set_callback :filter, :before, -> { puts 'before filter' }
1029
1052
 
1030
1053
  integer :x
1031
1054
 
@@ -1047,7 +1070,7 @@ class Increment < ActiveInteraction::Base
1047
1070
  end
1048
1071
 
1049
1072
  Increment.run!(x: 1)
1050
- # before type check
1073
+ # before filter
1051
1074
  # after validate
1052
1075
  # >>>
1053
1076
  # executing
@@ -1055,7 +1078,7 @@ Increment.run!(x: 1)
1055
1078
  # => 2
1056
1079
  ```
1057
1080
 
1058
- In order, the available callbacks are `type_check`, `validate`, and `execute`.
1081
+ In order, the available callbacks are `filter`, `validate`, and `execute`.
1059
1082
  You can set `before`, `after`, or `around` on any of them.
1060
1083
 
1061
1084
  ### Composition
@@ -1178,7 +1201,7 @@ errors.
1178
1201
  ``` rb
1179
1202
  outcome = BuyItem.run(item: 'Thing', options: { gift_wrapped: 'yes' })
1180
1203
  outcome.errors.messages
1181
- # => {: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"]}
1182
1205
  ```
1183
1206
 
1184
1207
  Determining the type of error based on the string is difficult if not
@@ -1187,7 +1210,7 @@ the same list of errors with a testable label representing the error.
1187
1210
 
1188
1211
  ``` rb
1189
1212
  outcome.errors.details
1190
- # => {: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"}]}
1191
1214
  ```
1192
1215
 
1193
1216
  Detailed errors can also be manually added during the execute call by passing a
@@ -1337,7 +1360,7 @@ used to define the inputs on your interaction will relay type information to
1337
1360
  these gems. As a result, form fields will automatically use the appropriate
1338
1361
  input type.
1339
1362
 
1340
- ### Grouped inputs
1363
+ ### Shared input options
1341
1364
 
1342
1365
  It can be convenient to apply the same options to a bunch of inputs. One common
1343
1366
  use case is making many inputs optional. Instead of setting `default: nil` on
@@ -1358,8 +1381,8 @@ Optional inputs can be defined by using the `:default` option as described in
1358
1381
  are merged to create `inputs`. There are times where it is useful to know
1359
1382
  whether a value was passed to `run` or the result of a filter default. In
1360
1383
  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
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
1363
1386
  the input of a hash or array filter by passing a series of keys or indexes to
1364
1387
  check.
1365
1388
 
@@ -1370,7 +1393,7 @@ class UpdateUser < ActiveInteraction::Base
1370
1393
  default: nil
1371
1394
 
1372
1395
  def execute
1373
- user.birthday = birthday if given?(:birthday)
1396
+ user.birthday = birthday if inputs.given?(:birthday)
1374
1397
  errors.merge!(user.errors) unless user.save
1375
1398
  user
1376
1399
  end
@@ -1424,7 +1447,6 @@ hsilgne:
1424
1447
  errors:
1425
1448
  messages:
1426
1449
  invalid: dilavni si
1427
- invalid_nested: (%{value} <= %{name}) eulav detsen dilavni na sah
1428
1450
  invalid_type: '%{type} dilav a ton si'
1429
1451
  missing: deriuqer si
1430
1452
  ```
@@ -1444,6 +1466,19 @@ I18nInteraction.run(name: false).errors.messages[:name]
1444
1466
  # => ["gnirts dilav a ton si"]
1445
1467
  ```
1446
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
+
1447
1482
  ## Credits
1448
1483
 
1449
1484
  ActiveInteraction is brought to you by [Aaron Lasseigne][].
@@ -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
@@ -30,7 +30,7 @@ module ActiveInteraction
30
30
  include ActiveRecordable
31
31
  include Runnable
32
32
 
33
- define_callbacks :type_check
33
+ define_callbacks :filter
34
34
 
35
35
  class << self
36
36
  include Hashable
@@ -43,7 +43,7 @@ module ActiveInteraction
43
43
  #
44
44
  # Runs validations and if there are no errors it will call {#execute}.
45
45
  #
46
- # @param (see ActiveInteraction::Inputs.process)
46
+ # @param input [Hash, ActionController::Parameters]
47
47
  #
48
48
  # @return [Base]
49
49
 
@@ -88,7 +88,8 @@ module ActiveInteraction
88
88
  # rubocop:enable Naming/MemoizedInstanceVariableName
89
89
  end
90
90
 
91
- # @private
91
+ private
92
+
92
93
  # rubocop:disable Style/MissingRespondToMissing
93
94
  def method_missing(*args, &block)
94
95
  super do |klass, names, options|
@@ -99,8 +100,6 @@ module ActiveInteraction
99
100
  end
100
101
  # rubocop:enable Style/MissingRespondToMissing
101
102
 
102
- private
103
-
104
103
  # @param klass [Class]
105
104
  # @param name [Symbol]
106
105
  # @param options [Hash]
@@ -162,7 +161,9 @@ module ActiveInteraction
162
161
  def initialize(inputs = {})
163
162
  @_interaction_raw_inputs = inputs
164
163
 
165
- populate_filters_and_inputs(Inputs.process(inputs))
164
+ @_interaction_inputs = Inputs.new(inputs, self) do |name, input|
165
+ public_send("#{name}=", input.value)
166
+ end
166
167
  end
167
168
 
168
169
  # @!method compose(other, inputs = {})
@@ -186,113 +187,28 @@ module ActiveInteraction
186
187
  # Returns the inputs provided to {.run} or {.run!} after being cast based
187
188
  # on the filters in the class.
188
189
  #
189
- # @return [Hash{Symbol => Object}] All inputs passed to {.run} or {.run!}.
190
+ # @return [Inputs] All expected inputs passed to {.run} or {.run!}.
190
191
  def inputs
191
192
  @_interaction_inputs
192
193
  end
193
194
 
194
- # Returns `true` if the given key was in the hash passed to {.run}.
195
- # Otherwise returns `false`. Use this to figure out if an input was given,
196
- # even if it was `nil`. Keys within nested hash filter can also be checked
197
- # by passing them in series. Arrays can be checked in the same manor as
198
- # hashes by passing an index.
199
- #
200
- # @example
201
- # class Example < ActiveInteraction::Base
202
- # integer :x, default: nil
203
- # def execute; given?(:x) end
204
- # end
205
- # Example.run!() # => false
206
- # Example.run!(x: nil) # => true
207
- # Example.run!(x: rand) # => true
208
- #
209
- # @example Nested checks
210
- # class Example < ActiveInteraction::Base
211
- # hash :x, default: {} do
212
- # integer :y, default: nil
213
- # end
214
- # array :a, default: [] do
215
- # integer
216
- # end
217
- # def execute; given?(:x, :y) || given?(:a, 2) end
218
- # end
219
- # Example.run!() # => false
220
- # Example.run!(x: nil) # => false
221
- # Example.run!(x: {}) # => false
222
- # Example.run!(x: { y: nil }) # => true
223
- # Example.run!(x: { y: rand }) # => true
224
- # Example.run!(a: [1, 2]) # => false
225
- # Example.run!(a: [1, 2, 3]) # => true
226
- #
227
- # @param input [#to_sym]
228
- #
229
- # @return [Boolean]
230
- #
231
- # @since 2.1.0
232
- # rubocop:disable all
233
- def given?(input, *rest)
234
- filter_level = self.class
235
- input_level = @_interaction_raw_inputs
236
-
237
- [input, *rest].each do |key_or_index|
238
- if key_or_index.is_a?(Symbol) || key_or_index.is_a?(String)
239
- key = key_or_index.to_sym
240
- key_to_s = key_or_index.to_s
241
- filter_level = filter_level.filters[key]
242
-
243
- break false if filter_level.nil? || input_level.nil?
244
- if filter_level.accepts_grouped_inputs?
245
- break false unless input_level.key?(key) || input_level.key?(key_to_s) || Inputs.keys_for_group?(input_level.keys, key)
246
- else
247
- break false unless input_level.key?(key) || input_level.key?(key_to_s)
248
- end
249
-
250
- input_level = input_level[key] || input_level[key_to_s]
251
- else
252
- index = key_or_index
253
- filter_level = filter_level.filters.first.last
254
-
255
- break false if filter_level.nil? || input_level.nil?
256
- break false unless index.between?(-input_level.size, input_level.size - 1)
257
-
258
- input_level = input_level[index]
259
- end
260
- end && true
195
+ # @private
196
+ def read_attribute_for_validation(attribute)
197
+ super(errors.local_attribute(attribute))
261
198
  end
262
- # rubocop:enable all
263
199
 
264
200
  protected
265
201
 
266
202
  def run_validations!
267
- type_check
203
+ filter
268
204
 
269
205
  super if errors.empty?
270
206
  end
271
207
 
272
208
  private
273
209
 
274
- def populate_filters_and_inputs(inputs)
275
- @_interaction_inputs = Inputs.new
276
-
277
- self.class.filters.each do |name, filter|
278
- value =
279
- begin
280
- filter.clean(inputs[name], self)
281
- rescue InvalidValueError, MissingValueError, NoDefaultError
282
- # #type_check will add errors if appropriate.
283
- # We'll get the original value for the error.
284
- inputs[name]
285
- end
286
-
287
- @_interaction_inputs[name] = value
288
- public_send("#{name}=", value)
289
- end
290
-
291
- @_interaction_inputs.freeze
292
- end
293
-
294
- def type_check
295
- run_callbacks(:type_check) do
210
+ def filter
211
+ run_callbacks(:filter) do
296
212
  Validation.validate(self, self.class.filters, inputs).each do |attr, type, kwargs = {}|
297
213
  errors.add(attr, type, **kwargs)
298
214
  end
@@ -17,15 +17,15 @@ module ActiveInteraction
17
17
  # end
18
18
  #
19
19
  # Interaction.new.column_for_attribute(:email)
20
- # # => #<ActiveInteraction::FilterColumn:0x007faebeb2a6c8 @type=:string>
20
+ # # => #<ActiveInteraction::Filter::Column:0x007faebeb2a6c8 @type=:string>
21
21
  #
22
22
  # Interaction.new.column_for_attribute(:not_a_filter)
23
23
  # # => nil
24
24
  #
25
- # @return [FilterColumn, nil]
25
+ # @return [Filter::Column, nil]
26
26
  def column_for_attribute(name)
27
27
  filter = self.class.filters[name]
28
- FilterColumn.intern(filter.database_column_type) if filter
28
+ Filter::Column.intern(filter.database_column_type) if filter
29
29
  end
30
30
 
31
31
  # Returns true if a filter of that name exists.
@@ -7,6 +7,8 @@ module ActiveInteraction
7
7
  module Missable
8
8
  extend ActiveSupport::Concern
9
9
 
10
+ private
11
+
10
12
  # @param slug [Symbol]
11
13
  #
12
14
  # @yield [klass, args, options]
@@ -26,8 +28,6 @@ module ActiveInteraction
26
28
  self
27
29
  end
28
30
 
29
- private
30
-
31
31
  # @param slug [Symbol]
32
32
  #
33
33
  # @return [Filter, nil]