use_cases 0.3.8 → 1.0.12

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad9722f48d9d6f073a9699cb042c246d935994b4384e340e3b985c35ea6a697b
4
- data.tar.gz: 8d3cc6daeef1b64fb86a96a9e6ff13a3b95ef328ff2a4e453696cae0724a1572
3
+ metadata.gz: e41f6c5b8670e047cafdd85d068a66aa0efac27700f2c179d5d893ee1593c466
4
+ data.tar.gz: 629cef02e18325528d6155a34356b80e48acaf31520ef45fddb4937e058c3ad2
5
5
  SHA512:
6
- metadata.gz: aefef04cfa79694d593848a6c245ad3d282416fdf28de6a934581f8fa15bfadf1faf5fccf1624a272b997473619af36841464feb8b1f26b6329ee8333a688e4b
7
- data.tar.gz: 00bb31d99dfd4e9f1d98d99778b1263e4d32949dc5270af41f78d19dba6a460afeba44797219984dcbe964c516c150e2c0ee1db1392645760de14a7599ae2876
6
+ metadata.gz: 2b5fdb9fd7bd52211b9194abd49fa2247b9b814e26f763f25f0c9f3ee995dff527977a19e7192576565a563af19dbf3cfeccae833fbd74f4fba74fa4589f46a3
7
+ data.tar.gz: 7d51d14db15b08764a1bbf3dc6ccede0806085e532e42273e436f1c91029f911282c3da31495b978a7dc87d5ba569fa33bc077b1b15673d7df795c9bf53ed781
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.1] - 2021-12-19
4
+
5
+ - Async published events are now suffixed by `".async"`
6
+
7
+ ## [1.0.0] - 2021-12-19
8
+
9
+ - Fixed some minor bugs that have been pending todos.
10
+ - Deprecated the `UseCases::Base` superclass as a DSL injection method. You must now use the `UseCase` module.
11
+ - Added the `publish` option for steps, which allows the publishing of an event when a step is completed.
12
+
3
13
  ## [0.1.0] - 2021-09-21
4
14
 
5
15
  - Added the basic DSL of them with the following modules:
@@ -9,7 +19,7 @@
9
19
  - Notifications [WIP]
10
20
  - StackRunner
11
21
  - Stack
12
- - StepResult
22
+ - UseCases::Result
13
23
  - Steps
14
24
  - Validate
15
25
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- use_cases (0.3.7)
4
+ use_cases (1.0.12)
5
5
  activesupport
6
6
  dry-events
7
7
  dry-matcher
@@ -126,4 +126,4 @@ DEPENDENCIES
126
126
  use_cases!
127
127
 
128
128
  BUNDLED WITH
129
- 2.2.25
129
+ 2.2.28
data/README.md CHANGED
@@ -4,250 +4,179 @@
4
4
 
5
5
  # UseCases
6
6
 
7
- ## Currently Unstable! Use at your own risk.
7
+ `UseCases` is a dry-ecosystem-based gem that implements a DSL for the use case pattern using the railway programming paradigm.
8
8
 
