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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +150 -1
- data/CONTRIBUTING.md +11 -3
- data/README.md +256 -215
- 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 +40 -52
- 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 +27 -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 +43 -44
- 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
|
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)
|
@@ -17,23 +17,25 @@ handles your verbs.
|
|
17
17
|
- [Basic usage](#basic-usage)
|
18
18
|
- [Validations](#validations)
|
19
19
|
- [Filters](#filters)
|
20
|
-
- [
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
- [
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
- [
|
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
|
-
- [
|
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', '~>
|
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 '~>
|
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.
|
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
|
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
|
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
|
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
|
-
###
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
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 :
|
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
|
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 `
|
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=>["
|
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"
|
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
|
-
###
|
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
|
-
[
|
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
|
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
|