active_interaction 1.4.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -3
- data/CONTRIBUTING.md +19 -0
- data/README.md +1121 -180
- data/lib/active_interaction/backports.rb +58 -13
- data/lib/active_interaction/base.rb +16 -52
- data/lib/active_interaction/concerns/active_recordable.rb +57 -0
- data/lib/active_interaction/concerns/runnable.rb +4 -14
- data/lib/active_interaction/errors.rb +14 -66
- data/lib/active_interaction/filters/array_filter.rb +12 -9
- data/lib/active_interaction/filters/file_filter.rb +5 -24
- data/lib/active_interaction/filters/hash_filter.rb +11 -13
- data/lib/active_interaction/filters/interface_filter.rb +2 -2
- data/lib/active_interaction/filters/{model_filter.rb → object_filter.rb} +6 -6
- data/lib/active_interaction/locale/en.yml +1 -1
- data/lib/active_interaction/modules/validation.rb +2 -2
- data/lib/active_interaction/version.rb +1 -1
- data/lib/active_interaction.rb +25 -13
- data/spec/active_interaction/base_spec.rb +15 -39
- data/spec/active_interaction/concerns/active_recordable_spec.rb +51 -0
- data/spec/active_interaction/concerns/runnable_spec.rb +2 -34
- data/spec/active_interaction/errors_spec.rb +6 -89
- data/spec/active_interaction/filters/array_filter_spec.rb +2 -2
- data/spec/active_interaction/filters/file_filter_spec.rb +4 -4
- data/spec/active_interaction/filters/hash_filter_spec.rb +1 -17
- data/spec/active_interaction/filters/{model_filter_spec.rb → object_filter_spec.rb} +17 -17
- data/spec/active_interaction/i18n_spec.rb +1 -2
- data/spec/active_interaction/integration/array_interaction_spec.rb +10 -0
- data/spec/active_interaction/integration/hash_interaction_spec.rb +12 -2
- data/spec/active_interaction/integration/interface_interaction_spec.rb +10 -1
- data/spec/active_interaction/integration/object_interaction_spec.rb +16 -0
- data/spec/active_interaction/modules/validation_spec.rb +1 -2
- metadata +32 -29
- data/lib/active_interaction/concerns/transactable.rb +0 -79
- data/spec/active_interaction/concerns/transactable_spec.rb +0 -135
- data/spec/active_interaction/integration/model_interaction_spec.rb +0 -7
data/README.md
CHANGED
|
@@ -1,244 +1,1173 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img alt="" src="https://a.pomf.se/auvctt.svg" width="250">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
<h1 align="center">
|
|
6
|
+
<a href="https://github.com/orgsync/active_interaction">
|
|
7
|
+
ActiveInteraction
|
|
8
|
+
</a>
|
|
9
|
+
</h1>
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
to this.
|
|
11
|
+
<p align="center">
|
|
12
|
+
ActiveInteraction manages application-specific business logic.
|
|
13
|
+
It's an implementation of the command pattern in Ruby.
|
|
14
|
+
</p>
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://rubygems.org/gems/active_interaction"><img alt="" src="https://img.shields.io/gem/v/active_interaction.svg?label=version&style=flat-square"></a>
|
|
18
|
+
<a href="https://travis-ci.org/orgsync/active_interaction"><img alt="" src="https://img.shields.io/travis/orgsync/active_interaction/master.svg?label=build&style=flat-square"></a>
|
|
19
|
+
<a href="https://coveralls.io/r/orgsync/active_interaction"><img alt="" src="https://img.shields.io/coveralls/orgsync/active_interaction/master.svg?label=coverage&style=flat-square"></a>
|
|
20
|
+
<a href="https://codeclimate.com/github/orgsync/active_interaction"><img alt="" src="https://img.shields.io/codeclimate/github/orgsync/active_interaction.svg?label=climate&style=flat-square"></a>
|
|
21
|
+
<a href="https://gemnasium.com/orgsync/active_interaction"><img alt="" src="https://img.shields.io/gemnasium/orgsync/active_interaction.svg?label=dependencies&style=flat-square"></a>
|
|
22
|
+
</p>
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
on RubyDoc.info.
|
|
24
|
+
<hr>
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
ActiveInteraction gives you a place to put your business logic. It also helps
|
|
27
|
+
you write safer code by validating that your inputs conform to your
|
|
28
|
+
expectations. If ActiveModel deals with your nouns, then ActiveInteraction
|
|
29
|
+
handles your verbs.
|
|
30
|
+
|
|
31
|
+
Read more on [the project page][] or check out [the full documentation][].
|
|
32
|
+
|
|
33
|
+
- [Installation](#installation)
|
|
34
|
+
- [Basic usage](#basic-usage)
|
|
35
|
+
- [Validations](#validations)
|
|
36
|
+
- [Filters](#filters)
|
|
37
|
+
- [Array](#array)
|
|
38
|
+
- [Boolean](#boolean)
|
|
39
|
+
- [File](#file)
|
|
40
|
+
- [Hash](#hash)
|
|
41
|
+
- [Interface](#interface)
|
|
42
|
+
- [Object](#object)
|
|
43
|
+
- [String](#string)
|
|
44
|
+
- [Symbol](#symbol)
|
|
45
|
+
- [Dates and times](#dates-and-times)
|
|
46
|
+
- [Date](#date)
|
|
47
|
+
- [DateTime](#datetime)
|
|
48
|
+
- [Time](#time)
|
|
49
|
+
- [Numbers](#numbers)
|
|
50
|
+
- [Decimal](#decimal)
|
|
51
|
+
- [Float](#float)
|
|
52
|
+
- [Integer](#integer)
|
|
53
|
+
- [Rails](#rails)
|
|
54
|
+
- [Controller](#controller)
|
|
55
|
+
- [Index](#index)
|
|
56
|
+
- [Show](#show)
|
|
57
|
+
- [New](#new)
|
|
58
|
+
- [Create](#create)
|
|
59
|
+
- [Destroy](#destroy)
|
|
60
|
+
- [Edit](#edit)
|
|
61
|
+
- [Update](#update)
|
|
62
|
+
- [Structure](#structure)
|
|
63
|
+
- [Advanced usage](#advanced-usage)
|
|
64
|
+
- [Callbacks](#callbacks)
|
|
65
|
+
- [Composition](#composition)
|
|
66
|
+
- [Descriptions](#descriptions)
|
|
67
|
+
- [Errors](#errors)
|
|
68
|
+
- [Forms](#forms)
|
|
69
|
+
- [Predicates](#predicates)
|
|
70
|
+
- [Translations](#translations)
|
|
71
|
+
- [Credits](#credits)
|
|
22
72
|
|
|
23
|
-
|
|
73
|
+
## Installation
|
|
24
74
|
|
|
25
75
|
Add it to your Gemfile:
|
|
26
76
|
|
|
27
|
-
```
|
|
28
|
-
gem 'active_interaction', '~>
|
|
77
|
+
``` rb
|
|
78
|
+
gem 'active_interaction', '~> 2.0'
|
|
29
79
|
```
|
|
30
80
|
|
|
31
|
-
|
|
81
|
+
Or install it manually:
|
|
32
82
|
|
|
33
83
|
``` sh
|
|
34
|
-
$
|
|
84
|
+
$ gem install active_interaction --version '~> 2.0'
|
|
35
85
|
```
|
|
36
86
|
|
|
37
|
-
|
|
87
|
+
This project uses [Semantic Versioning][]. Check out [the change log][] for a
|
|
88
|
+
detailed list of changes.
|
|
38
89
|
|
|
39
|
-
|
|
40
|
-
|
|
90
|
+
ActiveInteraction works with all supported versions of Ruby (2.0 through 2.2)
|
|
91
|
+
and ActiveModel (3.2 through 4.2).
|
|
92
|
+
|
|
93
|
+
## Basic usage
|
|
94
|
+
|
|
95
|
+
To define an interaction, create a subclass of `ActiveInteraction::Base`. Then
|
|
96
|
+
you need to do two things:
|
|
97
|
+
|
|
98
|
+
1. **Define your inputs.** Use class filter methods to define what you expect
|
|
99
|
+
your inputs to look like. For instance, if you need a boolean flag for
|
|
100
|
+
pepperoni, use `boolean :pepperoni`. Check out [the filters
|
|
101
|
+
section](#filters) for all the available options.
|
|
102
|
+
|
|
103
|
+
2. **Define your business logic.** Do this by implementing the `#execute`
|
|
104
|
+
method. Each input you defined will be available as the type you specified.
|
|
105
|
+
If any of the inputs are invalid, `#execute` won't be run. Filters are
|
|
106
|
+
responsible for type checking your inputs. Check out [the validations
|
|
107
|
+
section](#validations) if you need more than that.
|
|
108
|
+
|
|
109
|
+
That covers the basics. Let's put it all together into a simple example that
|
|
110
|
+
squares a number.
|
|
111
|
+
|
|
112
|
+
``` rb
|
|
113
|
+
require 'active_interaction'
|
|
114
|
+
|
|
115
|
+
class Square < ActiveInteraction::Base
|
|
116
|
+
float :x
|
|
117
|
+
|
|
118
|
+
def execute
|
|
119
|
+
x**2
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Call `.run` on your interaction to execute it. You must pass a single hash to
|
|
125
|
+
`.run`. It will return an instance of your interaction. By convention, we call
|
|
126
|
+
this an outcome. You can use the `#valid?` method to ask the outcome if it's
|
|
127
|
+
valid. If it's invalid, take a look at its errors with `#errors`. In either
|
|
128
|
+
case, the value returned from `#execute` will be stored in `#result`.
|
|
129
|
+
|
|
130
|
+
``` rb
|
|
131
|
+
outcome = Square.run(x: 'two point one')
|
|
132
|
+
outcome.valid?
|
|
133
|
+
# => nil
|
|
134
|
+
outcome.errors.messages
|
|
135
|
+
# => {:x=>["is not a valid float"]}
|
|
136
|
+
|
|
137
|
+
outcome = Square.run(x: 2.1)
|
|
138
|
+
outcome.valid?
|
|
139
|
+
# => true
|
|
140
|
+
outcome.result
|
|
141
|
+
# => 4.41
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
You can also use `.run!` to execute interactions. It's like `.run` but more
|
|
145
|
+
dangerous. It doesn't return an outcome. If the outcome would be invalid, it
|
|
146
|
+
will instead raise an error. But if the outcome would be valid, it simply
|
|
147
|
+
returns the result.
|
|
148
|
+
|
|
149
|
+
``` rb
|
|
150
|
+
Square.run!(x: 'two point one')
|
|
151
|
+
# ActiveInteraction::InvalidInteractionError: X is not a valid float
|
|
152
|
+
Square.run!(x: 2.1)
|
|
153
|
+
# => 4.41
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Validations
|
|
157
|
+
|
|
158
|
+
ActiveInteraction type checks your inputs. Often you'll want more than that.
|
|
159
|
+
For instance, you may want an input to be a string with at least one
|
|
160
|
+
non-whitespace character. Instead of writing your own validation for that, you
|
|
161
|
+
can use validations from ActiveModel.
|
|
162
|
+
|
|
163
|
+
These validations aren't provided by ActiveInteraction. They're from
|
|
164
|
+
ActiveModel. You can also use any custom validations you wrote yourself in your
|
|
165
|
+
interactions.
|
|
166
|
+
|
|
167
|
+
``` rb
|
|
168
|
+
class SayHello < ActiveInteraction::Base
|
|
169
|
+
string :name
|
|
170
|
+
|
|
171
|
+
validates :name,
|
|
172
|
+
presence: true
|
|
173
|
+
|
|
174
|
+
def execute
|
|
175
|
+
"Hello, #{name}!"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
When you run this interaction, two things will happen. First ActiveInteraction
|
|
181
|
+
will type check your inputs. Then ActiveModel will validate them. If both of
|
|
182
|
+
those are happy, it will be executed.
|
|
183
|
+
|
|
184
|
+
``` rb
|
|
185
|
+
SayHello.run!(name: nil)
|
|
186
|
+
# ActiveInteraction::InvalidInteractionError: Name is required
|
|
187
|
+
|
|
188
|
+
SayHello.run!(name: '')
|
|
189
|
+
# ActiveInteraction::InvalidInteractionError: Name can't be blank
|
|
190
|
+
|
|
191
|
+
SayHello.run!(name: 'Taylor')
|
|
192
|
+
# => "Hello, Taylor!"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Filters
|
|
196
|
+
|
|
197
|
+
You can define filters inside an interaction using the appropriate class method.
|
|
198
|
+
Each method has the same signature:
|
|
199
|
+
|
|
200
|
+
- Some symbolic names. These are the attributes to create.
|
|
201
|
+
|
|
202
|
+
- An optional hash of options. Each filter supports at least these two options:
|
|
203
|
+
|
|
204
|
+
- `default` is the fallback value to use if `nil` is give. To make a filter
|
|
205
|
+
optional, set `default: nil`.
|
|
206
|
+
|
|
207
|
+
- `desc` is a human-readable description of the input. This can be useful for
|
|
208
|
+
generating documentation. For more information about this, read [the
|
|
209
|
+
descriptions section](#descriptions).
|
|
210
|
+
|
|
211
|
+
- An optional block of sub-filters. Only [array](#array) and [hash](#hash)
|
|
212
|
+
filters support this. Other filters will ignore blocks when given to them.
|
|
213
|
+
|
|
214
|
+
Let's take a look at an example filter. It defines three inputs: `x`, `y`, and
|
|
215
|
+
`z`. Those inputs are optional and they all share the same description ("an
|
|
216
|
+
example filter").
|
|
217
|
+
|
|
218
|
+
``` rb
|
|
219
|
+
array :x, :y, :z,
|
|
220
|
+
default: nil,
|
|
221
|
+
desc: 'an example filter' do
|
|
222
|
+
# Some filters support sub-filters here.
|
|
223
|
+
end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
In general, filters accept values of the type the correspond to, plus a few
|
|
227
|
+
alternatives that can be reasonably coerced. Typically the coercions come from
|
|
228
|
+
Rails, so `"1"` can be interpreted as the boolean value `true`, the string
|
|
229
|
+
`"1"`, or the number `1`.
|
|
230
|
+
|
|
231
|
+
### Array
|
|
232
|
+
|
|
233
|
+
In addition to accepting arrays, array inputs will convert
|
|
234
|
+
`ActiveRecord::Relation`s into arrays.
|
|
235
|
+
|
|
236
|
+
``` rb
|
|
237
|
+
class ArrayInteraction < ActiveInteraction::Base
|
|
238
|
+
array :toppings
|
|
239
|
+
|
|
240
|
+
def execute
|
|
241
|
+
toppings.size
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
ArrayInteraction.run!(toppings: 'everything')
|
|
246
|
+
# ActiveInteraction::InvalidInteractionError: Toppings is not a valid array
|
|
247
|
+
ArrayInteraction.run!(toppings: [:cheese, 'pepperoni'])
|
|
248
|
+
# => 2
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Use a block to constrain the types of elements an array can contain.
|
|
252
|
+
|
|
253
|
+
``` rb
|
|
254
|
+
array :birthdays do
|
|
255
|
+
date
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Note that filters inside an array block don't have names. Also you can only
|
|
260
|
+
have one filter inside an array block.
|
|
261
|
+
|
|
262
|
+
### Boolean
|
|
263
|
+
|
|
264
|
+
Boolean filters convert the strings `"1"` and `"true"` (case-insensitive) into
|
|
265
|
+
`true`. They also convert `"0"` and `"false"` into `false`.
|
|
266
|
+
|
|
267
|
+
``` rb
|
|
268
|
+
class BooleanInteraction < ActiveInteraction::Base
|
|
269
|
+
boolean :kool_aid
|
|
270
|
+
|
|
271
|
+
def execute
|
|
272
|
+
'Oh yeah!' if kool_aid
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
BooleanInteraction.run!(kool_aid: 1)
|
|
277
|
+
# ActiveInteraction::InvalidInteractionError: Kool aid is not a valid boolean
|
|
278
|
+
BooleanInteraction.run!(kool_aid: true)
|
|
279
|
+
# => "Oh yeah!"
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### File
|
|
283
|
+
|
|
284
|
+
File filters also accept `TempFile`s and anything that responds to `#tempfile`.
|
|
285
|
+
That means that you can pass the `params` from uploading files via forms in
|
|
286
|
+
Rails.
|
|
287
|
+
|
|
288
|
+
``` rb
|
|
289
|
+
class FileInteraction < ActiveInteraction::Base
|
|
290
|
+
file :readme
|
|
291
|
+
|
|
292
|
+
def execute
|
|
293
|
+
readme.size
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
FileInteraction.run!(readme: 'README.md')
|
|
298
|
+
# ActiveInteraction::InvalidInteractionError: Readme is not a valid file
|
|
299
|
+
FileInteraction.run!(readme: File.open('README.md'))
|
|
300
|
+
# => 21563
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Hash
|
|
304
|
+
|
|
305
|
+
Hash filters accept hashes. The expected value types are given by passing a
|
|
306
|
+
block and nesting other filters. You can have any number of filters inside a
|
|
307
|
+
hash, including other hashes.
|
|
308
|
+
|
|
309
|
+
``` rb
|
|
310
|
+
class HashInteraction < ActiveInteraction::Base
|
|
311
|
+
hash :preferences do
|
|
312
|
+
boolean :newsletter
|
|
313
|
+
boolean :sweepstakes
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def execute
|
|
317
|
+
puts 'Thanks for joining the newsletter!' if preferences[:newsletter]
|
|
318
|
+
puts 'Good luck in the sweepstakes!' if preferences[:sweepstakes]
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
HashInteraction.run!(preferences: 'yes, no')
|
|
323
|
+
# ActiveInteraction::InvalidInteractionError: Preferences is not a valid hash
|
|
324
|
+
HashInteraction.run!(preferences: { newsletter: true, 'sweepstakes' => false })
|
|
325
|
+
# Thanks for joining the newsletter!
|
|
326
|
+
# => nil
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Setting default hash values can be tricky. The default value has to be either
|
|
330
|
+
`nil` or `{}`. Use `nil` to make the hash optional. Use `{}` if you want to set
|
|
331
|
+
some defaults for values inside the hash.
|
|
332
|
+
|
|
333
|
+
``` rb
|
|
334
|
+
hash :optional,
|
|
335
|
+
default: nil
|
|
336
|
+
# => {:optional=>nil}
|
|
337
|
+
|
|
338
|
+
hash :with_defaults,
|
|
339
|
+
default: {} do
|
|
340
|
+
boolean :likes_cookies,
|
|
341
|
+
default: true
|
|
342
|
+
end
|
|
343
|
+
# => {:with_defaults=>{:likes_cookies=>true}}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
By default, hashes remove any keys that aren't given as nested filters. To
|
|
347
|
+
allow all hash keys, set `strip: false`. In general we don't recommend doing
|
|
348
|
+
this, but it's sometimes necessary.
|
|
349
|
+
|
|
350
|
+
``` rb
|
|
351
|
+
hash :stuff,
|
|
352
|
+
strip: false
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Interface
|
|
356
|
+
|
|
357
|
+
Interface filters allow you to specify that an object must respond to a certain
|
|
358
|
+
set of methods. This allows you to do duck typing with interactions.
|
|
359
|
+
|
|
360
|
+
``` rb
|
|
361
|
+
class InterfaceInteraction < ActiveInteraction::Base
|
|
362
|
+
interface :serializer,
|
|
363
|
+
methods: %i[dump load]
|
|
364
|
+
|
|
365
|
+
def execute
|
|
366
|
+
input = '{ "is_json" : true }'
|
|
367
|
+
object = serializer.load(input)
|
|
368
|
+
output = serializer.dump(object)
|
|
369
|
+
|
|
370
|
+
output
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
require 'json'
|
|
375
|
+
|
|
376
|
+
InterfaceInteraction.run!(serializer: Object.new)
|
|
377
|
+
# ActiveInteraction::InvalidInteractionError: Serializer is not a valid interface
|
|
378
|
+
InterfaceInteraction.run!(serializer: JSON)
|
|
379
|
+
# => "{\"is_json\":true}"
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Object
|
|
383
|
+
|
|
384
|
+
Object filters allow you to require an instance of a particular class. It
|
|
385
|
+
checks either `#is_a?` on the instance or `.===` on the class. Because of that,
|
|
386
|
+
it also works with classes that have mixed modules in with `include`.
|
|
387
|
+
|
|
388
|
+
``` rb
|
|
389
|
+
class Cow
|
|
390
|
+
def moo
|
|
391
|
+
'Moo!'
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
class ObjectInteraction < ActiveInteraction::Base
|
|
396
|
+
object :cow
|
|
397
|
+
|
|
398
|
+
def execute
|
|
399
|
+
cow.moo
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
ObjectInteraction.run!(cow: Object.new)
|
|
404
|
+
# ActiveInteraction::InvalidInteractionError: Cow is not a valid object
|
|
405
|
+
ObjectInteraction.run!(cow: Cow.new)
|
|
406
|
+
# => "Moo!"
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
The class name is automatically determined by the filter name. If your filter
|
|
410
|
+
name is different than your class name, use the `class` option. It can be
|
|
411
|
+
either the class, a string, or a symbol.
|
|
412
|
+
|
|
413
|
+
``` rb
|
|
414
|
+
object :dolly1,
|
|
415
|
+
class: Sheep
|
|
416
|
+
object :dolly2,
|
|
417
|
+
class: 'Sheep'
|
|
418
|
+
object :dolly3,
|
|
419
|
+
class: :Sheep
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### String
|
|
423
|
+
|
|
424
|
+
String filters define inputs that only accept strings.
|
|
425
|
+
|
|
426
|
+
``` rb
|
|
427
|
+
class StringInteraction < ActiveInteraction::Base
|
|
428
|
+
string :name
|
|
429
|
+
|
|
430
|
+
def execute
|
|
431
|
+
"Hello, #{name}!"
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
StringInteraction.run!(name: 0xDEADBEEF)
|
|
436
|
+
# ActiveInteraction::InvalidInteractionError: Name is not a valid string
|
|
437
|
+
StringInteraction.run!(name: 'Taylor')
|
|
438
|
+
# => "Hello, Taylor!"
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
If you want to strip leading and trailing whitespace from a string, set the
|
|
442
|
+
`strip` option to `true`.
|
|
443
|
+
|
|
444
|
+
``` rb
|
|
445
|
+
string :comment,
|
|
446
|
+
strip: true
|
|
41
447
|
```
|
|
42
448
|
|
|
43
|
-
|
|
449
|
+
### Symbol
|
|
450
|
+
|
|
451
|
+
Symbol filters define inputs that accept symbols. Strings will be converted
|
|
452
|
+
into symbols.
|
|
453
|
+
|
|
454
|
+
``` rb
|
|
455
|
+
class SymbolInteraction < ActiveInteraction::Base
|
|
456
|
+
symbol :method
|
|
44
457
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`result`, and return an instance of your ActiveInteraction::Base
|
|
50
|
-
subclass. Let's look at a simple example:
|
|
458
|
+
def execute
|
|
459
|
+
method.to_proc
|
|
460
|
+
end
|
|
461
|
+
end
|
|
51
462
|
|
|
52
|
-
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
463
|
+
SymbolInteraction.run!(method: -> {})
|
|
464
|
+
# ActiveInteraction::InvalidInteractionError: Method is not a valid symbol
|
|
465
|
+
SymbolInteraction.run!(method: :object_id)
|
|
466
|
+
# => #<Proc:0x007fdc9ba94118>
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Dates and times
|
|
470
|
+
|
|
471
|
+
Filters that work with dates and times behave similarly. By default, they all
|
|
472
|
+
convert strings into their expected data types using `.parse`. If you give the
|
|
473
|
+
`format` option, they will instead convert strings using `.strptime`. Note that
|
|
474
|
+
formats won't work with `DateTime` and `Time` filters if a time zone is set.
|
|
475
|
+
|
|
476
|
+
#### Date
|
|
477
|
+
|
|
478
|
+
``` rb
|
|
479
|
+
class DateInteraction < ActiveInteraction::Base
|
|
480
|
+
date :birthday
|
|
481
|
+
|
|
482
|
+
def execute
|
|
483
|
+
birthday + (18 * 365)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
DateInteraction.run!(birthday: 'yesterday')
|
|
488
|
+
# ActiveInteraction::InvalidInteractionError: Birthday is not a valid date
|
|
489
|
+
DateInteraction.run!(birthday: Date.new(1989, 9, 1))
|
|
490
|
+
# => #<Date: 2007-08-28 ((2454341j,0s,0n),+0s,2299161j)>
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
``` rb
|
|
494
|
+
date :birthday,
|
|
495
|
+
format: '%Y-%m-%d'
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
#### DateTime
|
|
499
|
+
|
|
500
|
+
``` rb
|
|
501
|
+
class DateTimeInteraction < ActiveInteraction::Base
|
|
502
|
+
date_time :now
|
|
503
|
+
|
|
504
|
+
def execute
|
|
505
|
+
now.iso8601
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
DateTimeInteraction.run!(now: 'now')
|
|
510
|
+
# ActiveInteraction::InvalidInteractionError: Now is not a valid date time
|
|
511
|
+
DateTimeInteraction.run!(now: DateTime.now)
|
|
512
|
+
# => "2015-03-11T11:04:40-05:00"
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
``` rb
|
|
516
|
+
date_time :start,
|
|
517
|
+
format: '%Y-%m-%dT%H:%M:%S'
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
#### Time
|
|
521
|
+
|
|
522
|
+
In addition to converting strings with `.parse` (or `.strptime`), time filters
|
|
523
|
+
convert numbers with `.at`.
|
|
524
|
+
|
|
525
|
+
``` rb
|
|
526
|
+
class TimeInteraction < ActiveInteraction::Base
|
|
527
|
+
time :epoch
|
|
528
|
+
|
|
529
|
+
def execute
|
|
530
|
+
Time.now - epoch
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
TimeInteraction.run!(epoch: 'a long, long time ago')
|
|
535
|
+
# ActiveInteraction::InvalidInteractionError: Epoch is not a valid time
|
|
536
|
+
TimeInteraction.run!(epoch: Time.new(1970))
|
|
537
|
+
# => 1426068362.5136619
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
``` rb
|
|
541
|
+
time :start,
|
|
542
|
+
format: '%Y-%m-%dT%H:%M:%S'
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Numbers
|
|
546
|
+
|
|
547
|
+
All numeric filters accept numeric input. They will also convert strings using
|
|
548
|
+
the appropriate method from `Kernel` (like `.Float`).
|
|
549
|
+
|
|
550
|
+
#### Decimal
|
|
551
|
+
|
|
552
|
+
``` rb
|
|
553
|
+
class DecimalInteraction < ActiveInteraction::Base
|
|
554
|
+
decimal :price
|
|
555
|
+
|
|
556
|
+
def execute
|
|
557
|
+
price * 1.0825
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
DecimalInteraction.run!(price: 'one ninety-nine')
|
|
562
|
+
# ActiveInteraction::InvalidInteractionError: Price is not a valid decimal
|
|
563
|
+
DecimalInteraction.run!(price: BigDecimal.new(1.99, 2))
|
|
564
|
+
# => #<BigDecimal:7fe792a42028,'0.2165E1',18(45)>
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
To specify the number of significant digits, use the `digits` option.
|
|
568
|
+
|
|
569
|
+
``` rb
|
|
570
|
+
decimal :dollars,
|
|
571
|
+
digits: 2
|
|
572
|
+
```
|
|
57
573
|
|
|
58
|
-
|
|
59
|
-
boolean :newsletter_subscribe, default: nil
|
|
574
|
+
#### Float
|
|
60
575
|
|
|
61
|
-
|
|
62
|
-
|
|
576
|
+
``` rb
|
|
577
|
+
class FloatInteraction < ActiveInteraction::Base
|
|
578
|
+
float :x
|
|
63
579
|
|
|
64
|
-
# The execute method is called only if the inputs validate. It
|
|
65
|
-
# does your business action. The return value will be stored in
|
|
66
|
-
# `result`.
|
|
67
580
|
def execute
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
581
|
+
x**2
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
FloatInteraction.run!(x: 'two point one')
|
|
586
|
+
# ActiveInteraction::InvalidInteractionError: X is not a valid float
|
|
587
|
+
FloatInteraction.run!(x: 2.1)
|
|
588
|
+
# => 4.41
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
#### Integer
|
|
592
|
+
|
|
593
|
+
``` rb
|
|
594
|
+
class IntegerInteraction < ActiveInteraction::Base
|
|
595
|
+
integer :limit
|
|
596
|
+
|
|
597
|
+
def execute
|
|
598
|
+
limit.downto(0).to_a
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
IntegerInteraction.run!(limit: 'ten')
|
|
603
|
+
# ActiveInteraction::InvalidInteractionError: Limit is not a valid integer
|
|
604
|
+
IntegerInteraction.run!(limit: 10)
|
|
605
|
+
# => [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
## Rails
|
|
609
|
+
|
|
610
|
+
ActiveInteraction plays nicely with Rails. You can use interactions to handle
|
|
611
|
+
your business logic instead of models or controllers. To see how it all works,
|
|
612
|
+
let's take a look at a complete example of a controller with the typical
|
|
613
|
+
resourceful actions.
|
|
614
|
+
|
|
615
|
+
### Controller
|
|
616
|
+
|
|
617
|
+
#### Index
|
|
618
|
+
|
|
619
|
+
``` rb
|
|
620
|
+
# GET /accounts
|
|
621
|
+
def index
|
|
622
|
+
@accounts = ListAccounts.run!
|
|
623
|
+
end
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
Since we're not passing any inputs to `ListAccounts`, it makes sense to use
|
|
627
|
+
`.run!` instead of `.run`. If it failed, that would mean we probably messed up
|
|
628
|
+
writing the interaction.
|
|
629
|
+
|
|
630
|
+
``` rb
|
|
631
|
+
class ListAccounts < ActiveInteraction::Base
|
|
632
|
+
def execute
|
|
633
|
+
Account.not_deleted.order(last_name: :asc, first_name: :asc)
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
#### Show
|
|
639
|
+
|
|
640
|
+
Up next is the show action. For this one we'll define a helper method to handle
|
|
641
|
+
raising the correct errors. We have to do this because calling `.run!` would
|
|
642
|
+
raise an `ActiveInteraction::InvalidInteractionError` instead of an
|
|
643
|
+
`ActiveRecord::RecordNotFound`. That means Rails would render a 500 instead of
|
|
644
|
+
a 404.
|
|
645
|
+
|
|
646
|
+
``` rb
|
|
647
|
+
# GET /accounts/:id
|
|
648
|
+
def show
|
|
649
|
+
@account = find_account!
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
private
|
|
653
|
+
|
|
654
|
+
def find_account!
|
|
655
|
+
outcome = FindAccount.run(params)
|
|
656
|
+
|
|
657
|
+
if outcome.valid?
|
|
658
|
+
outcome.result
|
|
659
|
+
else
|
|
660
|
+
fail ActiveRecord::RecordNotFound, outcome.errors.full_messages.to_sentence
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
This probably looks a little different than you're used to. Rails commonly
|
|
666
|
+
handles this with a `before_filter` that sets the `@account` instance variable.
|
|
667
|
+
Why is all this interaction code better? Two reasons: One, you can reuse the
|
|
668
|
+
`FindAccount` interaction in other places, like your API controller or a Resque
|
|
669
|
+
task. And two, if you want to change how accounts are found, you only have to
|
|
670
|
+
change one place.
|
|
671
|
+
|
|
672
|
+
Inside the interaction, we could use `#find` instead of `#find_by_id`. That way
|
|
673
|
+
we wouldn't need the `#find_account!` helper method in the controller because
|
|
674
|
+
the error would bubble all the way up. However, you should try to avoid raising
|
|
675
|
+
errors from interactions. If you do, you'll have to deal with raised exceptions
|
|
676
|
+
as well as the validity of the outcome.
|
|
677
|
+
|
|
678
|
+
``` rb
|
|
679
|
+
class FindAccount < ActiveInteraction::Base
|
|
680
|
+
integer :id
|
|
681
|
+
|
|
682
|
+
def execute
|
|
683
|
+
account = Account.not_deleted.find_by_id(id)
|
|
684
|
+
|
|
685
|
+
if account
|
|
686
|
+
account
|
|
687
|
+
else
|
|
688
|
+
errors.add(:id, 'does not exist')
|
|
71
689
|
end
|
|
72
|
-
UserMailer.async(:deliver_welcome, user.id)
|
|
73
|
-
user
|
|
74
690
|
end
|
|
75
691
|
end
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Note that it's perfectly fine to add errors during execution. Not all errors
|
|
695
|
+
have to come from type checking or validation.
|
|
696
|
+
|
|
697
|
+
#### New
|
|
76
698
|
|
|
77
|
-
|
|
699
|
+
The new action will be a little different than the ones we've looked at so far.
|
|
700
|
+
Instead of calling `.run` or `.run!`, it's going to initialize a new
|
|
701
|
+
interaction. This is possible because interactions behave like ActiveModels.
|
|
702
|
+
|
|
703
|
+
``` rb
|
|
704
|
+
# GET /accounts/new
|
|
78
705
|
def new
|
|
79
|
-
@
|
|
706
|
+
@account = CreateAccount.new
|
|
80
707
|
end
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
Since interactions behave like ActiveModels, we can use ActiveModel validations
|
|
711
|
+
with them. We'll use validations here to make sure that the first and last
|
|
712
|
+
names are not blank. [The validations section](#validations) goes into more
|
|
713
|
+
detail about this.
|
|
81
714
|
|
|
715
|
+
``` rb
|
|
716
|
+
class CreateAccount < ActiveInteraction::Base
|
|
717
|
+
string :first_name, :last_name
|
|
718
|
+
|
|
719
|
+
validates :first_name, :last_name,
|
|
720
|
+
presence: true
|
|
721
|
+
|
|
722
|
+
def to_model
|
|
723
|
+
Account.new
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def execute
|
|
727
|
+
account = Account.new(inputs)
|
|
728
|
+
|
|
729
|
+
unless account.save
|
|
730
|
+
errors.merge!(account.errors)
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
account
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
We used a couple of advanced features here. The `#to_model` method helps
|
|
739
|
+
determine the correct form to use in the view. Check out [the section on
|
|
740
|
+
forms](#forms) for more about that. Inside `#execute`, we merge errors. This is
|
|
741
|
+
a convenient way to move errors from one object to another. Read more about it
|
|
742
|
+
in [the errors section](#errors).
|
|
743
|
+
|
|
744
|
+
#### Create
|
|
745
|
+
|
|
746
|
+
The create action has a lot in common with the new action. Both of them use the
|
|
747
|
+
`CreateAccount` interaction. And if creating the account fails, this action
|
|
748
|
+
falls back to rendering the new action.
|
|
749
|
+
|
|
750
|
+
``` rb
|
|
751
|
+
# POST /accounts
|
|
82
752
|
def create
|
|
83
|
-
|
|
753
|
+
outcome = CreateAccount.run(params.fetch(:account, {}))
|
|
84
754
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
redirect_to welcome_path(user_id: signup.result.id)
|
|
755
|
+
if outcome.valid?
|
|
756
|
+
redirect_to(outcome.result)
|
|
88
757
|
else
|
|
89
|
-
|
|
758
|
+
@account = outcome
|
|
759
|
+
render(:new)
|
|
90
760
|
end
|
|
91
761
|
end
|
|
92
762
|
```
|
|
93
763
|
|
|
94
|
-
|
|
95
|
-
ActiveRecord::Base. It can use validations from your Rails application
|
|
96
|
-
and check option validity with `valid?`. Any errors are added to
|
|
97
|
-
`errors` which works exactly like an ActiveRecord model. By default,
|
|
98
|
-
everything within the `execute` method is run in a transaction if
|
|
99
|
-
ActiveRecord is available.
|
|
764
|
+
Note that we have to pass a hash to `.run`. Passing `nil` is an error.
|
|
100
765
|
|
|
101
|
-
|
|
766
|
+
Since we're using an interaction, we don't need strong parameters. The
|
|
767
|
+
interaction will ignore any inputs that weren't defined by filters. So you can
|
|
768
|
+
forget about `params.require` and `params.permit` because interactions handle
|
|
769
|
+
that for you.
|
|
102
770
|
|
|
103
|
-
|
|
104
|
-
do this:
|
|
771
|
+
#### Destroy
|
|
105
772
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
773
|
+
The destroy action will reuse the `#find_account!` helper method we wrote
|
|
774
|
+
earlier.
|
|
775
|
+
|
|
776
|
+
``` rb
|
|
777
|
+
# DELETE /accounts/:id
|
|
778
|
+
def destroy
|
|
779
|
+
DestroyAccount.run!(account: find_account!)
|
|
780
|
+
redirect_to(accounts_url)
|
|
112
781
|
end
|
|
113
782
|
```
|
|
114
783
|
|
|
115
|
-
|
|
784
|
+
In this simple example, the destroy interaction doesn't do much. It's not clear
|
|
785
|
+
that you gain anything by putting it in an interaction. But in the future, when
|
|
786
|
+
you need to do more than `account.destroy`, you'll only have to update one
|
|
787
|
+
spot.
|
|
788
|
+
|
|
789
|
+
``` rb
|
|
790
|
+
class DestroyAccount < ActiveInteraction::Base
|
|
791
|
+
object :account
|
|
116
792
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
793
|
+
def execute
|
|
794
|
+
account.destroy
|
|
795
|
+
end
|
|
796
|
+
end
|
|
121
797
|
```
|
|
122
798
|
|
|
123
|
-
|
|
799
|
+
#### Edit
|
|
800
|
+
|
|
801
|
+
Just like the destroy action, editing uses the `#find_account!` helper. Then it
|
|
802
|
+
creates a new interaction instance to use as a form object.
|
|
803
|
+
|
|
804
|
+
``` rb
|
|
805
|
+
# GET /accounts/:id/edit
|
|
806
|
+
def edit
|
|
807
|
+
account = find_account!
|
|
808
|
+
@account = UpdateAccount.new(
|
|
809
|
+
account: account,
|
|
810
|
+
first_name: account.first_name,
|
|
811
|
+
last_name: account.last_name)
|
|
812
|
+
end
|
|
813
|
+
```
|
|
124
814
|
|
|
125
|
-
|
|
815
|
+
The interaction that updates accounts is more complicated than the others. It
|
|
816
|
+
requires an account to update, but the other inputs are optional. If they're
|
|
817
|
+
missing, it'll ignore those attributes. If they're present, it'll update them.
|
|
126
818
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
model :article, :user
|
|
131
|
-
string :comment
|
|
819
|
+
ActiveInteraction generates predicate methods (like `#first_name?`) for your
|
|
820
|
+
inputs. They will return `false` if the input is `nil` and `true` otherwise.
|
|
821
|
+
Skip to [the predicates section](#predicates) for more information about them.
|
|
132
822
|
|
|
133
|
-
|
|
823
|
+
``` rb
|
|
824
|
+
class UpdateAccount < ActiveInteraction::Base
|
|
825
|
+
object :account
|
|
134
826
|
|
|
135
|
-
|
|
827
|
+
string :first_name, :last_name,
|
|
828
|
+
default: nil
|
|
829
|
+
|
|
830
|
+
validates :first_name,
|
|
831
|
+
presence: true,
|
|
832
|
+
if: :first_name?
|
|
833
|
+
validates :last_name,
|
|
834
|
+
presence: true,
|
|
835
|
+
if: :last_name?
|
|
836
|
+
|
|
837
|
+
def execute
|
|
838
|
+
account.first_name = first_name if first_name?
|
|
839
|
+
account.last_name = last_name if last_name?
|
|
840
|
+
|
|
841
|
+
unless account.save
|
|
842
|
+
errors.merge!(account.errors)
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
account
|
|
846
|
+
end
|
|
136
847
|
end
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
#### Update
|
|
137
851
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
852
|
+
Hopefully you've gotten the hang of this by now. We'll use `#find_account!` to
|
|
853
|
+
get the account. Then we'll build up the inputs for `UpdateAccount`. Then we'll
|
|
854
|
+
run the interaction and either redirect to the updated account or back to the
|
|
855
|
+
edit page.
|
|
856
|
+
|
|
857
|
+
``` rb
|
|
858
|
+
# PUT /accounts/:id
|
|
859
|
+
def update
|
|
860
|
+
inputs = { account: find_account! }.reverse_merge(params[:account])
|
|
861
|
+
outcome = UpdateAccount.run(inputs)
|
|
862
|
+
|
|
863
|
+
if outcome.valid?
|
|
864
|
+
redirect_to(outcome.result)
|
|
865
|
+
else
|
|
866
|
+
@account = outcome
|
|
867
|
+
render(:edit)
|
|
868
|
+
end
|
|
144
869
|
end
|
|
145
870
|
```
|
|
146
871
|
|
|
147
|
-
|
|
872
|
+
### Structure
|
|
148
873
|
|
|
149
|
-
|
|
874
|
+
We recommend putting your interactions in `app/interactions`. It's also very
|
|
875
|
+
helpful to group them by model. That way you can look in
|
|
876
|
+
`app/interactions/accounts` for all the ways you can interact with accounts.
|
|
150
877
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
878
|
+
```
|
|
879
|
+
- app/
|
|
880
|
+
- controllers/
|
|
881
|
+
- accounts_controller.rb
|
|
882
|
+
- interactions/
|
|
883
|
+
- accounts/
|
|
884
|
+
- create_account.rb
|
|
885
|
+
- destroy_account.rb
|
|
886
|
+
- find_account.rb
|
|
887
|
+
- list_accounts.rb
|
|
888
|
+
- update_account.rb
|
|
889
|
+
- models/
|
|
890
|
+
- account.rb
|
|
891
|
+
- views/
|
|
892
|
+
- account/
|
|
893
|
+
- edit.html.erb
|
|
894
|
+
- index.html.erb
|
|
895
|
+
- new.html.erb
|
|
896
|
+
- show.html.erb
|
|
897
|
+
```
|
|
156
898
|
|
|
157
|
-
|
|
899
|
+
## Advanced usage
|
|
158
900
|
|
|
159
|
-
|
|
160
|
-
string :name, :state
|
|
161
|
-
integer :age
|
|
162
|
-
boolean :is_special
|
|
163
|
-
model :account
|
|
164
|
-
array :tags, default: nil do
|
|
165
|
-
string
|
|
166
|
-
end
|
|
167
|
-
hash :prefs, default: nil do
|
|
168
|
-
boolean :smoking
|
|
169
|
-
boolean :view
|
|
170
|
-
end
|
|
171
|
-
date :arrives_on, default: -> { Date.current }
|
|
172
|
-
date :departs_on, default: -> { Date.tomorrow }
|
|
173
|
-
```
|
|
901
|
+
### Callbacks
|
|
174
902
|
|
|
175
|
-
|
|
903
|
+
ActiveModel provides a powerful framework for defining callbacks.
|
|
904
|
+
ActiveInteraction hooks into that framework to allow hooking into various parts
|
|
905
|
+
of an interaction's lifecycle.
|
|
176
906
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
validate :arrives_before_departs
|
|
907
|
+
``` rb
|
|
908
|
+
class Increment < ActiveInteraction::Base
|
|
909
|
+
set_callback :type_check, :before, -> { puts 'before type check' }
|
|
181
910
|
|
|
182
|
-
|
|
911
|
+
integer :x
|
|
183
912
|
|
|
184
|
-
|
|
185
|
-
if departs_on <= arrives_on
|
|
186
|
-
errors.add(:departs_on, 'must come after the arrival time')
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
```
|
|
913
|
+
set_callback :validate, :after, -> { puts 'after validate' }
|
|
190
914
|
|
|
191
|
-
|
|
915
|
+
validates :x,
|
|
916
|
+
numericality: { greater_than_or_equal_to: 0 }
|
|
192
917
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
end
|
|
199
|
-
```
|
|
918
|
+
set_callback :execute, :around, lambda { |_interaction, block|
|
|
919
|
+
puts '>>>'
|
|
920
|
+
block.call
|
|
921
|
+
puts '<<<'
|
|
922
|
+
}
|
|
200
923
|
|
|
201
|
-
|
|
924
|
+
def execute
|
|
925
|
+
puts 'executing'
|
|
926
|
+
x + 1
|
|
927
|
+
end
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
Increment.run!(x: 1)
|
|
931
|
+
# before type check
|
|
932
|
+
# after validate
|
|
933
|
+
# >>>
|
|
934
|
+
# executing
|
|
935
|
+
# <<<
|
|
936
|
+
# => 2
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
In order, the available callbacks are `type_check`, `validate`, and `execute`.
|
|
940
|
+
You can set `before`, `after`, or `around` on any of them.
|
|
941
|
+
|
|
942
|
+
### Composition
|
|
943
|
+
|
|
944
|
+
You can run interactions from within other interactions with `#compose`. If the
|
|
945
|
+
interaction is successful, it'll return the result (just like if you had called
|
|
946
|
+
it with `.run!`). If something went wrong, execution will halt immediately and
|
|
947
|
+
the errors will be moved onto the caller.
|
|
202
948
|
|
|
203
|
-
|
|
949
|
+
``` rb
|
|
950
|
+
class Add < ActiveInteraction::Base
|
|
951
|
+
integer :x, :y
|
|
204
952
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
953
|
+
def execute
|
|
954
|
+
x + y
|
|
955
|
+
end
|
|
956
|
+
end
|
|
209
957
|
|
|
210
|
-
``` ruby
|
|
211
958
|
class AddThree < ActiveInteraction::Base
|
|
212
959
|
integer :x
|
|
960
|
+
|
|
213
961
|
def execute
|
|
214
962
|
compose(Add, x: x, y: 3)
|
|
215
963
|
end
|
|
216
964
|
end
|
|
965
|
+
|
|
217
966
|
AddThree.run!(x: 5)
|
|
218
967
|
# => 8
|
|
219
968
|
```
|
|
220
969
|
|
|
221
|
-
To bring in filters from another interaction, use
|
|
970
|
+
To bring in filters from another interaction, use `.import_filters`. Combined
|
|
222
971
|
with `inputs`, delegating to another interaction is a piece of cake.
|
|
223
972
|
|
|
224
|
-
```
|
|
973
|
+
``` rb
|
|
225
974
|
class AddAndDouble < ActiveInteraction::Base
|
|
226
975
|
import_filters Add
|
|
976
|
+
|
|
227
977
|
def execute
|
|
228
978
|
compose(Add, inputs) * 2
|
|
229
979
|
end
|
|
230
980
|
end
|
|
231
981
|
```
|
|
232
982
|
|
|
233
|
-
|
|
983
|
+
### Descriptions
|
|
984
|
+
|
|
985
|
+
Use the `desc` option to provide human-readable descriptions of filters. You
|
|
986
|
+
should prefer these to comments because they can be used to generate
|
|
987
|
+
documentation. The interaction class has a `.filters` method that returns a
|
|
988
|
+
hash of filters. Each filter has a `#desc` method that returns the description.
|
|
989
|
+
|
|
990
|
+
``` rb
|
|
991
|
+
class Descriptive < ActiveInteraction::Base
|
|
992
|
+
string :first_name,
|
|
993
|
+
desc: 'your first name'
|
|
994
|
+
string :last_name,
|
|
995
|
+
desc: 'your last name'
|
|
996
|
+
end
|
|
997
|
+
|
|
998
|
+
Descriptive.filters.each do |name, filter|
|
|
999
|
+
puts "#{name}: #{filter.desc}"
|
|
1000
|
+
end
|
|
1001
|
+
# first_name: your first name
|
|
1002
|
+
# last_name: your last name
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
### Errors
|
|
1006
|
+
|
|
1007
|
+
ActiveInteraction provides detailed errors for easier introspection and testing
|
|
1008
|
+
of errors. Detailed errors improve on regular errors by adding a symbol that
|
|
1009
|
+
represents the type of error that has occurred. Let's look at an example where
|
|
1010
|
+
an item is purchased using a credit card.
|
|
1011
|
+
|
|
1012
|
+
``` rb
|
|
1013
|
+
class BuyItem < ActiveInteraction::Base
|
|
1014
|
+
object :credit_card, :item
|
|
1015
|
+
hash :options do
|
|
1016
|
+
boolean :gift_wrapped
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
def execute
|
|
1020
|
+
order = credit_card.purchase(item)
|
|
1021
|
+
notify(credit_card.account)
|
|
1022
|
+
order
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
private def notify(account)
|
|
1026
|
+
# ...
|
|
1027
|
+
end
|
|
1028
|
+
end
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
Having missing or invalid inputs causes the interaction to fail and return
|
|
1032
|
+
errors.
|
|
1033
|
+
|
|
1034
|
+
``` rb
|
|
1035
|
+
outcome = BuyItem.run(item: 'Thing', options: { gift_wrapped: 'yes' })
|
|
1036
|
+
outcome.errors.messages
|
|
1037
|
+
# => {:credit_card=>["is required"], :item=>["is not a valid object"], :options=>["has an invalid nested value (\"gift_wrapped\" => \"yes\")"]}
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
Determining the type of error based on the string is difficult if not
|
|
1041
|
+
impossible. Calling `#details` instead of `#messages` on `errors` gives you
|
|
1042
|
+
the same list of errors with a testable label representing the error.
|
|
1043
|
+
|
|
1044
|
+
``` rb
|
|
1045
|
+
outcome.errors.details
|
|
1046
|
+
# => {:credit_card=>[{:error=>:missing}], :item=>[{:type=>"object", :error=>:invalid_type}], :options=>[{:name=>"\"gift_wrapped\"", :value=>"\"yes\"", :error=>:invalid_nested}]}
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
Detailed errors can also be manually added during the execute call by passing a
|
|
1050
|
+
symbol to `#add` instead of a string.
|
|
1051
|
+
|
|
1052
|
+
``` rb
|
|
1053
|
+
def execute
|
|
1054
|
+
errors.add(:monster, :no_passage)
|
|
1055
|
+
end
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
These types of errors will become standard with Rails 5. ActiveInteraction's
|
|
1059
|
+
implementation is based off of [active_model-errors_details][].
|
|
1060
|
+
|
|
1061
|
+
ActiveInteraction also supports merging errors. This is useful if you want to
|
|
1062
|
+
delegate validation to some other object. For example, if you have an
|
|
1063
|
+
interaction that updates a record, you might want that record to validate
|
|
1064
|
+
itself. By using the `#merge!` helper on `errors`, you can do exactly that.
|
|
1065
|
+
|
|
1066
|
+
``` rb
|
|
1067
|
+
class UpdateThing < ActiveInteraction::Base
|
|
1068
|
+
object :thing
|
|
1069
|
+
|
|
1070
|
+
def execute
|
|
1071
|
+
unless thing.save
|
|
1072
|
+
errors.merge!(thing.errors)
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
thing
|
|
1076
|
+
end
|
|
1077
|
+
end
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
### Forms
|
|
1081
|
+
|
|
1082
|
+
The outcome returned by `.run` can be used in forms as though it were an ActiveModel object. You can also create a form object by calling `.new` on the interaction.
|
|
1083
|
+
|
|
1084
|
+
Given an application with an `Account` model we'll create a new `Account` using the `CreateAccount` interaction.
|
|
1085
|
+
|
|
1086
|
+
```rb
|
|
1087
|
+
# GET /accounts/new
|
|
1088
|
+
def new
|
|
1089
|
+
@account = CreateAccount.new
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
# POST /accounts
|
|
1093
|
+
def create
|
|
1094
|
+
outcome = CreateAccount.run(params.fetch(:account, {}))
|
|
1095
|
+
|
|
1096
|
+
if outcome.valid?
|
|
1097
|
+
redirect_to(outcome.result)
|
|
1098
|
+
else
|
|
1099
|
+
@account = outcome
|
|
1100
|
+
render(:new)
|
|
1101
|
+
end
|
|
1102
|
+
end
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
The form used to create a new `Account` has slightly more information on the `form_for` call than you might expect.
|
|
1106
|
+
|
|
1107
|
+
```rb
|
|
1108
|
+
<%= form_for @account, as: :account, url: accounts_path do |f| %>
|
|
1109
|
+
<%= f.text_field :first_name %>
|
|
1110
|
+
<%= f.text_field :last_name %>
|
|
1111
|
+
<%= f.submit 'Create' %>
|
|
1112
|
+
<% end %>
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
This is necessary because we want the form to act like it is creating a new `Account`. Defining `to_model` on the `CreateAccount` interaction tells the form to treat our interaction like an `Account`.
|
|
1116
|
+
|
|
1117
|
+
```rb
|
|
1118
|
+
class CreateAccount < ActiveInteraction::Base
|
|
1119
|
+
...
|
|
1120
|
+
|
|
1121
|
+
def to_model
|
|
1122
|
+
Account.new
|
|
1123
|
+
end
|
|
1124
|
+
end
|
|
1125
|
+
```
|
|
234
1126
|
|
|
235
|
-
|
|
236
|
-
is add translations to your project. In Rails, they typically go
|
|
237
|
-
into `config/locales`. So, for example, let's say that (for whatever
|
|
238
|
-
reason) you want to print out everything backwards. Simply add
|
|
239
|
-
translations for ActiveInteraction to your `hsilgne` locale:
|
|
1127
|
+
Now our `form_for` call knows how to generate the correct URL and param name (i.e. `params[:account]`).
|
|
240
1128
|
|
|
241
|
-
```
|
|
1129
|
+
```rb
|
|
1130
|
+
# app/views/accounts/new.html.erb
|
|
1131
|
+
<%= form_for @account do |f| %>
|
|
1132
|
+
...
|
|
1133
|
+
<% end %>
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
ActiveInteraction also supports [formtastic][] and [simple_form][]. The filters used to define the inputs on your interaction will relay type information to these gems. As a result, form fields will automatically use the appropriate input type.
|
|
1137
|
+
|
|
1138
|
+
### Predicates
|
|
1139
|
+
|
|
1140
|
+
ActiveInteraction creates a predicate method for every input defined by a filter. So if you have an input called `foo`, there will be a predicate method called `#foo?`. That method will tell you if the input was given (that is, if it was not `nil`).
|
|
1141
|
+
|
|
1142
|
+
``` rb
|
|
1143
|
+
class SayHello < ActiveInteraction::Base
|
|
1144
|
+
string :name,
|
|
1145
|
+
default: nil
|
|
1146
|
+
|
|
1147
|
+
def execute
|
|
1148
|
+
if name?
|
|
1149
|
+
"Hello, #{name}!"
|
|
1150
|
+
else
|
|
1151
|
+
"Howdy, stranger!"
|
|
1152
|
+
end
|
|
1153
|
+
end
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
SayHello.run!(name: nil)
|
|
1157
|
+
# => "Howdy, stranger!"
|
|
1158
|
+
SayHello.run!(name: 'Taylor')
|
|
1159
|
+
# => "Hello, Taylor!"
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
### Translations
|
|
1163
|
+
|
|
1164
|
+
ActiveInteraction is i18n aware out of the box! All you have to do is add
|
|
1165
|
+
translations to your project. In Rails, these typically go into
|
|
1166
|
+
`config/locales`. For example, let's say that for some reason you want to print
|
|
1167
|
+
everything out backwards. Simply add translations for ActiveInteraction to your
|
|
1168
|
+
`hsilgne` locale.
|
|
1169
|
+
|
|
1170
|
+
``` yml
|
|
242
1171
|
# config/locales/hsilgne.yml
|
|
243
1172
|
hsilgne:
|
|
244
1173
|
active_interaction:
|
|
@@ -252,49 +1181,61 @@ hsilgne:
|
|
|
252
1181
|
float: taolf
|
|
253
1182
|
hash: hsah
|
|
254
1183
|
integer: regetni
|
|
255
|
-
|
|
1184
|
+
interface: ecafretni
|
|
1185
|
+
object: tcejbo
|
|
256
1186
|
string: gnirts
|
|
1187
|
+
symbol: lobmys
|
|
257
1188
|
time: emit
|
|
258
1189
|
errors:
|
|
259
1190
|
messages:
|
|
260
1191
|
invalid: dilavni si
|
|
1192
|
+
invalid_nested: (%{value} <= %{name}) eulav detsen dilavni na sah
|
|
261
1193
|
invalid_type: '%{type} dilav a ton si'
|
|
262
1194
|
missing: deriuqer si
|
|
263
1195
|
```
|
|
264
1196
|
|
|
265
|
-
Then set your locale and run
|
|
1197
|
+
Then set your locale and run interactions like normal.
|
|
266
1198
|
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
boolean :a
|
|
271
|
-
def execute; end
|
|
1199
|
+
``` rb
|
|
1200
|
+
class I18nInteraction < ActiveInteraction::Base
|
|
1201
|
+
string :name
|
|
272
1202
|
end
|
|
273
|
-
|
|
274
|
-
|
|
1203
|
+
|
|
1204
|
+
I18nInteraction.run(name: false).errors.messages[:name]
|
|
1205
|
+
# => ["is not a valid string"]
|
|
1206
|
+
|
|
1207
|
+
I18n.locale = :hsilgne
|
|
1208
|
+
I18nInteraction.run(name: false).errors.messages[:name]
|
|
1209
|
+
# => ["gnirts dilav a ton si"]
|
|
275
1210
|
```
|
|
276
1211
|
|
|
277
1212
|
## Credits
|
|
278
1213
|
|
|
279
|
-
ActiveInteraction is brought to you by [
|
|
280
|
-
[
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
1214
|
+
ActiveInteraction is brought to you by [Aaron Lasseigne][] and
|
|
1215
|
+
[Taylor Fausak][] from [OrgSync][]. We were inspired by the fantastic work done
|
|
1216
|
+
by [Jonathan Novak][] on [Mutations][].
|
|
1217
|
+
|
|
1218
|
+
If you want to contribute to ActiveInteraction, please read
|
|
1219
|
+
[our contribution guidelines][]. A [complete list of contributors][] is
|
|
1220
|
+
available on GitHub.
|
|
1221
|
+
|
|
1222
|
+
ActiveInteraction is licensed under [the MIT License][].
|
|
1223
|
+
|
|
1224
|
+
Logo design by [Tyler Lee][].
|
|
1225
|
+
|
|
1226
|
+
[the project page]: http://orgsync.github.io/active_interaction/
|
|
1227
|
+
[the full documentation]: http://rubydoc.info/github/orgsync/active_interaction
|
|
1228
|
+
[semantic versioning]: http://semver.org/spec/v2.0.0.html
|
|
1229
|
+
[the change log]: CHANGELOG.md
|
|
1230
|
+
[active_model-errors_details]: https://github.com/cowbell/active_model-errors_details
|
|
1231
|
+
[aaron lasseigne]: https://github.com/AaronLasseigne
|
|
1232
|
+
[taylor fausak]: https://github.com/tfausak
|
|
1233
|
+
[orgsync]: https://github.com/orgsync
|
|
1234
|
+
[jonathan novak]: https://github.com/cypriss
|
|
1235
|
+
[mutations]: https://github.com/cypriss/mutations
|
|
1236
|
+
[our contribution guidelines]: CONTRIBUTING.md
|
|
1237
|
+
[complete list of contributors]: https://github.com/orgsync/active_interaction/graphs/contributors
|
|
1238
|
+
[the mit license]: LICENSE.txt
|
|
1239
|
+
[formtastic]: https://rubygems.org/gems/formtastic
|
|
1240
|
+
[simple_form]: https://rubygems.org/gems/simple_form
|
|
1241
|
+
[tyler lee]: https://github.com/tylerlee
|