use_cases 1.0.6 → 1.0.11
Sign up to get free protection for your applications and to get access to all the features.
- 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
|