active_interaction 4.0.6 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +156 -7
- data/CONTRIBUTING.md +11 -3
- data/README.md +260 -219
- data/lib/active_interaction/array_input.rb +77 -0
- data/lib/active_interaction/base.rb +14 -98
- data/lib/active_interaction/concerns/active_recordable.rb +3 -3
- data/lib/active_interaction/concerns/missable.rb +2 -2
- data/lib/active_interaction/errors.rb +6 -88
- data/lib/active_interaction/exceptions.rb +47 -0
- data/lib/active_interaction/filter/column.rb +59 -0
- data/lib/active_interaction/filter/error.rb +40 -0
- data/lib/active_interaction/filter.rb +44 -53
- data/lib/active_interaction/filters/abstract_date_time_filter.rb +9 -6
- data/lib/active_interaction/filters/abstract_numeric_filter.rb +7 -3
- data/lib/active_interaction/filters/array_filter.rb +40 -6
- data/lib/active_interaction/filters/boolean_filter.rb +4 -3
- data/lib/active_interaction/filters/date_filter.rb +1 -1
- data/lib/active_interaction/filters/date_time_filter.rb +1 -1
- data/lib/active_interaction/filters/decimal_filter.rb +1 -1
- data/lib/active_interaction/filters/float_filter.rb +1 -1
- data/lib/active_interaction/filters/hash_filter.rb +23 -15
- data/lib/active_interaction/filters/integer_filter.rb +1 -1
- data/lib/active_interaction/filters/interface_filter.rb +12 -12
- data/lib/active_interaction/filters/object_filter.rb +9 -3
- data/lib/active_interaction/filters/record_filter.rb +21 -11
- data/lib/active_interaction/filters/string_filter.rb +1 -1
- data/lib/active_interaction/filters/symbol_filter.rb +1 -1
- data/lib/active_interaction/filters/time_filter.rb +4 -4
- data/lib/active_interaction/hash_input.rb +43 -0
- data/lib/active_interaction/input.rb +23 -0
- data/lib/active_interaction/inputs.rb +161 -46
- data/lib/active_interaction/locale/en.yml +0 -1
- data/lib/active_interaction/locale/fr.yml +0 -1
- data/lib/active_interaction/locale/it.yml +0 -1
- data/lib/active_interaction/locale/ja.yml +0 -1
- data/lib/active_interaction/locale/pt-BR.yml +0 -1
- data/lib/active_interaction/modules/validation.rb +6 -17
- data/lib/active_interaction/version.rb +1 -1
- data/lib/active_interaction.rb +41 -36
- data/spec/active_interaction/array_input_spec.rb +166 -0
- data/spec/active_interaction/base_spec.rb +34 -248
- data/spec/active_interaction/concerns/active_modelable_spec.rb +3 -3
- data/spec/active_interaction/concerns/active_recordable_spec.rb +7 -7
- data/spec/active_interaction/concerns/hashable_spec.rb +8 -8
- data/spec/active_interaction/concerns/missable_spec.rb +9 -9
- data/spec/active_interaction/concerns/runnable_spec.rb +34 -32
- data/spec/active_interaction/errors_spec.rb +60 -43
- data/spec/active_interaction/{filter_column_spec.rb → filter/column_spec.rb} +3 -10
- data/spec/active_interaction/filter_spec.rb +6 -6
- data/spec/active_interaction/filters/abstract_date_time_filter_spec.rb +2 -2
- data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +2 -2
- data/spec/active_interaction/filters/array_filter_spec.rb +109 -16
- data/spec/active_interaction/filters/boolean_filter_spec.rb +12 -11
- data/spec/active_interaction/filters/date_filter_spec.rb +32 -27
- data/spec/active_interaction/filters/date_time_filter_spec.rb +34 -29
- data/spec/active_interaction/filters/decimal_filter_spec.rb +20 -18
- data/spec/active_interaction/filters/file_filter_spec.rb +7 -7
- data/spec/active_interaction/filters/float_filter_spec.rb +19 -17
- data/spec/active_interaction/filters/hash_filter_spec.rb +16 -18
- data/spec/active_interaction/filters/integer_filter_spec.rb +24 -22
- data/spec/active_interaction/filters/interface_filter_spec.rb +105 -82
- data/spec/active_interaction/filters/object_filter_spec.rb +52 -36
- data/spec/active_interaction/filters/record_filter_spec.rb +61 -39
- data/spec/active_interaction/filters/string_filter_spec.rb +7 -7
- data/spec/active_interaction/filters/symbol_filter_spec.rb +6 -6
- data/spec/active_interaction/filters/time_filter_spec.rb +57 -34
- data/spec/active_interaction/hash_input_spec.rb +58 -0
- data/spec/active_interaction/i18n_spec.rb +22 -17
- data/spec/active_interaction/inputs_spec.rb +170 -18
- data/spec/active_interaction/integration/array_interaction_spec.rb +3 -7
- data/spec/active_interaction/integration/record_integration_spec.rb +5 -0
- data/spec/active_interaction/modules/validation_spec.rb +8 -31
- data/spec/spec_helper.rb +9 -0
- data/spec/support/concerns.rb +2 -2
- data/spec/support/filters.rb +27 -51
- data/spec/support/interactions.rb +4 -4
- metadata +50 -50
- 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
|
4
|
+
It's an implementation of service objects designed to blend seamlessly into Rails.
|
5
5
|
|
6
6
|
[](https://rubygems.org/gems/active_interaction)
|
7
7
|
[](https://github.com/AaronLasseigne/active_interaction/actions?query=workflow%3ATest)
|
8
|
-
[](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
|
-
- [
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
- [
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
- [
|
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
|
-
- [
|
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', '~>
|
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 '~>
|
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.
|
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
|
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
|
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
|
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
|
-
###
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
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
|
-
|
1024
|
-
ActiveInteraction
|
1025
|
-
|
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 :
|
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
|
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 `
|
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=>["
|
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"
|
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
|
-
###
|
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
|
-
[
|
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
|
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
|