use_cases 1.0.6 → 1.0.11
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/Gemfile.lock +1 -1
- data/README.md +104 -184
- data/lib/use_cases/events/publish_job.rb +2 -8
- data/lib/use_cases/events/publisher.rb +42 -1
- data/lib/use_cases/events/subscriber.rb +7 -0
- data/lib/use_cases/module_optins/publishing.rb +21 -19
- data/lib/use_cases/stack_runner.rb +0 -2
- data/lib/use_cases/step_adapters/abstract.rb +0 -1
- data/lib/use_cases/version.rb +1 -1
- data/lib/use_cases.rb +1 -19
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f810a00826d0e18fb1defbddb6a54055f4b77b43141a943d48d8a56273b7bf9
|
4
|
+
data.tar.gz: b6f1724c28186e1e8f4ca3b0b79807d4db534903b5f0ee5ace23363b180b8fc2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8973b6cc8d4b89d16af39f010af3a8bf8cd9aea88f9c5c3679356955fc566ee1596711eed422beb9bd928364645640689ec03efd4e8cde11ec7b9d5c3e5c1357
|
7
|
+
data.tar.gz: b7c595a0645bf0b8938aa09f99d9fe8167b34c0ca6d9c6e9dfa4ed652a9c8341dfcf216fe70c222ad2321fb094f4667417a6bf1efb7cc1c8c2f2b79b8fe5c0a4
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -8,254 +8,174 @@
|
|
8
8
|
|
9
9
|
It's concept is largely based on `dry-transaction` but does not use it behind the scenes. Instead it relies on other `dry` libraries like [dry-validation](https://dry-rb.org/gems/dry-validation/), [dry-events](https://dry-rb.org/gems/dry-validation/) and [dry-monads](https://dry-rb.org/gems/dry-validation/) to implement a DSL that can be flexible enough for your needs.
|
10
10
|
|
11
|
-
|
11
|
+
### Including UseCase
|
12
12
|
|
13
|
-
|
14
|
-
2. It abstracts common steps like **authorization** and **validation** into macros.
|
15
|
-
3. It solves what we consider a problem of `dry-transaction`. The way it funnels down `input` through `Dry::Monads` payloads alone. `UseCases` offers more flexibility in a way that still promotes functional programming values.
|
16
|
-
4. It implements a simple pub/sub mechanism which can be async when `ActiveJob` is a project dependency.
|
17
|
-
5. It implements an `enqueue` mechanism to delay execution of steps, also using `ActiveJob` as a dependency.
|
13
|
+
Including the `UseCase` module ensures that your class implements the base use case [Base DSL](#the-base-dsl).
|
18
14
|
|
19
|
-
|
15
|
+
```ruby
|
16
|
+
class Users::Create
|
17
|
+
include UseCase
|
18
|
+
end
|
19
|
+
````
|
20
20
|
|
21
|
-
|
21
|
+
In order to add optional modules (optins), use the following notation:
|
22
22
|
|
23
23
|
```ruby
|
24
|
-
|
24
|
+
class Users::Create
|
25
|
+
include UseCase[:validated, :transactional, :publishing]
|
26
|
+
end
|
25
27
|
```
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
$ bundle install
|
30
|
-
|
31
|
-
Or install it yourself as:
|
29
|
+
### Using a UseCase
|
32
30
|
|
33
|
-
|
34
|
-
|
35
|
-
|
31
|
+
```ruby
|
32
|
+
create_user = Users::Create.new
|
33
|
+
params = { first_name: 'Don', last_name: 'Quixote' }
|
36
34
|
|
37
|
-
|
35
|
+
result = create_user.call(params, current_user)
|
38
36
|
|
39
|
-
|
37
|
+
# Checking if succeeded
|
38
|
+
result.success?
|
40
39
|
|
41
|
-
|
40
|
+
# Checking if failed
|
41
|
+
result.failure?
|
42
42
|
|
43
|
-
|
43
|
+
# Getting return value
|
44
|
+
result.value!
|
45
|
+
```
|
44
46
|
|
45
|
-
|
47
|
+
Or with using dry-matcher by passing a block:
|
46
48
|
|
47
49
|
```ruby
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
check :current_user_is_user?
|
52
|
-
step :build_user
|
53
|
-
map :persist_user
|
50
|
+
create_user = Users::Create.new
|
51
|
+
params = { first_name: 'Don', last_name: 'Quixote' }
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
create_user.call(params, current_user) do |on|
|
54
|
+
on.success do |user|
|
55
|
+
puts "#{user.first_name} created!"
|
58
56
|
end
|
59
57
|
|
60
|
-
|
58
|
+
on.failure do |(code, message)|
|
59
|
+
puts "Failure (#{code}): #{message}"
|
61
60
|
end
|
61
|
+
end
|
62
|
+
```
|
62
63
|
|
64
|
+
### Available Optins
|
63
65
|
|
64
|
-
def do_something(user, params, current_user)
|
65
|
-
params[:should_fail] ? Failure([:failed, "failed"]) : Success("it succeeds!")
|
66
|
-
end
|
67
|
-
end
|
68
66
|
|
69
|
-
|
67
|
+
| Optin | Description |
|
68
|
+
|---|---|
|
69
|
+
| `:authorized` | Adds an extra `authorize` step macro, used to check user permissions. |
|
70
|
+
| `:prepared` | Adds an extra `prepare` step macro, used to run some code before the use case runs. |
|
71
|
+
| `:publishing` | Adds extra extra `publish` option to all steps, which allows a step to broadcast an event after executing |
|
72
|
+
| `:transactional` | Calls `#transaction` on a given `transaction_handler` object around the use case execution |
|
73
|
+
| `:validated` | Adds all methods of `dry-transaction` to the use case DSL, which run validations on the received `params` object. |
|
70
74
|
|
71
|
-
|
72
|
-
match.failure :failed do |(code, result)|
|
73
|
-
puts code
|
74
|
-
end
|
75
|
+
### The base DSL
|
75
76
|
|
76
|
-
|
77
|
-
puts message
|
78
|
-
end
|
79
|
-
end
|
80
|
-
# => failed
|
77
|
+
Use cases implements a DSL similar to dry-transaction, using the [Railway programming paradigm](https://fsharpforfunandprofit.com/rop/).
|
81
78
|
|
82
|
-
params = { should_fail: false }
|
83
79
|
|
84
|
-
|
85
|
-
match.failure :failed do |(code, result)|
|
86
|
-
puts code
|
87
|
-
end
|
80
|
+
Each step macro has a different use case, and so a different subset of available options, different expectations in return values, and interaction with the following step.
|
88
81
|
|
89
|
-
|
90
|
-
puts message
|
91
|
-
end
|
92
|
-
end
|
93
|
-
# => it succeeds!
|
94
|
-
```
|
82
|
+
By taking a simple look at the definition of a use case, anyone should be able to understand the business rules it emcompasses. For that it is necessary to understand the following matrix.
|
95
83
|
|
96
|
-
**Complex Example**
|
97
84
|
|
98
|
-
|
99
|
-
|
100
|
-
|
85
|
+
| | Rationale for use | Accepted Options | Expected return | Passes return value |
|
86
|
+
|---|---|---|---|---|
|
87
|
+
| **step** | This step has some complexity, and it can fail or succeed. | `with`, `pass` | `Success`/ `Failure` | ✅ |
|
88
|
+
| **check** | This step checks sets some rules for the operation, usually verifying that domain models fulfil some conditions. | `with`, `pass`, `failure`, `failure_message` | `boolean` | ❌ |
|
89
|
+
| **map** | Nothing should go wrong within this step. If it does, it's an unexpected application error. | `with`, `pass` | `any` | ✅ |
|
90
|
+
| **try** | We expect that, in some cases, errors will occur, and the operation fails in that case. | `catch`, `with`, `pass`, `failure`, `failure_message` | `any` | ✅ |
|
91
|
+
| **tee** | We don't care if this step succeeds or fails, it's used for non essential side effects. | `with`, `pass` | `any` | ❌ |
|
101
92
|
|
102
|
-
|
93
|
+
#### Optional steps
|
103
94
|
|
104
|
-
|
95
|
+
| | Rationale for use | Accepted Options | Expected return | Passes return value |
|
96
|
+
|---|---|---|---|---|
|
97
|
+
| **enqueue** *(requires ActiveJob defined) | The same as a `tee`, but executed later to perform non-essential expensive operations. | `with`, `pass`, and sidekiq options | `any` | ❌ |
|
98
|
+
| **authorize**<br> *(requires authorized) | Performs authorization on the current user, by running a `check` which, in case of failure, always returns an `unauthorized` failure. | `with`, `pass`, `failure_message` | `boolean` | ❌ |
|
99
|
+
| **prepare**<br> *(requires prepared) | Adds a `tee` step that always runs first. Used to mutate params if necessary. | `with`, `pass` | `any` | ❌ |
|
105
100
|
|
106
|
-
tee :log_a_message
|
107
101
|
|
108
|
-
|
102
|
+
### Defining Steps
|
109
103
|
|
110
|
-
|
104
|
+
Defining a step can be done in the body of the use case.
|
111
105
|
|
112
|
-
|
106
|
+
```ruby
|
107
|
+
class Users::DeleteAccount
|
108
|
+
include UseCases[:validated, :transactional, :publishing, :validated]
|
113
109
|
|
114
|
-
|
110
|
+
step :do_something, {}
|
111
|
+
```
|
115
112
|
|
116
|
-
|
117
|
-
Resource.find(params[:id])
|
118
|
-
end
|
113
|
+
In real life, a simple use case would look something like:
|
119
114
|
|
120
|
-
|
121
|
-
|
115
|
+
```ruby
|
116
|
+
class Users::DeleteAccount
|
117
|
+
include UseCases[:validated, :transactional, :publishing, :validated]
|
122
118
|
|
123
|
-
|
124
|
-
|
125
|
-
else
|
126
|
-
Failure([:failed, "could not update resource"])
|
127
|
-
end
|
119
|
+
params do
|
120
|
+
required(:id).filled(:str?)
|
128
121
|
end
|
129
122
|
|
130
|
-
|
131
|
-
|
132
|
-
|
123
|
+
authorize :user_owns_account?, failure_message: 'Cannot delete account'
|
124
|
+
try :load_account, catch: ActiveRecord::RecordNotFound, failure: :account_not_found, failure_message: 'Account not found'
|
125
|
+
map :delete_account, publish: :account_deleted
|
126
|
+
enqueue :send_farewell_email
|
133
127
|
|
134
|
-
|
135
|
-
user.can_create?(Resource)
|
136
|
-
end
|
128
|
+
private
|
137
129
|
|
138
|
-
def
|
139
|
-
|
130
|
+
def user_owns_account?(_previous_step_input, params, current_user)
|
131
|
+
current_user.account_id == params[:id]
|
140
132
|
end
|
141
133
|
|
142
|
-
def
|
143
|
-
|
134
|
+
def load_account(_previous_step_input, params, _current_user)
|
135
|
+
Account.find_by!(user_id: params[:id])
|
144
136
|
end
|
145
|
-
end
|
146
|
-
```
|
147
|
-
|
148
|
-
### Authorization
|
149
|
-
|
150
|
-
`authorize` is a `check` step that returns an `:unauthorized` code in it's `Failure`.
|
151
137
|
|
152
|
-
|
153
|
-
|
154
|
-
```ruby
|
155
|
-
class YourCase < UseCases::Base
|
156
|
-
authorize 'User must be over 18' do |user|
|
157
|
-
user.age >= 18
|
138
|
+
def delete_account(account, _params, _current_user)
|
139
|
+
account.destroy!
|
158
140
|
end
|
159
|
-
end
|
160
141
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
puts code
|
142
|
+
# since this executed async, all args are serialized
|
143
|
+
def send_farewell_email(account_attrs, params, current_user_attrs)
|
144
|
+
user = User.find(params[:id])
|
145
|
+
UserMailer.farewell(user).deliver_now!
|
166
146
|
end
|
167
147
|
end
|
168
|
-
# => User must be over 18
|
169
148
|
```
|
170
149
|
|
171
|
-
|
150
|
+
#### Available Options
|
172
151
|
|
173
|
-
|
152
|
+
| Name | Description | Expected Usage |
|
153
|
+
|---|---|---|
|
154
|
+
| `with` | Retrieves the callable object used to perform the step. |<em><br> Symbol: `send(options[:with])` <br> String: `UseCases.config.container[options[:with]]` <br> Class: `options[:with]`</em> |
|
155
|
+
| `pass` | An array of the arguments to pass to the object set by `with`. <br> _options: params, current_user & previous_step_result_ | Array\<Symbol> |
|
156
|
+
| `failure` | The code passed to the Failure object. | Symbol / String |
|
157
|
+
| `failure_message` | The string message passed to the Failure object. | Symbol / String |
|
158
|
+
| `catch` | Array of error classes to rescue from. | Array\<Exception>
|
174
159
|
|
175
|
-
|
160
|
+
## Installation
|
176
161
|
|
177
|
-
|
178
|
-
- The post must be between 25 and 150 characters.
|
179
|
-
- The post must be sanitized to remove any sensitive or explicit content.
|
180
|
-
- The post must be saved to the database in the end.
|
181
|
-
- In case any conditions are not met, an failure should be returned with it's own error code.
|
162
|
+
Add this line to your application's Gemfile:
|
182
163
|
|
183
164
|
```ruby
|
184
|
-
|
185
|
-
|
186
|
-
params do
|
187
|
-
required(:body).filled(:string).value(size?: 25..150)
|
188
|
-
required(:thread_id).filled(:integer)
|
189
|
-
end
|
190
|
-
|
191
|
-
try :load_post_thread, failure: :not_found
|
192
|
-
|
193
|
-
authorize 'User is not active' do |user, params, thread|
|
194
|
-
user.active?
|
195
|
-
end
|
196
|
-
|
197
|
-
authorize 'User must be active or the thread author.' do |user, params, thread|
|
198
|
-
user.active? || thread.author == user
|
199
|
-
end
|
200
|
-
|
201
|
-
step :sanitize_body
|
202
|
-
|
203
|
-
step :create_post
|
204
|
-
|
205
|
-
private
|
165
|
+
gem 'use_cases'
|
166
|
+
```
|
206
167
|
|
207
|
-
|
208
|
-
Thread.find(params[:thread_id])
|
209
|
-
end
|
168
|
+
And then execute:
|
210
169
|
|
211
|
-
|
212
|
-
SanitizeText.new.call(params[:body]).to_monad
|
213
|
-
end
|
170
|
+
$ bundle install
|
214
171
|
|
215
|
-
|
216
|
-
post = Post.new(body: body, user_id: user.id, thread_id: params[:thread_id])
|
172
|
+
Or install it yourself as:
|
217
173
|
|
218
|
-
|
219
|
-
end
|
220
|
-
end
|
221
|
-
```
|
174
|
+
$ gem install use_cases
|
222
175
|
|
223
|
-
|
176
|
+
## Usage
|
224
177
|
|
225
|
-
|
226
|
-
# app/controllers/posts_controller.rb
|
227
|
-
class PostsController < ApplicationController
|
228
|
-
def create
|
229
|
-
Posts::Create.new.call(params, current_user) do |match|
|
230
|
-
|
231
|
-
# in success, the return value is the Success payload of the last step (#create_post)
|
232
|
-
match.success do |post|
|
233
|
-
# result => <Post:>
|
234
|
-
end
|
235
|
-
|
236
|
-
# in case ::params or any other dry-validation fails.
|
237
|
-
match.failure :validation_error do |result|
|
238
|
-
# result => [:validation_error, ['validation_error', { thread_id: 'is missing' }]
|
239
|
-
end
|
240
|
-
|
241
|
-
# in case ::try raises an error (ActiveRecord::NotFound in this case)
|
242
|
-
match.failure :not_found do |result|
|
243
|
-
# result => [:not_found, ['not_found', 'Could not find thread with id='<params[:thread_id]>'']
|
244
|
-
end
|
245
|
-
|
246
|
-
# in case any of the ::authorize blocks returns false
|
247
|
-
match.failure :unauthorized do |result|
|
248
|
-
# result => [:unauthorized, ['unauthorized', 'User is not active']
|
249
|
-
end
|
250
|
-
|
251
|
-
# in case #create_post returns a Failure
|
252
|
-
match.failure :failed_to_save do |result|
|
253
|
-
# result => [:failed_to_save, ['failed_to_save', { user_id: 'some error' }]
|
254
|
-
end
|
255
|
-
end
|
256
|
-
end
|
257
|
-
end
|
258
|
-
```
|
178
|
+
To get a good basis to get started on `UseCases`, make sure to read [dry-transaction](https://dry-rb.org/gems/dry-transaction/0.13/)'s documentation first.
|
259
179
|
|
260
180
|
## Development
|
261
181
|
|
@@ -5,14 +5,8 @@ module UseCases
|
|
5
5
|
module Events
|
6
6
|
class PublishJob < ActiveJob::Base
|
7
7
|
def perform(publish_key, payload)
|
8
|
-
publish_key +=
|
9
|
-
UseCases.publisher.
|
10
|
-
end
|
11
|
-
|
12
|
-
private
|
13
|
-
|
14
|
-
def publisher
|
15
|
-
UseCases.publisher
|
8
|
+
publish_key += ".async"
|
9
|
+
UseCases.publisher.register_and_publish_event(publish_key, payload)
|
16
10
|
end
|
17
11
|
end
|
18
12
|
end
|
@@ -1,9 +1,50 @@
|
|
1
|
-
require
|
1
|
+
require "dry/events/publisher"
|
2
2
|
|
3
3
|
module UseCases
|
4
4
|
module Events
|
5
5
|
class Publisher
|
6
6
|
include Dry::Events::Publisher[:use_cases]
|
7
|
+
|
8
|
+
def self.register_and_publish_event(event_name, payload)
|
9
|
+
register_events(event_name)
|
10
|
+
|
11
|
+
new.tap do |publisher|
|
12
|
+
publisher.subscribe_to_event(event_name)
|
13
|
+
publisher.publish(event_name, payload)
|
14
|
+
end
|
15
|
+
|
16
|
+
return unless defined? UseCases::Events::PublishJob
|
17
|
+
|
18
|
+
UseCases::Events::PublishJob.perform_later(event_name, payload)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.extract_payload(use_case_args)
|
22
|
+
{
|
23
|
+
return_value: use_case_args[0],
|
24
|
+
params: use_case_args[1],
|
25
|
+
current_user: use_case_args[2]
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.register_events(event_name)
|
30
|
+
[event_name, "#{event_name}.aync"].each do |key|
|
31
|
+
register_event(key) unless events[key]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def subscribe_to_event(event_name)
|
36
|
+
subscribers_for(event_name).each(&method(:subscribe))
|
37
|
+
end
|
38
|
+
|
39
|
+
def subscribers_for(event_name)
|
40
|
+
available_subscribers.select do |subscriber|
|
41
|
+
subscriber.subscribed_to?(event_name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def available_subscribers
|
46
|
+
UseCases::Events::Subscriber.descendants.map(&:new) + UseCases.subscribers
|
47
|
+
end
|
7
48
|
end
|
8
49
|
end
|
9
50
|
end
|
@@ -1,6 +1,13 @@
|
|
1
|
+
require "active_support/core_ext/class/subclasses"
|
2
|
+
|
1
3
|
module UseCases
|
2
4
|
module Events
|
3
5
|
class Subscriber
|
6
|
+
def subscribed_to?(event_name)
|
7
|
+
event_handler_name = "on_#{event_name.gsub('.', '_')}"
|
8
|
+
|
9
|
+
respond_to?(event_handler_name)
|
10
|
+
end
|
4
11
|
end
|
5
12
|
end
|
6
13
|
end
|
@@ -9,35 +9,37 @@ module UseCases
|
|
9
9
|
module Publishing
|
10
10
|
def self.included(base)
|
11
11
|
super
|
12
|
-
|
12
|
+
StepAdapters::Abstract.prepend StepCallPatch
|
13
13
|
end
|
14
14
|
|
15
|
-
module
|
16
|
-
def
|
17
|
-
super(*args
|
15
|
+
module StepCallPatch
|
16
|
+
def call(*args)
|
17
|
+
super(*args).tap do |result|
|
18
|
+
publish_step_result(result, args)
|
19
|
+
end
|
18
20
|
end
|
19
21
|
|
20
|
-
def publish_step_result(step_result,
|
21
|
-
|
22
|
-
publish_key += step_result.success? ? ".success" : ".failure"
|
23
|
-
payload = step_result.value!
|
24
|
-
return unless publish_key
|
22
|
+
def publish_step_result(step_result, args)
|
23
|
+
return unless options[:publish]
|
25
24
|
|
26
|
-
|
27
|
-
|
25
|
+
key = extract_event_key(step_result)
|
26
|
+
payload = extract_payload(step_result, args)
|
27
|
+
|
28
|
+
UseCases.publisher.register_and_publish_event(key, payload)
|
28
29
|
end
|
29
30
|
|
30
|
-
|
31
|
-
return if UseCases.publisher.class.events[publish_key]
|
31
|
+
private
|
32
32
|
|
33
|
-
|
33
|
+
def extract_payload(step_result, args)
|
34
|
+
{
|
35
|
+
return_value: step_result.value!,
|
36
|
+
params: args[-2],
|
37
|
+
current_user: args[-1]
|
38
|
+
}
|
34
39
|
end
|
35
40
|
|
36
|
-
def
|
37
|
-
|
38
|
-
return unless defined? UseCases::Events::PublishJob
|
39
|
-
|
40
|
-
UseCases::Events::PublishJob.perform_later(publish_key, payload.to_json)
|
41
|
+
def extract_event_key(step_result)
|
42
|
+
options[:publish].to_s + (step_result.success? ? ".success" : ".failure")
|
41
43
|
end
|
42
44
|
end
|
43
45
|
end
|
data/lib/use_cases/version.rb
CHANGED
data/lib/use_cases.rb
CHANGED
@@ -15,24 +15,6 @@ module UseCases
|
|
15
15
|
extend Dry::Configurable
|
16
16
|
|
17
17
|
setting :container, reader: true
|
18
|
-
setting :publisher, ::UseCases::Events::Publisher
|
18
|
+
setting :publisher, ::UseCases::Events::Publisher, reader: true
|
19
19
|
setting :subscribers, [], reader: true
|
20
|
-
|
21
|
-
def self.configure(&blk)
|
22
|
-
super
|
23
|
-
finalize_subscriptions!
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.finalize_subscriptions!
|
27
|
-
subscribe(*subscribers)
|
28
|
-
return unless UseCases::Events::Subscriber.respond_to?(:descendants)
|
29
|
-
|
30
|
-
usecase_subscriber_children = UseCases::Events::Subscriber.descendants
|
31
|
-
subscribe(*usecase_subscriber_children)
|
32
|
-
subscribers.concat(usecase_subscriber_children)
|
33
|
-
end
|
34
|
-
|
35
|
-
def self.subscribe(*subscribers)
|
36
|
-
subscribers.each { |subscriber| publisher.subscribe(subscriber) }
|
37
|
-
end
|
38
20
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: use_cases
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.11
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ring Twice
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-01-
|
11
|
+
date: 2022-01-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|