9
- `UseCases` is a gem based on the [dry-transaction](https://dry-rb.org/gems/dry-transaction/) DSL that implements macros commonly used internally by Ring Twice.
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
- `UseCases` does not however use `dry-transaction` 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 our needs.
11
+ ### Including UseCase
12
12
 
13
- ## Why `UseCases` came about:
13
+ Including the `UseCase` module ensures that your class implements the base use case [Base DSL](#the-base-dsl).
14
14
 
15
- 1. It allows you to use `dry-validation` without much gymastics.
16
- 2. It abstracts common steps like **authorization** and **validation** into macros.
17
- 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.
18
-
19
- ## Installation
15
+ ```ruby
16
+ class Users::Create
17
+ include UseCase
18
+ end
19
+ ````
20
20
 
21
- Add this line to your application's Gemfile:
21
+ In order to add optional modules (optins), use the following notation:
22
22
 
23
23
  ```ruby
24
- gem 'use_cases'
24
+ class Users::Create
25
+ include UseCase[:validated, :transactional, :publishing]
26
+ end
25
27
  ```
26
28
 
27
- And then execute:
28
-
29
- $ bundle install
30
-
31
- Or install it yourself as:
32
-
33
- $ gem install use_cases
34
-
35
- ## Usage
36
-
37
- To fully understand `UseCases`, make sure to read [dry-transaction](https://dry-rb.org/gems/dry-transaction/0.13/)'s documentation first.
38
-
39
- ### Validations
40
-
41
- See [dry-validation](https://dry-rb.org/gems/dry-validation/)
42
-
43
- ### Step Adapters
44
-
45
- https://dry-rb.org/gems/dry-transaction/0.13/step-adapters/
46
-
47
- **Basic Example**
29
+ ### Using a UseCase
48
30
 
49
31
  ```ruby
50
- class YourCase < UseCases::Base
51
- params {}
32
+ create_user = Users::Create.new
33
+ params = { first_name: 'Don', last_name: 'Quixote' }
52
34
 
53
- step :do_something
35
+ result = create_user.call(params, current_user)
54
36
 
55
- def do_something(params, current_user)
56
- params[:should_fail] ? Failure([:failed, "failed"]) : Success("it succeeds!")
57
- end
58
- end
37
+ # Checking if succeeded
38
+ result.success?
59
39
 
60
- params = { should_fail: true }
40
+ # Checking if failed
41
+ result.failure?
61
42
 
62
- YourCase.new.call(params, nil) do |match|
63
- match.failure :failed do |(code, result)|
64
- puts code
65
- end
43
+ # Getting return value
44
+ result.value!
45
+ ```
66
46
 
67
- match.success do |message|
68
- puts message
69
- end
70
- end
71
- # => failed
47
+ Or with using dry-matcher by passing a block:
72
48
 
73
- params = { should_fail: false }
49
+ ```ruby
50
+ create_user = Users::Create.new
51
+ params = { first_name: 'Don', last_name: 'Quixote' }
74
52
 
75
- YourCase.new.call(params, nil) do |match|
76
- match.failure :failed do |(code, result)|
77
- puts code
53
+ create_user.call(params, current_user) do |on|
54
+ on.success do |user|
55
+ puts "#{user.first_name} created!"
78
56
  end
79
57
 
80
- match.success do |message|
81
- puts message
58
+ on.failure do |(code, message)|
59
+ puts "Failure (#{code}): #{message}"
82
60
  end
83
61
  end
84
- # => it succeeds!
85
62
  ```
86
63
 
87
- **Complex Example**
88
-
89
- ```ruby
90
- class YourCase < UseCases::Base
91
- params {}
64
+ ### Available Optins
92
65
 
93
- try :load_some_resource
94
66
 
95
- step :change_this_resource
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. |
96
74
 
97
- tee :log_a_message
75
+ ### The base DSL
98
76
 
99
- check :user_can_create_another_resource?
77
+ Use cases implements a DSL similar to dry-transaction, using the [Railway programming paradigm](https://fsharpforfunandprofit.com/rop/).
100
78
 
101
- map :create_some_already_validated_resource
102
79
 
103
- enqueue :send_email_to_user
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.
104
81
 
105
- private
106
-
107
- def load_some_resource(_, params)
108
- Resource.find(params[:id])
109
- end
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.
110
83
 
111
- def change_this_resource(resource, params)
112
- resource.text = params[:new_text]
113
-
114
- if resource.text == params[:new_text]
115
- Success(resource)
116
- else
117
- Failure([:failed, "could not update resource"])
118
- end
119
- end
120
-
121
- def log_a_message(resource)
122
- Logger.info('Resource updated')
123
- end
124
84
 
125
- def user_can_create_another_resource?(_, _, user)
126
- user.can_create?(Resource)
127
- end
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` | ❌ |
128
92
 
129
- def create_some_already_validated_resource(resource, params, user)
130
- new_resource = Resource.create(text: params[:text])
131
- end
93
+ #### Optional steps
132
94
 
133
- def send_email_to_user(new_resource, _, user)
134
- ResourcEMailer.notify_user(user, new_resource).deliver!
135
- end
136
- end
137
- ```
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` | ❌ |
138
100
 
139
- ### Authorization
140
101
 
141
- `authorize` is a `check` step that returns an `:unauthorized` code in it's `Failure`.
102
+ ### Defining Steps
142
103
 
143
- **Example**
104
+ Defining a step can be done in the body of the use case.
144
105
 
145
106
  ```ruby
146
- class YourCase < UseCases::Base
147
- authorize 'User must be over 18' do |user|
148
- user.age >= 18
149
- end
150
- end
151
-
152
- user = User.where('age = 15').first
107
+ class Users::DeleteAccount
108
+ include UseCases[:validated, :transactional, :publishing, :validated]
153
109
 
154
- YourCase.new.call({}, user) do |match|
155
- match.failure :unauthorized do |(code, result)|
156
- puts code
157
- end
158
- end
159
- # => User must be over 18
110
+ step :do_something, {}
160
111
  ```
161
112
 
162
- ### Example
163
-
164
- For the case of creating posts within a thread.
165
-
166
- **Specs**
167
-
168
- - Only active users or the thread owner can post.
169
- - The post must be between 25 and 150 characters.
170
- - The post must be sanitized to remove any sensitive or explicit content.
171
- - The post must be saved to the database in the end.
172
- - In case any conditions are not met, an failure should be returned with it's own error code.
113
+ In real life, a simple use case would look something like:
173
114
 
174
115
  ```ruby
175
- class Posts::Create < UseCases::Base
116
+ class Users::DeleteAccount
117
+ include UseCases[:validated, :transactional, :publishing, :validated]
176
118
 
177
119
  params do
178
- required(:body).filled(:string).value(size?: 25..150)
179
- required(:thread_id).filled(:integer)
120
+ required(:id).filled(:str?)
180
121
  end
181
122
 
182
- try :load_post_thread, failure: :not_found
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
183
127
 
184
- authorize 'User is not active' do |user, params, thread|
185
- user.active?
186
- end
128
+ private
187
129
 
188
- authorize 'User must be active or the thread author.' do |user, params, thread|
189
- user.active? || thread.author == user
130
+ def user_owns_account?(_previous_step_input, params, current_user)
131
+ current_user.account_id == params[:id]
190
132
  end
191
133
 
192
- step :sanitize_body
193
-
194
- step :create_post
195
-
196
- private
197
-
198
- def load_post_thread(params, user)
199
- Thread.find(params[:thread_id])
134
+ def load_account(_previous_step_input, params, _current_user)
135
+ Account.find_by!(user_id: params[:id])
200
136
  end
201
137
 
202
- def sanitize_body(params)
203
- SanitizeText.new.call(params[:body]).to_monad
138
+ def delete_account(account, _params, _current_user)
139
+ account.destroy!
204
140
  end
205
141
 
206
- def create_post(body, params, user)
207
- post = Post.new(body: body, user_id: user.id, thread_id: params[:thread_id])
208
-
209
- post.save ? Success(post) : Failure([:failed_to_save, post.errors.details])
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!
210
146
  end
211
147
  end
212
148
  ```
213
149
 
214
- And in your controller action
150
+ #### Available Options
151
+
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>
159
+
160
+ ## Installation
161
+
162
+ Add this line to your application's Gemfile:
215
163
 
216
164
  ```ruby
217
- # app/controllers/posts_controller.rb
218
- class PostsController < ApplicationController
219
- def create
220
- Posts::Create.new.call(params, current_user) do |match|
221
-
222
- # in success, the return value is the Success payload of the last step (#create_post)
223
- match.success do |post|
224
- # result => <Post:>
225
- end
226
-
227
- # in case ::params or any other dry-validation fails.
228
- match.failure :validation_error do |result|
229
- # result => [:validation_error, ['validation_error', { thread_id: 'is missing' }]
230
- end
231
-
232
- # in case ::try raises an error (ActiveRecord::NotFound in this case)
233
- match.failure :not_found do |result|
234
- # result => [:not_found, ['not_found', 'Could not find thread with id='<params[:thread_id]>'']
235
- end
236
-
237
- # in case any of the ::authorize blocks returns false
238
- match.failure :unauthorized do |result|
239
- # result => [:unauthorized, ['unauthorized', 'User is not active']
240
- end
241
-
242
- # in case #create_post returns a Failure
243
- match.failure :failed_to_save do |result|
244
- # result => [:failed_to_save, ['failed_to_save', { user_id: 'some error' }]
245
- end
246
- end
247
- end
248
- end
165
+ gem 'use_cases'
249
166
  ```
250
167
 
168
+ And then execute:
169
+
170
+ $ bundle install
171
+
172
+ Or install it yourself as:
173
+
174
+ $ gem install use_cases
175
+
176
+ ## Usage
177
+
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.
179
+
251
180
  ## Development
252
181
 
253
182
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/lib/use_case.rb CHANGED
@@ -6,20 +6,16 @@ require "dry/monads/do"
6
6
  require "dry/monads/do/all"
7
7
  require "dry/matcher/result_matcher"
8
8
 
9
- require "use_cases/authorize"
10
9
  require "use_cases/dsl"
11
- require "use_cases/errors"
12
- require "use_cases/validate"
13
10
  require "use_cases/stack"
14
11
  require "use_cases/params"
15
12
  require "use_cases/stack_runner"
16
- require "use_cases/step_result"
17
- require "use_cases/notifications"
18
- require "use_cases/prepare"
13
+ require "use_cases/result"
19
14
  require "use_cases/step_adapters"
20
15
  require "use_cases/module_optins"
21
16
 
22
17
  module UseCase
18
+ extend UseCases::DSL
23
19
  extend UseCases::ModuleOptins
24
20
 
25
21
  def self.included(base)
@@ -33,7 +29,6 @@ module UseCase
33
29
  extend UseCases::ModuleOptins
34
30
 
35
31
  include UseCases::StepAdapters
36
- include UseCases::Notifications
37
32
  end
38
33
  end
39
34
 
@@ -41,7 +36,6 @@ module UseCase
41
36
 
42
37
  def initialize(*)
43
38
  @stack = UseCases::Stack.new(self.class.__steps__).bind(self)
44
- # self.class.bind_step_subscriptions
45
39
  end
46
40
 
47
41
  def call(params, current_user = nil)
data/lib/use_cases/dsl.rb CHANGED
@@ -17,19 +17,5 @@ module UseCases
17
17
  def __steps__
18
18
  @__steps__ ||= []
19
19
  end
20
-
21
- def subscribe(listeners)
22
- @listeners = listeners
23
-
24
- if listeners.is_a?(Hash)
25
- listeners.each do |step_name, listener|
26
- __steps__.detect { |step| step.name == step_name }.subscribe(listener)
27
- end
28
- else
29
- __steps__.each do |step|
30
- step.subscribe(listeners)
31
- end
32
- end
33
- end
34
20
  end
35
21
  end
@@ -0,0 +1,13 @@
1
+
2
+ return unless defined? ActiveJob
3
+
4
+ module UseCases
5
+ module Events
6
+ class PublishJob < ActiveJob::Base
7
+ def perform(publish_key, payload)
8
+ publish_key += ".async"
9
+ UseCases.publisher.register_and_publish_event(publish_key, payload)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,50 @@
1
+ require "dry/events/publisher"
2
+
3
+ module UseCases
4
+ module Events
5
+ class Publisher
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
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,13 @@
1
+ require "active_support/core_ext/class/subclasses"
2
+
3
+ module UseCases
4
+ module Events
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
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_cases/step_adapters/check"
4
+
5
+ module UseCases
6
+ module ModuleOptins
7
+ module Authorized
8
+ def self.included(base)
9
+ super
10
+ base.class_eval do
11
+ extend DSL
12
+ end
13
+ end
14
+
15
+ module DSL
16
+ DEFAULT_OPTIONS = {
17
+ failure: :unauthorized,
18
+ failure_message: "Not Authorized",
19
+ merge_input_as: :resource
20
+ }
21
+
22
+ def authorize(name, options = {})
23
+ options = DEFAULT_OPTIONS.merge(options)
24
+
25
+ check name, options
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_cases/step_adapters/tee"
4
+
5
+ module UseCases
6
+ module ModuleOptins
7
+ module Prepared
8
+ def self.included(base)
9
+ super
10
+ base.class_eval do
11
+ extend DSL
12
+ end
13
+ end
14
+
15
+ module DSL
16
+ def prepare(name, options = {})
17
+ __steps__.unshift StepAdapters::Tee.new(name, nil, options)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_cases/events/publisher"
4
+ require "use_cases/events/subscriber"
5
+ require "use_cases/events/publish_job"
6
+
7
+ module UseCases
8
+ module ModuleOptins
9
+ module Publishing
10
+ def self.included(base)
11
+ super
12
+ StepAdapters::Abstract.prepend StepCallPatch
13
+ end
14
+
15
+ module StepCallPatch
16
+ def call(*args)
17
+ super(*args).tap do |result|
18
+ publish_step_result(result, args)
19
+ end
20
+ end
21
+
22
+ def publish_step_result(step_result, args)
23
+ return unless options[:publish]
24
+
25
+ key = extract_event_key(step_result)
26
+ payload = extract_payload(step_result, args)
27
+
28
+ UseCases.publisher.register_and_publish_event(key, payload)
29
+ end
30
+
31
+ private
32
+
33
+ def extract_payload(step_result, args)
34
+ {
35
+ return_value: step_result.value!,
36
+ params: args[-2],
37
+ current_user: args[-1]
38
+ }
39
+ end
40
+
41
+ def extract_event_key(step_result)
42
+ options[:publish].to_s + (step_result.success? ? ".success" : ".failure")
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end