active_interaction 4.0.6 → 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Version](https://img.shields.io/gem/v/active_interaction.svg?style=flat-square)](https://rubygems.org/gems/active_interaction)
|
7
7
|
[![Test](https://img.shields.io/github/workflow/status/AaronLasseigne/active_interaction/Test?label=Test&style=flat-square)](https://github.com/AaronLasseigne/active_interaction/actions?query=workflow%3ATest)
|
8
|
-
[![Climate](https://img.shields.io/codeclimate/maintainability/orgsync/active_interaction.svg?style=flat-square)](https://codeclimate.com/github/orgsync/active_interaction)
|
9
8
|
|
10
9
|
---
|
11
10
|
|
@@ -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
|