active_interaction 4.1.0 → 5.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![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)
|
@@ -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
|