active_interaction 1.5.0 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9ba87c0f7766f1154697c93d29d10c0b0b0774d0
4
- data.tar.gz: 0d299041f295fe030c7353fa23d058ac1d3f0f27
3
+ metadata.gz: 05a744023cf02eb1f2ea70d41ca68b807779e13a
4
+ data.tar.gz: 455646aa8861af2545cd805f4eafe0856a9ff8f5
5
5
  SHA512:
6
- metadata.gz: 02ce12f0701b9ec571b9988f0ba2bbdee87611b8504542ea052fa02fc854079c2ded5962b782e066b43d17812aee70d89750ed4c8a2d84204c05285e6de03b7e
7
- data.tar.gz: eef2e988d3daa2e5ff0ea85a94fdfd9a4d4fc4acebb1b7ec555c326c2b8d6d4c4643038cb5955102d8427ef6200cca930d8cd13c78dda6e02c456cf9d03e91ae
6
+ metadata.gz: 64c694eb64b7550b224b12d271463ef508e8d23fa6edbb295f97374a9693010d2cfbd0be22c6a98c66aa0dd1b1e4ff58893e524b93024be8dee39ee6ab841a4a
7
+ data.tar.gz: d657ef014a9e0bf547f83402e0d8becc33d1fad871bbfb26d8680fcf37a0d5ad9cad5fe263d7b43105f584caad4b98a349050dc41115bb3de70231cf42788676
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [1.5.1][] (2015-04-28)
2
+
3
+ ## Fixed
4
+
5
+ - [#265][]: Allow `nil` inputs for interface and model filters.
6
+ - [#256][]: Improve error messages for nested invalid values.
7
+
1
8
  # [1.5.0][] (2015-02-05)
2
9
 
3
10
  ## Added
@@ -374,7 +381,8 @@
374
381
 
375
382
  - Initial release.
376
383
 
377
- [1.5.0]: https://github.com/orgsync/active_interaction/compare/v1.4.1...1.5.0
384
+ [1.5.1]: https://github.com/orgsync/active_interaction/compare/v1.5.0...v1.5.1
385
+ [1.5.0]: https://github.com/orgsync/active_interaction/compare/v1.4.1...v1.5.0
378
386
  [1.4.1]: https://github.com/orgsync/active_interaction/compare/v1.4.0...v1.4.1
379
387
  [1.4.0]: https://github.com/orgsync/active_interaction/compare/v1.3.1...v1.4.0
380
388
  [1.3.1]: https://github.com/orgsync/active_interaction/compare/v1.3.0...v1.3.1
@@ -417,7 +425,7 @@
417
425
  [0.1.3]: https://github.com/orgsync/active_interaction/compare/v0.1.2...v0.1.3
418
426
  [0.1.2]: https://github.com/orgsync/active_interaction/compare/v0.1.1...v0.1.2
419
427
  [0.1.1]: https://github.com/orgsync/active_interaction/compare/v0.1.0...v0.1.1
420
- [0.1.0]: https://github.com/orgsync/active_interaction/compare/62f999b...v0.1.0
428
+ [0.1.0]: https://github.com/orgsync/active_interaction/compare/v0.0.0...v0.1.0
421
429
 
422
430
  [#20]: https://github.com/orgsync/active_interaction/issues/20
423
431
  [#23]: https://github.com/orgsync/active_interaction/issues/23
@@ -491,3 +499,5 @@
491
499
  [#239]: https://github.com/orgsync/active_interaction/issues/239
492
500
  [#244]: https://github.com/orgsync/active_interaction/issues/244
493
501
  [#248]: https://github.com/orgsync/active_interaction/issues/248
502
+ [#256]: https://github.com/orgsync/active_interaction/issues/256
503
+ [#265]: https://github.com/orgsync/active_interaction/issues/265
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,19 @@
1
+ # Steps
2
+
3
+ 1. [Fork][] the repo.
4
+ 2. Add a breaking test for your change.
5
+ 3. Make the tests pass.
6
+ 4. Push your fork.
7
+ 5. Submit a pull request.
8
+
9
+ # Code Style
10
+
11
+ Running the tests using `rake` (with no args) or within `guard` will also check for style issues in the code. Ideally all submissions would pass these checks. If the code style is causing issues (particularly the method or class size) we can work with you to correct it. Don't let it stop you from contributing.
12
+
13
+ # Vagrant
14
+
15
+ If you like to use [Vagrant][] we've provided a [Vagrantfile][] for your convenience.
16
+
17
+ [fork]: https://github.com/orgsync/active_interaction/fork
18
+ [Vagrant]: http://www.vagrantup.com
19
+ [Vagrantfile]: Vagrantfile
data/README.md CHANGED
@@ -1,264 +1,1171 @@
1
1
  <p align="center">
2
- <img alt="briefcase" src="briefcase.png">
2
+ <img alt="" src="https://a.pomf.se/auvctt.svg" width="250">
3
3
  </p>
4
4
 
5
- <h1 align="center">ActiveInteraction</h1>
6
-
7
- <p align="center">Manage application specific business logic.</p>
8
-
9
- <p align="center">
10
- <a href="https://rubygems.org/gems/active_interaction">
11
- <img alt="Gem Version" src="https://img.shields.io/gem/v/active_interaction.svg?style=flat">
12
- </a>
13
- <a href="https://travis-ci.org/orgsync/active_interaction">
14
- <img alt="Build Status" src="https://img.shields.io/travis/orgsync/active_interaction/master.svg?style=flat">
15
- </a>
16
- <a href="https://coveralls.io/r/orgsync/active_interaction?branch=master">
17
- <img alt="Coverage Status" src="https://img.shields.io/coveralls/orgsync/active_interaction/master.svg?style=flat">
18
- </a>
19
- <a href="https://codeclimate.com/github/orgsync/active_interaction">
20
- <img alt="Code Climate" src="https://img.shields.io/codeclimate/github/orgsync/active_interaction.svg?style=flat">
21
- </a>
22
- <a href="https://gemnasium.com/orgsync/active_interaction">
23
- <img alt="Dependency Status" src="https://img.shields.io/gemnasium/orgsync/active_interaction.svg?style=flat">
5
+ <h1 align="center">
6
+ <a href="https://github.com/orgsync/active_interaction">
7
+ ActiveInteraction
24
8
  </a>
9
+ </h1>
25
10
 
26
- <hr>
11
+ <p align="center">
12
+ ActiveInteraction manages application-specific business logic.
13
+ It's an implementation of the command pattern in Ruby.
14
+ </p>
15
+
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&amp;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&amp;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&amp;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&amp;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&amp;style=flat-square"></a>
27
22
  </p>
28
23
 
29
- At first it seemed alright. A little business logic in a controller
30
- or model wasn't going to hurt anything. Then one day you wake up
31
- and you're surrounded by fat models and unwieldy controllers. Curled
32
- up and crying in the corner, you can't help but wonder how it came
33
- to this.
24
+ <hr>
34
25
 
35
- Take back control. Slim down models and wrangle monstrous controller
36
- methods with ActiveInteraction.
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.
37
30
 
38
- Read more on the [project page][1] or check out the full [documentation][2]
39
- on RubyDoc.info.
31
+ Read more on [the project page][] or check out [the full documentation][].
40
32
 
41
- ## Installation
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
+ - [Model](#model)
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)
42
72
 
43
- This project uses [semantic versioning][3].
73
+ ## Installation
44
74
 
45
75
  Add it to your Gemfile:
46
76
 
47
- ``` ruby
77
+ ``` rb
48
78
  gem 'active_interaction', '~> 1.5'
49
79
  ```
50
80
 
51
- And then execute:
81
+ Or install it manually:
52
82
 
53
83
  ``` sh
54
- $ bundle
84
+ $ gem install active_interaction --version '~> 1.5'
55
85
  ```
56
86
 
57
- Or install it yourself with:
87
+ This project uses [Semantic Versioning][]. Check out [the change log][] for a
88
+ detailed list of changes.
58
89
 
59
- ``` sh
60
- $ gem install active_interaction
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`. If it's
128
+ valid, `#result` will be the value returned from `#execute`.
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
+ ### Model
383
+
384
+ Model filters allow you to require an instance of a particular class. It checks
385
+ either `#is_a?` on the instance or `.===` on the class. Because of that, it
386
+ 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 ModelInteraction < ActiveInteraction::Base
396
+ model :cow
397
+
398
+ def execute
399
+ cow.moo
400
+ end
401
+ end
402
+
403
+ ModelInteraction.run!(cow: Object.new)
404
+ # ActiveInteraction::InvalidInteractionError: Cow is not a valid model
405
+ ModelInteraction.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
+ model :dolly1,
415
+ class: Sheep
416
+ model :dolly2,
417
+ class: 'Sheep'
418
+ model :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
447
+ ```
448
+
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
457
+
458
+ def execute
459
+ method.to_proc
460
+ end
461
+ end
462
+
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
+ ```
573
+
574
+ #### Float
575
+
576
+ ``` rb
577
+ class FloatInteraction < ActiveInteraction::Base
578
+ float :x
579
+
580
+ def execute
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
61
636
  ```
62
637
 
63
- ## What do I get?
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
64
653
 
65
- ActiveInteraction::Base lets you create interaction models. These
66
- models ensure that certain inputs are provided and that those
67
- inputs are in the format you want them in. If the inputs are valid
68
- it will call `execute`, store the return value of that method in
69
- `result`, and return an instance of your ActiveInteraction::Base
70
- subclass. Let's look at a simple example:
654
+ def find_account!
655
+ outcome = FindAccount.run(params)
71
656
 
72
- ``` ruby
73
- # Define an interaction that signs up a user.
74
- class UserSignup < ActiveInteraction::Base
75
- # required
76
- string :email, :name
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.
77
671
 
78
- # optional
79
- boolean :newsletter_subscribe, default: nil
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.
80
677
 
81
- # ActiveRecord validations
82
- validates :email, format: EMAIL_REGEX
678
+ ``` rb
679
+ class FindAccount < ActiveInteraction::Base
680
+ integer :id
83
681
 
84
- # The execute method is called only if the inputs validate. It
85
- # does your business action. The return value will be stored in
86
- # `result`.
87
682
  def execute
88
- user = User.create!(email: email, name: name)
89
- if newsletter_subscribe
90
- NewsletterSubscriptions.create(email: email, user_id: user.id)
683
+ account = Account.not_deleted.find_by_id(id)
684
+
685
+ if account
686
+ account
687
+ else
688
+ errors.add(:id, 'does not exist')
91
689
  end
92
- UserMailer.async(:deliver_welcome, user.id)
93
- user
94
690
  end
95
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
96
698
 
97
- # In a controller action (for instance), you can run it:
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
98
705
  def new
99
- @signup = UserSignup.new
706
+ @account = CreateAccount.new
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.
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
100
735
  end
736
+ ```
101
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
102
752
  def create
103
- @signup = UserSignup.run(params[:user])
753
+ outcome = CreateAccount.run(params.fetch(:account, {}))
104
754
 
105
- # Then check to see if it worked:
106
- if @signup.valid?
107
- redirect_to welcome_path(user_id: signup.result.id)
755
+ if outcome.valid?
756
+ redirect_to(outcome.result)
108
757
  else
109
- render action: :new
758
+ @account = outcome
759
+ render(:new)
110
760
  end
111
761
  end
112
762
  ```
113
763
 
114
- You may have noticed that ActiveInteraction::Base quacks like
115
- ActiveRecord::Base. It can use validations from your Rails application
116
- and check option validity with `valid?`. Any errors are added to
117
- `errors` which works exactly like an ActiveRecord model. By default,
118
- everything within the `execute` method is run in a transaction if
119
- ActiveRecord is available.
764
+ Note that we have to pass a hash to `.run`. Passing `nil` is an error.
765
+
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.
770
+
771
+ #### Destroy
772
+
773
+ The destroy action will reuse the `#find_account!` helper method we wrote
774
+ earlier.
120
775
 
121
- ## How do I call an interaction?
776
+ ``` rb
777
+ # DELETE /accounts/:id
778
+ def destroy
779
+ DestroyAccount.run!(account: find_account!)
780
+ redirect_to(accounts_url)
781
+ end
782
+ ```
783
+
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.
122
788
 
123
- There are two way to call an interaction. Given UserSignup, you can
124
- do this:
789
+ ``` rb
790
+ class DestroyAccount < ActiveInteraction::Base
791
+ model :account
125
792
 
126
- ``` ruby
127
- outcome = UserSignup.run(params)
128
- if outcome.valid?
129
- # Do something with outcome.result...
130
- else
131
- # Do something with outcome.errors...
793
+ def execute
794
+ account.destroy
795
+ end
132
796
  end
133
797
  ```
134
798
 
135
- Or, you can do this:
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.
136
803
 
137
- ``` ruby
138
- result = UserSignup.run!(params)
139
- # Either returns the result of execute,
140
- # or raises ActiveInteraction::InvalidInteractionError
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
141
813
  ```
142
814
 
143
- ## What can I pass to an interaction?
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.
818
+
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.
822
+
823
+ ``` rb
824
+ class UpdateAccount < ActiveInteraction::Base
825
+ model :account
144
826
 
145
- Interactions only accept a Hash for `run` and `run!`.
827
+ string :first_name, :last_name,
828
+ default: nil
146
829
 
147
- ``` ruby
148
- # A user comments on an article
149
- class CreateComment < ActiveInteraction::Base
150
- model :article, :user
151
- string :comment
830
+ validates :first_name,
831
+ presence: true,
832
+ if: :first_name?
833
+ validates :last_name,
834
+ presence: true,
835
+ if: :last_name?
152
836
 
153
- validates :comment, length: { maximum: 500 }
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
154
844
 
155
- def execute; ...; end
845
+ account
846
+ end
156
847
  end
848
+ ```
849
+
850
+ #### Update
157
851
 
158
- def somewhere
159
- outcome = CreateComment.run(
160
- comment: params[:comment],
161
- article: Article.find(params[:article_id]),
162
- user: current_user
163
- )
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
164
869
  end
165
870
  ```
166
871
 
167
- ## How do I define an interaction?
872
+ ### Structure
168
873
 
169
- 1. Subclass ActiveInteraction::Base
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.
170
877
 
171
- ``` ruby
172
- class YourInteraction < ActiveInteraction::Base
173
- # ...
174
- end
175
- ```
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
+ ```
176
898
 
177
- 2. Define your attributes:
899
+ ## Advanced usage
178
900
 
179
- ``` ruby
180
- string :name, :state
181
- integer :age
182
- boolean :is_special
183
- model :account
184
- array :tags, default: nil do
185
- string
186
- end
187
- hash :prefs, default: nil do
188
- boolean :smoking
189
- boolean :view
190
- end
191
- date :arrives_on, default: -> { Date.current }
192
- date :departs_on, default: -> { Date.tomorrow }
193
- ```
901
+ ### Callbacks
194
902
 
195
- 3. Use any additional validations you need:
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.
196
906
 
197
- ``` ruby
198
- validates :name, length: { maximum: 10 }
199
- validates :state, inclusion: { in: %w(AL AK AR ... WY) }
200
- validate :arrives_before_departs
907
+ ``` rb
908
+ class Increment < ActiveInteraction::Base
909
+ set_callback :type_check, :before, -> { puts 'before type check' }
201
910
 
202
- private
911
+ integer :x
203
912
 
204
- def arrive_before_departs
205
- if departs_on <= arrives_on
206
- errors.add(:departs_on, 'must come after the arrival time')
207
- end
208
- end
209
- ```
913
+ set_callback :validate, :after, -> { puts 'after validate' }
210
914
 
211
- 4. Define your execute method. It can return whatever you like:
915
+ validates :x,
916
+ numericality: { greater_than_or_equal_to: 0 }
212
917
 
213
- ``` ruby
214
- def execute
215
- record = do_thing(...)
216
- # ...
217
- record
218
- end
219
- ```
918
+ set_callback :execute, :around, lambda { |_interaction, block|
919
+ puts '>>>'
920
+ block.call
921
+ puts '<<<'
922
+ }
923
+
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
+ ```
220
938
 
221
- Check out the [documentation][12] for a full list of methods.
939
+ In order, the available callbacks are `type_check`, `validate`, and `execute`.
940
+ You can set `before`, `after`, or `around` on any of them.
222
941
 
223
- ## How do I compose interactions?
942
+ ### Composition
224
943
 
225
- You can run interactions from within other interactions by calling `compose`.
226
- If the interaction is successful, it'll return the result (just like if you had
227
- called it with `run!`). If something went wrong, execution will halt
228
- immediately and the errors will be moved onto the caller.
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.
948
+
949
+ ``` rb
950
+ class Add < ActiveInteraction::Base
951
+ integer :x, :y
952
+
953
+ def execute
954
+ x + y
955
+ end
956
+ end
229
957
 
230
- ``` ruby
231
958
  class AddThree < ActiveInteraction::Base
232
959
  integer :x
960
+
233
961
  def execute
234
962
  compose(Add, x: x, y: 3)
235
963
  end
236
964
  end
965
+
237
966
  AddThree.run!(x: 5)
238
967
  # => 8
239
968
  ```
240
969
 
241
- To bring in filters from another interaction, use `import_filters`. Combined
970
+ To bring in filters from another interaction, use `.import_filters`. Combined
242
971
  with `inputs`, delegating to another interaction is a piece of cake.
243
972
 
244
- ``` ruby
973
+ ``` rb
245
974
  class AddAndDouble < ActiveInteraction::Base
246
975
  import_filters Add
976
+
247
977
  def execute
248
978
  compose(Add, inputs) * 2
249
979
  end
250
980
  end
251
981
  ```
252
982
 
253
- ## How do I translate an interaction?
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 symbolic errors for easier introspection and testing
1008
+ of errors. Symbolic 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
+ model :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 model"], :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 `#symbolic` 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.symbolic
1046
+ # => {"credit_card"=>[:missing], "item"=>[:invalid_type], "options"=>[:invalid_nested]}
1047
+ ```
1048
+
1049
+ Symbolic errors can also be manually added during the execute call by calling
1050
+ `#add_sym` instead of `#add` on `errors`. It works the same way as `add` except
1051
+ that the second argument is the error label.
1052
+
1053
+ ``` rb
1054
+ def execute
1055
+ errors.add_sym(:monster, :no_passage, 'You shall not pass!')
1056
+ end
1057
+ ```
1058
+
1059
+ ActiveInteraction also supports merging errors. This is useful if you want to
1060
+ delegate validation to some other object. For example, if you have an
1061
+ interaction that updates a record, you might want that record to validate
1062
+ itself. By using the `#merge!` helper on `errors`, you can do exactly that.
1063
+
1064
+ ``` rb
1065
+ class UpdateThing < ActiveInteraction::Base
1066
+ model :thing
1067
+
1068
+ def execute
1069
+ unless thing.save
1070
+ errors.merge!(thing.errors)
1071
+ end
1072
+
1073
+ thing
1074
+ end
1075
+ end
1076
+ ```
1077
+
1078
+ ### Forms
1079
+
1080
+ 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.
1081
+
1082
+ Given an application with an `Account` model we'll create a new `Account` using the `CreateAccount` interaction.
1083
+
1084
+ ```rb
1085
+ # GET /accounts/new
1086
+ def new
1087
+ @account = CreateAccount.new
1088
+ end
1089
+
1090
+ # POST /accounts
1091
+ def create
1092
+ outcome = CreateAccount.run(params.fetch(:account, {}))
1093
+
1094
+ if outcome.valid?
1095
+ redirect_to(outcome.result)
1096
+ else
1097
+ @account = outcome
1098
+ render(:new)
1099
+ end
1100
+ end
1101
+ ```
1102
+
1103
+ The form used to create a new `Account` has slightly more information on the `form_for` call than you might expect.
1104
+
1105
+ ```rb
1106
+ <%= form_for @account, as: :account, url: accounts_path do |f| %>
1107
+ <%= f.text_field :first_name %>
1108
+ <%= f.text_field :last_name %>
1109
+ <%= f.submit 'Create' %>
1110
+ <% end %>
1111
+ ```
1112
+
1113
+ 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`.
1114
+
1115
+ ```rb
1116
+ class CreateAccount < ActiveInteraction::Base
1117
+ ...
1118
+
1119
+ def to_model
1120
+ Account.new
1121
+ end
1122
+ end
1123
+ ```
1124
+
1125
+ Now our `form_for` call knows how to generate the correct URL and param name (i.e. `params[:account]`).
1126
+
1127
+ ```rb
1128
+ # app/views/accounts/new.html.erb
1129
+ <%= form_for @account do |f| %>
1130
+ ...
1131
+ <% end %>
1132
+ ```
1133
+
1134
+ 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.
1135
+
1136
+ ### Predicates
1137
+
1138
+ 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`).
1139
+
1140
+ ``` rb
1141
+ class SayHello < ActiveInteraction::Base
1142
+ string :name,
1143
+ default: nil
1144
+
1145
+ def execute
1146
+ if name?
1147
+ "Hello, #{name}!"
1148
+ else
1149
+ "Howdy, stranger!"
1150
+ end
1151
+ end
1152
+ end
1153
+
1154
+ SayHello.run!(name: nil)
1155
+ # => "Howdy, stranger!"
1156
+ SayHello.run!(name: 'Taylor')
1157
+ # => "Hello, Taylor!"
1158
+ ```
1159
+
1160
+ ### Translations
254
1161
 
255
- ActiveInteraction is i18n-aware out of the box! All you have to do
256
- is add translations to your project. In Rails, they typically go
257
- into `config/locales`. So, for example, let's say that (for whatever
258
- reason) you want to print out everything backwards. Simply add
259
- translations for ActiveInteraction to your `hsilgne` locale:
1162
+ ActiveInteraction is i18n aware out of the box! All you have to do is add
1163
+ translations to your project. In Rails, these typically go into
1164
+ `config/locales`. For example, let's say that for some reason you want to print
1165
+ everything out backwards. Simply add translations for ActiveInteraction to your
1166
+ `hsilgne` locale.
260
1167
 
261
- ``` yaml
1168
+ ``` yml
262
1169
  # config/locales/hsilgne.yml
263
1170
  hsilgne:
264
1171
  active_interaction:
@@ -272,41 +1179,60 @@ hsilgne:
272
1179
  float: taolf
273
1180
  hash: hsah
274
1181
  integer: regetni
1182
+ interface: ecafretni
275
1183
  model: ledom
276
1184
  string: gnirts
1185
+ symbol: lobmys
277
1186
  time: emit
278
1187
  errors:
279
1188
  messages:
280
1189
  invalid: dilavni si
1190
+ invalid_nested: (%{value} <= %{name}) eulav detsen dilavni na sah
281
1191
  invalid_type: '%{type} dilav a ton si'
282
1192
  missing: deriuqer si
283
1193
  ```
284
1194
 
285
- Then set your locale and run an interaction like normal:
1195
+ Then set your locale and run interactions like normal.
286
1196
 
287
- ``` ruby
288
- I18n.locale = :hsilgne
289
- class Interaction < ActiveInteraction::Base
290
- boolean :a
291
- def execute; end
1197
+ ``` rb
1198
+ class I18nInteraction < ActiveInteraction::Base
1199
+ string :name
292
1200
  end
293
- p Interaction.run.errors.messages
294
- # => {:a=>["deriuqer si"]}
1201
+
1202
+ I18nInteraction.run(name: false).errors.messages[:name]
1203
+ # => ["is not a valid string"]
1204
+
1205
+ I18n.locale = :hsilgne
1206
+ I18nInteraction.run(name: false).errors.messages[:name]
1207
+ # => ["gnirts dilav a ton si"]
295
1208
  ```
296
1209
 
297
1210
  ## Credits
298
1211
 
299
- ActiveInteraction is brought to you by [@AaronLasseigne][4] and
300
- [@tfausak][5] from [@orgsync][6]. We were inspired by the fantastic
301
- work done in [Mutations][7].
1212
+ ActiveInteraction is brought to you by [Aaron Lasseigne][] and
1213
+ [Taylor Fausak][] from [OrgSync][]. We were inspired by the fantastic work done
1214
+ by [Jonathan Novak][] on [Mutations][].
1215
+
1216
+ If you want to contribute to ActiveInteraction, please read
1217
+ [our contribution guidelines][]. A [complete list of contributors][] is
1218
+ available on GitHub.
1219
+
1220
+ ActiveInteraction is licensed under [the MIT License][].
302
1221
 
303
- Logo provided free by [Emoji One][8].
1222
+ Logo design by [Tyler Lee][].
304
1223
 
305
- [1]: http://orgsync.github.io/active_interaction/
306
- [2]: http://rubydoc.info/github/orgsync/active_interaction
307
- [3]: http://semver.org/spec/v2.0.0.html
308
- [4]: https://github.com/AaronLasseigne
309
- [5]: https://github.com/tfausak
310
- [6]: https://github.com/orgsync
311
- [7]: https://github.com/cypriss/mutations
312
- [8]: http://emojione.com/
1224
+ [the project page]: http://orgsync.github.io/active_interaction/
1225
+ [the full documentation]: http://rubydoc.info/github/orgsync/active_interaction
1226
+ [semantic versioning]: http://semver.org/spec/v2.0.0.html
1227
+ [the change log]: CHANGELOG.md
1228
+ [aaron lasseigne]: https://github.com/AaronLasseigne
1229
+ [taylor fausak]: https://github.com/tfausak
1230
+ [orgsync]: https://github.com/orgsync
1231
+ [jonathan novak]: https://github.com/cypriss
1232
+ [mutations]: https://github.com/cypriss/mutations
1233
+ [our contribution guidelines]: CONTRIBUTING.md
1234
+ [complete list of contributors]: https://github.com/orgsync/active_interaction/graphs/contributors
1235
+ [the mit license]: LICENSE.txt
1236
+ [formtastic]: https://rubygems.org/gems/formtastic
1237
+ [simple_form]: https://rubygems.org/gems/simple_form
1238
+ [tyler lee]: https://github.com/tylerlee