runger_actions 0.19.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.
data/README.md ADDED
@@ -0,0 +1,662 @@
1
+ [![codecov](https://codecov.io/gh/davidrunger/runger_actions/branch/master/graph/badge.svg)](https://codecov.io/gh/davidrunger/runger_actions)
2
+ ![GitHub tag (latest SemVer pre-release)](https://img.shields.io/github/v/tag/davidrunger/runger_actions?include_prereleases)
3
+
4
+ # RungerActions
5
+
6
+ Organize and validate the business logic of your Rails application with this combined form object /
7
+ command object.
8
+
9
+ # Table of Contents
10
+
11
+ <!--ts-->
12
+ * [RungerActions](#rungeractions)
13
+ * [Table of Contents](#table-of-contents)
14
+ * [Installation](#installation)
15
+ * [Usage in general](#usage-in-general)
16
+ * [Setup](#setup)
17
+ * [Generate your actions](#generate-your-actions)
18
+ * [Define your actions](#define-your-actions)
19
+ * [Invoke your actions](#invoke-your-actions)
20
+ * [Available methods](#available-methods)
21
+ * [Usage in specific](#usage-in-specific)
22
+ * [An #execute instance method is required!](#an-execute-instance-method-is-required)
23
+ * [Action class methods](#action-class-methods)
24
+ * [::requires](#requires)
25
+ * [Specifying the expected shape of a Hash input](#specifying-the-expected-shape-of-a-hash-input)
26
+ * [Specifying ActiveModel-style validations](#specifying-activemodel-style-validations)
27
+ * [Specifying arbitrary input "shapes" by providing a callable object](#specifying-arbitrary-input-shapes-by-providing-a-callable-object)
28
+ * [Specifying validations for ActiveRecord inputs](#specifying-validations-for-activerecord-inputs)
29
+ * [::returns](#returns)
30
+ * [The result object](#the-result-object)
31
+ * [All promised values must be returned](#all-promised-values-must-be-returned)
32
+ * [Validating the "shape" of returned values](#validating-the-shape-of-returned-values)
33
+ * [::fails_with](#fails_with)
34
+ * [Setting an error_message](#setting-an-error_message)
35
+ * [Alternatives](#alternatives)
36
+ * [Status / Context](#status--context)
37
+ * [Development](#development)
38
+ * [License](#license)
39
+
40
+ <!-- Created by https://github.com/ekalinin/github-markdown-toc -->
41
+ <!-- Added by: david, at: Sat May 20 12:56:10 CDT 2023 -->
42
+
43
+ <!--te-->
44
+
45
+ # Installation
46
+
47
+ Add the gem to your application's `Gemfile`.
48
+
49
+ ```rb
50
+ gem 'runger_actions'
51
+ ```
52
+
53
+ And then execute:
54
+
55
+ ```
56
+ $ bundle install
57
+ ```
58
+
59
+ # Usage in general
60
+
61
+ ## Setup
62
+
63
+ Create a new subdirectory within the `app/` directory in your Rails app: `app/actions/`.
64
+
65
+ Create an `app/actions/application_action.rb` file with this content:
66
+ ```rb
67
+ # app/actions/application_action.rb
68
+
69
+ class ApplicationAction < RungerActions::Base
70
+ end
71
+ ```
72
+
73
+ ## Generate your actions
74
+
75
+ This gem provides a Rails generator. For example, running:
76
+
77
+ ```
78
+ bin/rails g runger_actions:action Users::Create
79
+ ```
80
+
81
+ will create an empty action in `app/actions/users/create.rb`.
82
+
83
+ ## Define your actions
84
+
85
+ Then, you can start defining actions. Here's an example:
86
+ ```rb
87
+ # app/actions/send_text_message.rb
88
+
89
+ class SendTextMessage < ApplicationAction
90
+ requires :message_body, String, length: { minimum: 3 } # don't send any super short messages
91
+ requires :user, User do
92
+ validates :phone, presence: true, format: { with: /[[:digit:]]{11}/ }
93
+ end
94
+
95
+ returns :cost, Float, numericality: { greater_than_or_equal_to: 0 }
96
+ returns :nexmo_id, String, presence: true
97
+
98
+ fails_with :nexmo_request_failed
99
+
100
+ def execute
101
+ nexmo_response = NexmoClient.send_text!(number: user.phone, message: message_body)
102
+ if nexmo_response.success?
103
+ nexmo_response_data = nexmo_response.parsed_response
104
+ result.cost = nexmo_response_data['cost']
105
+ result.nexmo_id = nexmo_response_data['message-id']
106
+ else
107
+ result.nexmo_request_failed!
108
+ end
109
+ end
110
+ end
111
+ ```
112
+
113
+ ## Invoke your actions
114
+
115
+ Once you have defined one or more actions, you can invoke the action(s) anywhere in your code, such
116
+ as in a controller, as illustrated below.
117
+
118
+ ```rb
119
+ # app/controllers/api/text_messages_controller.rb
120
+
121
+ class Api::TextMessagesController < ApplicationController
122
+ def create
123
+ send_message_action =
124
+ SendTextMessage.new(
125
+ user: current_user,
126
+ message_body: "Hello! This message was generated at #{Time.current}.",
127
+ )
128
+
129
+ if !send_message_action.valid?
130
+ # We'll enter this block if one of the ActiveRecord inputs (`user`, in this case) for the
131
+ # action doesn't meet the required validations, e.g. if the user's `phone` is blank.
132
+ render json: { error: send_message_action.errors.full_messages.join(', ') }, status: 400
133
+ return
134
+ end
135
+
136
+ result = send_message_action.run
137
+ if result.success?
138
+ Rails.logger.info("Sent message with Nexmo id #{result.nexmo_id} at a cost of #{result.cost}")
139
+ head :created
140
+ elsif result.nexmo_request_failed?
141
+ render json: { error: 'An error occurred when sending the text message' }, status: 500
142
+ end
143
+ end
144
+ end
145
+ ```
146
+
147
+ You aren't limited to invoking actions from a controller action, though; you can invoke an action
148
+ from anywhere in your code.
149
+
150
+ One good place to invoke an action is from within *another* action. For a complex or multi-step
151
+ process, you might want to break that process down into several "sub actions" that can be invoked
152
+ from the `#execute` method of a coordinating "parent action".
153
+
154
+ ### Available methods
155
+
156
+ There are a few different methods that can be used to instantiate and/or run an action:
157
+ 1. `::run!` class method
158
+ 2. `::new!` class method
159
+ 3. `::new` class method
160
+ 4. `#run!` instance method
161
+ 5. `#run` instance method
162
+
163
+ #### `::run!` class method
164
+
165
+ This will attempt to instantiate an action (via `::new!`) and then attempt to run the action (via
166
+ `#run!`). If there are any validation errors and/or if any `fails_with` conditions are invoked
167
+ during execution, then an error will be raised.
168
+
169
+ Example:
170
+ ```rb
171
+ SendTextMessage.run!(user: current_user, message_body: 'Hello!')
172
+ ```
173
+
174
+ #### `::new!` class method
175
+
176
+ This will attempt to instantiate an action. If there are any validation errors, then an error will
177
+ be raised.
178
+
179
+ Example:
180
+ ```rb
181
+ action = SendTextMessage.new!(user: current_user, message_body: 'Hi!')
182
+ ```
183
+
184
+ #### `::new` class method
185
+
186
+ This will instantiate an action. Even if there are ActiveModel validation errors, an error will
187
+ **not** be raised.
188
+
189
+ Example:
190
+ ```rb
191
+ action = SendTextMessage.new(user: current_user, message_body: 'Hi!')
192
+ ```
193
+
194
+ #### `#run!` instance method
195
+
196
+ This will attempt to run an action. If any `fails_with` conditions are invoked during execution,
197
+ then an error will be raised.
198
+
199
+ Example:
200
+ ```rb
201
+ action = SendTextMessage.new!(user: current_user, message_body: 'Hi!')
202
+ action.run!
203
+ ```
204
+
205
+ #### `#run` instance method
206
+
207
+ This will run an action. If any `fails_with` conditions are invoked during execution, then an error
208
+ will **not** be raised. The errors will be registered on the `result` object.
209
+
210
+ Example:
211
+ ```rb
212
+ action = SendTextMessage.new!(user: current_user, message_body: 'Hi!')
213
+ result = action.run
214
+ result.nexmo_request_failed? # check if a `fails_with` condition was invoked
215
+ ```
216
+
217
+ # Usage in specific
218
+
219
+ ## An `#execute` instance method is required!
220
+
221
+ The only real requirement for an action is that it implements an `#execute` instance method.
222
+
223
+ ```rb
224
+ class DoSomething < ApplicationAction
225
+ def execute
226
+ # you MUST write an #execute instance method for your action
227
+ end
228
+ end
229
+ ```
230
+
231
+ Although all actions must implement an `#execute` instance method, you should generally not invoke
232
+ that method directly in your application code. Instead, call `#run` on an instance of the class:
233
+
234
+ ```rb
235
+ # this will run the DoSomething#execute instance method
236
+ DoSomething.new.run
237
+ ```
238
+
239
+ ## Action class methods
240
+
241
+ When defining an action class, these three class methods are available:
242
+ 1. `requires`
243
+ 2. `returns`
244
+ 3. `fails_with`
245
+
246
+ Those class methods are all optional, though. We'll detail/illustrate their usage below.
247
+
248
+ ### `::requires`
249
+
250
+ The `::requires` class method declares the necessary, expected inputs that are needed in order to
251
+ execute an action.
252
+
253
+ An action can have zero, one, or more `requires` statements.
254
+
255
+ An action that requires no input values will have no `requires` statements:
256
+
257
+ ```rb
258
+ class PrintCurrentTime < ApplicationAction
259
+ def execute
260
+ puts("The current time is #{Time.now}.")
261
+ end
262
+ end
263
+
264
+ PrintCurrentTime.new.run
265
+ # => prints "The current time is 2020-06-20 03:25:14 -0700."
266
+ ```
267
+
268
+ Most actions probably will take one or more inputs, though. Here's an example of an action with one
269
+ `requires` statement:
270
+
271
+ ```rb
272
+ class PrintDoubledNumber < ApplicationAction
273
+ requires :number, Numeric
274
+
275
+ def execute
276
+ puts("#{number} doubled is #{number * 2}")
277
+ end
278
+ end
279
+
280
+ PrintDoubledNumber.new(number: 8).run
281
+ # => prints "8 doubled is 16"
282
+ ```
283
+
284
+ In the example above, because the `PrintDoubledNumber` action class declares `requires :number`, a
285
+ `#number` instance method is available for all instances of that action class. This `#number`
286
+ instance method is used within the `PrintDoubledNumber#execute` action.
287
+
288
+ All subsequent arguments given to `requires` are used to define a "shape" via the [`shaped`
289
+ gem](https://github.com/davidrunger/shaped/).
290
+
291
+ The simplest way to define the expected "shape" of a required action parameter is probably to
292
+ declare its expected class, as illustrated above (where we specified that the `number` input
293
+ parameter must be an instance of `Numeric`). However, the `shaped` gem supports a wide variety of
294
+ ways to specify the expected "shape" of an input. A few additional examples are shown below; see the
295
+ [`shaped` documentation](https://github.com/davidrunger/shaped/) for more possibilities.
296
+
297
+ #### Specifying the expected shape of a Hash input
298
+
299
+ ```rb
300
+ class PrintNameAndEmail < ApplicationAction
301
+ # The `{ email: String, phone: String }` argument specifies the expected shape of `user_data`.
302
+ requires :user_data, { name: String, email: String }
303
+
304
+ def execute
305
+ puts("The email of #{user_data[:name]} is #{user_data[:email]}.")
306
+ end
307
+ end
308
+
309
+ PrintNameAndEmail.new(user_data: { name: 'Tom', email: 'tommy@example.com' }).run
310
+ # => prints "The email of Tom is tommy@example.com."
311
+
312
+ # The name and email keys are strings; they are supposed to be symbols.
313
+ PrintNameAndEmail.new(user_data: { 'name' => 'Thomas', 'email' => 'tommy@example.com' })
314
+ # => raises RungerActions::TypeMismatch
315
+
316
+ # The `:name` key is missing in the `user_data` hash.
317
+ PrintNameAndEmail.new(user_data: { email: 'tommy@example.com' })
318
+ # => raises RungerActions::TypeMismatch
319
+ ```
320
+
321
+ #### Specifying ActiveModel-style validations
322
+
323
+ ```rb
324
+ class PrintEmail < ApplicationAction
325
+ requires :email, String, format: { with: /.+@.+\..+/ }, length: { minimum: 6 }
326
+
327
+ def execute
328
+ puts("The email is '#{email}'.")
329
+ end
330
+ end
331
+
332
+ PrintEmail.new(email: 'jefferson@example.com').run
333
+ # => prints "The email is 'jefferson@example.com'."
334
+
335
+ # This email doesn't match the specified regex
336
+ PrintEmail.new(email: 'Thomas Jefferson')
337
+ # => raises RungerActions::TypeMismatch
338
+
339
+ # This email is too short
340
+ PrintEmail.new(email: 'a@b.c')
341
+ # => raises RungerActions::TypeMismatch
342
+ ```
343
+
344
+ #### Specifying arbitrary input "shapes" by providing a callable object
345
+
346
+ You can leverage `shaped`'s [`Callable` shape
347
+ type](https://github.com/davidrunger/shaped/#shapedshapescallable) by providing any object that
348
+ responds to `#call` (such as a lambda). This allows you unlimited flexibility to define requirements
349
+ for the action's input(s).
350
+
351
+ ```rb
352
+ class PrintSmallEvenNumber < ApplicationAction
353
+ requires :small_even_number, ->(number) { (0..6).cover?(number) && number.even? }
354
+
355
+ def execute
356
+ puts("#{small_even_number} is a small, even number.")
357
+ end
358
+ end
359
+
360
+ PrintSmallEvenNumber.new(small_even_number: 2).run
361
+ # => prints "2 is a small, even number."
362
+
363
+ # This number is not even
364
+ PrintSmallEvenNumber.new(small_even_number: 3).run
365
+ # => raises RungerActions::TypeMismatch
366
+
367
+ # This number is not small
368
+ PrintSmallEvenNumber.new(small_even_number: 200).run
369
+ # => raises RungerActions::TypeMismatch
370
+ ```
371
+
372
+ #### Specifying validations for ActiveRecord inputs
373
+
374
+ When declaring a `requires` where the input is specified (via the second argument to `requires`) to
375
+ be a class that inherits from `ActiveRecord::Base`, there are a few special things that happen:
376
+ 1. You can provide a **validation block** for the ActiveRecord object. Within this block, you can
377
+ specify validations on attributes of that ActiveRecord model.
378
+ 2. You can check, by calling `valid?` on an instance of the action, whether the ActiveRecord
379
+ object(s) that are inputs for the action meet the **validation block** validations.
380
+ 3. You can access any validation errors (from the **validation block**) via the `#errors` method of
381
+ the action instance.
382
+ 4. You can execute the action instance via `run!` rather than `run`; this will raise an exception
383
+ (and not run the `#execute` method) if any of the validations from a **validation block** are not
384
+ met.
385
+
386
+ ```rb
387
+ class PrintFirstAndLastName < ApplicationAction
388
+ requires :user, User do
389
+ validates :name, format: { with: /.+ .+/ }
390
+ end
391
+
392
+ def execute
393
+ name_parts = user.name.split(' ')
394
+ puts("First name: #{name_parts.first}. Last name: #{name_parts.last}")
395
+ end
396
+ end
397
+
398
+ user = User.find(1)
399
+ user.is_a?(ActiveRecord::Base)
400
+ # => true
401
+ user.name
402
+ # => "David Runger"
403
+ action = PrintFirstAndLastName.new(user: user)
404
+ action.valid?
405
+ # => true
406
+ action.errors.to_hash
407
+ # => {}
408
+ action.run!
409
+ # => prints "First name: David. Last name: Runger"
410
+
411
+ user = User.find(2)
412
+ user.name
413
+ # => "Cher"
414
+ action = PrintFirstAndLastName.new(user: user)
415
+ action.valid?
416
+ # => false
417
+ action.errors.to_hash
418
+ # => {:name=>["is invalid"]}
419
+ action.run!
420
+ # => raises RungerActions::InvalidParam
421
+ ```
422
+
423
+ ### `::returns`
424
+
425
+ The `::returns` class method describes the value(s) that an action promises to return (if any).
426
+
427
+ As with `requires`, an action can have zero, one, or more `returns` statements.
428
+
429
+ An action that is used for its "side effects," such as most of the examples above that use `puts` to
430
+ print output, will probably not have any `returns` statements.
431
+
432
+ However, if you want the action to return object(s)/data to other parts of your code, then you'll
433
+ need to declare those return values using the `returns` class method.
434
+
435
+ Here's an example:
436
+
437
+ ```rb
438
+ class MultiplyNumber < ApplicationAction
439
+ requires :input_number, Numeric
440
+
441
+ returns :doubled_number, Numeric
442
+ returns :tripled_number, Numeric
443
+
444
+ def execute
445
+ result.doubled_number = input_number * 2
446
+ result.tripled_number = input_number * 3
447
+ end
448
+ end
449
+
450
+ multiply_result = MultiplyNumber.new(input_number: 1.5).run
451
+ multiply_result.class
452
+ # => MultiplyNumber::Result
453
+ puts("The number doubled is #{multiply_result.doubled_number}")
454
+ # => prints "The number doubled is 3.0"
455
+ puts("The number tripled is #{multiply_result.tripled_number}")
456
+ # => prints "The number tripled is 4.5"
457
+ ```
458
+
459
+ #### The `result` object
460
+
461
+ We can see in the example above that `MultiplyNumber#execute` references `result`, which is an
462
+ object provided automatically to action instances. Because the `MultiplyNumber` action declares
463
+ `returns :doubled_number` and `returns :tripled_number`, the `result` object automatically has
464
+ `#doubled_number=` and `#tripled_number=` writer methods, which can (and should) be invoked by the
465
+ action instance in order to set those values on the `result` object.
466
+
467
+ When we call `MultiplyNumber.new(input_number: 1.5).run`, the return value of `#run` is the action's
468
+ `result` object. Outside of the action, we can then access the return values that were set within
469
+ the action's `#execute` method; we do this via the `#doubled_number` and `#tripled_number` reader
470
+ methods that are defined on the result object (which we captured in a local variable called
471
+ `multiply_result`).
472
+
473
+ #### All promised values must be returned
474
+
475
+ If an action fails to set any promised return values on the `result` object, then an error will be
476
+ raised when `#run` is called:
477
+
478
+ ```rb
479
+ class MultiplyNumber < ApplicationAction
480
+ requires :input_number, Numeric
481
+
482
+ returns :doubled_number, Numeric
483
+ returns :tripled_number, Numeric
484
+
485
+ def execute
486
+ # PROBLEM BELOW! An error will be raised when this action is executed,
487
+ # because we fail to set a `doubled_number` return value.
488
+
489
+ # result.doubled_number = input_number * 2
490
+ result.tripled_number = input_number * 3
491
+ end
492
+ end
493
+
494
+ multiply_result = MultiplyNumber.new(input_number: 10).run
495
+ # => raises RungerActions::MissingResultValue
496
+ ```
497
+
498
+ #### Validating the "shape" of returned values
499
+
500
+ As with the `requires` action class method, the "shape" of the promised return values declared via
501
+ `returns` can be described via the arguments to `returns`, which are passed to the [`shaped`
502
+ gem](https://github.com/davidrunger/shaped/). Leveraging this functionality allows you to ensure
503
+ that your action is providing the expected type of return values.
504
+
505
+ ```rb
506
+ class UppercaseEmail < ApplicationAction
507
+ requires :email, String, format: { with: /.+@.+/ }
508
+
509
+ returns :uppercased_email, String, format: { with: /[A-Z]+@[A-Z.]+/ }
510
+
511
+ def execute
512
+ result.uppercased_email = email.upcase
513
+ end
514
+ end
515
+
516
+ UppercaseEmail.new(email: 'david@protonmail.com').run.uppercased_email
517
+ # => "DAVID@PROTONMAIL.COM"
518
+ ```
519
+
520
+ If an action attempts to set a return value that doesn't match the specified "shape" for that return
521
+ value, then an `RungerActions::TypeMismatch` error will be raised:
522
+
523
+ ```rb
524
+ class UppercaseEmail < ApplicationAction
525
+ requires :email, String, format: { with: /.+@.+/ }
526
+
527
+ returns :uppercased_email, String, format: { with: /[A-Z]+@[A-Z.]+/ }
528
+
529
+ def execute
530
+ # PROBLEM BELOW! This action is supposed to _upcase_ the email, not downcase it!
531
+ result.uppercased_email = email.downcase
532
+ end
533
+ end
534
+
535
+ UppercaseEmail.new(email: 'david@protonmail.com').run
536
+ # => raises RungerActions::TypeMismatch
537
+ ```
538
+
539
+ ### `::fails_with`
540
+
541
+ The `::fails_with` class method can be used to enumerate possible "failure modes" for the action.
542
+
543
+ As with `requires` and `returns`, an action can have zero, one, or more `fails_with` statements.
544
+
545
+ Generally, it's best to try to write actions in a way such that we don't expect any failures, but
546
+ sometimes there are things outside of our control; in such cases, using `fails_with` to list these
547
+ possible points of failure is a good idea. For example, a call to an external API might time out or
548
+ receive a 500 error response.
549
+
550
+ Here's a (contrived) example with one `fails_with` declaration:
551
+
552
+ ```rb
553
+ class PrintRandomNumberAboveFive < ApplicationAction
554
+ fails_with :number_was_too_small
555
+
556
+ def execute
557
+ random_number = rand(10)
558
+ if random_number > 5
559
+ puts(random_number)
560
+ else
561
+ result.number_was_too_small!
562
+ end
563
+ end
564
+ end
565
+
566
+ result = PrintRandomNumberAboveFive.new.run
567
+ # => prints "9" (sometimes)
568
+ result.success?
569
+ # => true
570
+ result.number_was_too_small?
571
+ # => false
572
+ ```
573
+
574
+ In the case above, we didn't encounter the error condition, which we can verify via the `#success?`
575
+ and `#number_was_too_small?` methods on the result. `#success?` is available on all action results,
576
+ and `#number_was_too_small?` is available for this particular action result because the action class
577
+ declares `fails_with :number_was_too_small`.
578
+
579
+ And here's what a failure case would look like:
580
+
581
+ ```rb
582
+ result = PrintRandomNumberAboveFive.new.run
583
+ # => [doesn't print anything, if the random number is <= 5]
584
+ result.success?
585
+ # => false
586
+ result.number_was_too_small?
587
+ # => true
588
+ ```
589
+
590
+ In this case, we entered the `else` branch of the action's `#execute` method and called the
591
+ `result.number_was_too_small!` method (made available automatically because of the class's
592
+ `fails_with :number_was_too_small` declaration). Since we called the `result.number_was_too_small!`
593
+ method, indicating that that failure mode occurred when executing the action, `#success?` returns
594
+ `false` and `#number_was_too_small?` returns `true`.
595
+
596
+ #### Setting an `error_message`
597
+
598
+ When invoking a `fails_with` error case, the bang method can optionally take an error message as an
599
+ argument, which will then be made available via a special `error_message` reader on the result
600
+ object:
601
+
602
+ ```rb
603
+ class SellAlcohol < ApplicationAction
604
+ requires :age, Numeric
605
+
606
+ fails_with :too_young
607
+
608
+ def execute
609
+ if age < 21
610
+ result.too_young!("Age #{age} is too young to buy alcohol.")
611
+ else
612
+ puts('Enjoy your alcohol responsibly!')
613
+ end
614
+ end
615
+ end
616
+
617
+ result = SellAlcohol.new!(age: 17).run
618
+ result.success?
619
+ # => false
620
+ result.too_young?
621
+ # => true
622
+ result.error_message
623
+ # => "Age 17 is too young to buy alcohol."
624
+ ```
625
+
626
+ # Alternatives
627
+
628
+ This project is not the first of its kind!
629
+
630
+ Here are a few similar projects:
631
+ * [`interactor`](https://github.com/collectiveidea/interactor)
632
+ * [`active_interaction`](https://github.com/AaronLasseigne/active_interaction)
633
+ * [`mutations`](https://github.com/cypriss/mutations)
634
+ * [`service_actor`](https://github.com/sunny/actor)
635
+
636
+ # Status / Context
637
+
638
+ I wouldn't recommend using this gem in production. It's very new (i.e. probably rough around the
639
+ edges, subject to significant changes at a relatively rapid rate, and arguably somewhat feature
640
+ incomplete) and I am not committed to maintaing the gem.
641
+
642
+ I mostly built this gem because I wasn't _quite_ satisfied with any of the above alternatives that I
643
+ knew about at the time that I decided to start building it. I built this gem mostly to scratch my
644
+ own itch and for the sake of exploring this problem space a little bit.
645
+
646
+ I am actively using this gem in the small Rails application that hosts my personal website and apps;
647
+ you can check out its [`app/actions/`
648
+ directory](https://github.com/davidrunger/david_runger/tree/master/app/actions) if you are
649
+ interested in seeing some real-world use cases.
650
+
651
+ # Development
652
+
653
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/rspec` to run
654
+ the tests. You can also run `bin/console` for an interactive prompt that will allow you to
655
+ experiment.
656
+
657
+ To install this gem onto your local machine, run `bundle exec rake install`.
658
+
659
+ # License
660
+
661
+ The gem is available as open source under the terms of the [MIT
662
+ License](https://opensource.org/licenses/MIT).
data/RELEASING.md ADDED
@@ -0,0 +1,7 @@
1
+ To release a new version, run `bin/release` with an appropriate `--type` option, e.g.:
2
+
3
+ ```
4
+ bin/release --type minor
5
+ ```
6
+
7
+ (This uses the `release_assistant` gem.)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task(default: :spec)
data/bin/_guard-core ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application '_guard-core' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', Pathname.new(__FILE__).realpath)
13
+
14
+ bundle_binstub = File.expand_path('bundle', __dir__)
15
+
16
+ if File.file?(bundle_binstub)
17
+ if File.read(bundle_binstub, 300).include?('This file was generated by Bundler')
18
+ load(bundle_binstub)
19
+ else
20
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
21
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
22
+ end
23
+ end
24
+
25
+ require 'rubygems'
26
+ require 'bundler/setup'
27
+
28
+ load Gem.bin_path('guard', '_guard-core')