outbacker 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4e5ffe637e1cb7ee527b55157fdadae9fd86a4d1
4
+ data.tar.gz: 2da7571e590a4b764946c6e0049e3665e6000df6
5
+ SHA512:
6
+ metadata.gz: 7fad92a39354277e9a3424943750c57b488ce2fdcab753e46affc2f007bfcd9ba1107a346cd38e022bb7097e6f87d0790883de034b369f639da34db54f7a0666
7
+ data.tar.gz: c5e054b18cc3231eafab40b7e01238f8bb376499a550881a7e2085f04e76e8f79746f5981ba4db620de9562c86678f10fa05372c1030ce6d779c15a6ea78edeb
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ cache: bundler
3
+
4
+ rvm:
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - 2.1
8
+ - 2.2.2
9
+ - ruby-head
10
+ - jruby
11
+ - rbx-2
12
+
13
+ script: 'bundle exec rake'
@@ -0,0 +1,9 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+ This project adheres to [Semantic Versioning](http://semver.org/).
4
+
5
+ ## 0.0.2 2015-07-27
6
+ Prepping for initial release.
7
+
8
+ ## 0.0.1 2015-07-26
9
+ This is a pre-release version.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in outbacker.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Anthony Garcia
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,348 @@
1
+ # Outbacker
2
+
3
+ [![Build Status](https://travis-ci.org/polypressure/outbacker.svg?branch=master)](https://travis-ci.org/polypressure/outbacker)
4
+ [![Code Climate](https://codeclimate.com/github/polypressure/outbacker/badges/gpa.svg)](https://codeclimate.com/github/polypressure/outbacker)
5
+ [![Test Coverage](https://codeclimate.com/github/polypressure/outbacker/badges/coverage.svg)](https://codeclimate.com/github/polypressure/outbacker/coverage)
6
+
7
+ Rails developers have long known how important it is to keep controllers "skinny" and free of business logic. Our controllers are supposed to be dumb dispatchers that take results from the model layer and turn them into redirects, flash messages, form re-renderings, session state updates, JSON responses, HTTP status codes, and so on.
8
+
9
+ But far too often, the conditional logic in typical Rails controllers to act on results from models and decide what to do next attracts business logic and spirals out of control. Complicated logic sneaks into our controllers as we add code to handle new features, stories, and special cases. And the cultural and process controls we put in place to enforce good code hygiene chronically break down in the face of schedule pressure, growing teams, emergency fixes, etc.
10
+
11
+ **Outbacker** ("outcome callbacks") is a very simple micro library that makes it easy to keep controllers free of this conditional logic. Controllers become simple, declarative mappings of business logic results to the redirects, flash messages, session state updates, HTTP status codes, and other actions that deliver results to the user.
12
+
13
+ It turns out that not only is Outbacker a prophylaxis against fat, complicated controllers, it more generally supports a very simple, low-ceremony way to write intention-revealing Rails code with both skinny controllers _and_ skinny models. If you feel these are worthwhile aims for your Ruby/Rails code—but you've found many approaches to accomplish this ineffective or not worth the trouble—then you might find Outbacker valuable.
14
+
15
+ **Note:** The README that follows has a lot of motivation, rationale, and explanation—maybe excessively so for such a simple library. If you're impatient, you can go straight to some [code examples](https://github.com/polypressure/outbacker/tree/master/examples). Hopefully, these examples are sufficient for you to get an understanding of what Outbacker provides, and how to use it. If not, you can always come back to this readme.
16
+
17
+ ## A Typical Rails Controller
18
+
19
+ Let's look at a typical simple Rails controller method:
20
+
21
+ ```ruby
22
+ class AppointmentsController < ApplicationController
23
+
24
+ def create
25
+ @appointment = Appointment.new(appointment_params)
26
+ if @appointment.save
27
+ redirect_to appointments_path,
28
+ notice: "Your appointment has been booked."
29
+ else
30
+ render :new
31
+ end
32
+ rescue InsufficientCredits => e
33
+ redirect_to new_credits_path,
34
+ alert: "You don't have enough credits, please purchase more."
35
+ end
36
+
37
+ ...
38
+
39
+ end
40
+ ```
41
+
42
+ For the most part, this is idiomatic Rails controller code, and it's free of business logic. We can guess that this method is trying to book an appointment of some sort, with a prerequisite that the user has a minimum account balance denominated in something called "credits." But we're having to make assumptions about the intent of the code, because it's trying to express business logic with the limited, non-intention-revealing vocabulary of low-level ActiveRecord CRUD verbs.
43
+
44
+ ActiveRecord's constrained interface also forces us to fit our outcomes into one of only two values: true or false, representing a successful save or a validation error respectively. Unfortunately, an outcome where the user lacks sufficient credits can't be naturally expressed as a validation error here. That's because we don't want the controller to merely re-render the form—as we do with typical validation errors. We want to redirect the user to some other page where they can purchase additional credits.
45
+
46
+ Consequently, this code is resorting to the use of an exception to indicate that the user doesn't have enough credits to book an appointment. But exceptions should be reserved for unexpected or abnormal conditions that the code isn't prepared to handle.
47
+
48
+ Alternatively, we could set some sort of flag or status code on the Appointment model. But checking return values and status codes results in some ugly conditional code in our controller. And as we've said, too often this conditional code spirals out of control, is a magnet for business logic, and becomes increasingly brittle over time.
49
+
50
+ ## Improving Rails Controllers with Outbacker
51
+
52
+ Here's how the same controller looks with Outbacker:
53
+
54
+ ```ruby
55
+ class AppointmentsController < ApplicationController
56
+
57
+ def create
58
+ calendar.book_appointment(appointment_params) do |on_outcome|
59
+
60
+ on_outcome.of(:successful_booking) do |appointment|
61
+ redirect_to appointments_path,
62
+ notice: 'Your appointment has been booked.'
63
+ end
64
+
65
+ on_outcome.of(:insufficient_credits) do
66
+ redirect_to new_credits_path,
67
+ alert: "You don't have enough credits, please purchase more."
68
+ end
69
+
70
+ on_outcome.of(:failed_validation) do |appointment|
71
+ @appointment = appointment
72
+ render :new
73
+ end
74
+
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def calendar
81
+ @calendar ||= AppointmentCalendar.for_the current_user
82
+ end
83
+
84
+ ...
85
+
86
+ end
87
+ ```
88
+
89
+ Hopefully, the above example is mostly self-explanatory, but here are a few notes:
90
+
91
+ First, we've replaced the ActiveRecord `save` method with a `book_appointment` method defined on a separate plain-old Ruby `AppointmentCalendar` object (which we'll discuss in more detail shortly). This reifies the business task of booking an appointment, giving us a method that unambiguously conveys intent.
92
+
93
+ This also allows us to replace the conditional logic that's required in Rails controllers to act on results. Instead, we now declaratively specify the actions we'd like to execute for each possible outcome from invoking `AppointmentCalendar#book_appointment`. This is done with a DSL-ish block passed to our business logic method (`book_appointment`), which provides short callback blocks for each of the possible outcomes when trying to book an appointment:
94
+
95
+ * :successful_booking
96
+ * :insufficient_credits
97
+ * :failed_validation
98
+
99
+ The `on_outcome` object in the above controller is an instance of an internal class used by Outbacker (`Outbacker::OutcomeHandlerSet`). We've named it "on_outcome" strictly for the sake of readability, and to indulge the DSL-ish syntax. For the most part, you really don't need to worry about the details of this object. You simply invoke the `of` method on it to define an outcome callback block, providing a key corresponding to the specific outcome this block handles—in this case, `successful_booking`. As we'll see shortly, this key matches a corresponding key used in our business logic method, `AppointmentCalendar#book_appointment`.
100
+
101
+ (FYI, these "outcome callbacks" are the namesake for this library, "Outbacker." Yeah, I know, pretty weak and uninspired. But you know how they say naming is hard.)
102
+
103
+ An outcome callback block can take any number of arguments, passed on from the business-logic method. Here, a single Appointment object representing the appointment that has been booked is passed to the outcome callback block for `:successful_booking`. And a single Appointment object with validation errors is passed to the outcome callback block for `:failed_validation`.
104
+
105
+ You can see that our controller method can now easily accommodate any number of possible outcomes from our business logic, without having to pile on more conditional checks and clauses, exception rescue blocks, etc. There's no reason for our controller to be anything but skinny.
106
+
107
+ Another benefit is that all the cases and outcomes that we need to consider from your business logic method are explicitly enumerated—as opposed to having to be inferred from the various conditional paths in a typical Rails controller method. This makes it easier to come in and quickly understand the intent of the code, makes for easier testing, etc.
108
+
109
+ ### Alternate, method-based syntax
110
+
111
+ Outbacker provides an alternative syntax here that uses dynamic method names against the yielded object, rather than passing a symbol to the `of` method:
112
+
113
+ ```ruby
114
+ class AppointmentsController < ApplicationController
115
+
116
+ def create
117
+
118
+ calendar.book_appointment(appointment_params) do |on|
119
+
120
+ on.outcome_of_successful_booking do |appointment|
121
+ redirect_to appointments_path,
122
+ notice: 'Your appointment has been booked.'
123
+ end
124
+
125
+ on.outcome_of_insufficient_credits do
126
+ redirect_to new_credits_path,
127
+ alert: "You don't have enough credits, please purchase more."
128
+ end
129
+
130
+ on.outcome_of_failed_validation do |appointment|
131
+ @appointment = appointment
132
+ render :new
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+
139
+
140
+ private
141
+
142
+ def calendar
143
+ @calendar ||= AppointmentCalendar.for_the current_user
144
+ end
145
+
146
+ ...
147
+
148
+ end
149
+ ```
150
+
151
+ Note that your method names here must begin with the "outcome_of" prefix. The outcome key is extracted from the method name by stripping that prefix. The `on_outcome` object has been renamed to simply `on`, again for the sake of readability.
152
+
153
+ For whatever reasons, you might prefer this syntax. But note that the implementation of this syntax depends on `method_missing`—for which the usual caveats apply.
154
+
155
+
156
+ ## Business Logic Objects with Outbacker
157
+
158
+ Now, let's take a look at the corresponding business logic object that uses Outbacker:
159
+
160
+ ```ruby
161
+ class AppointmentCalendar
162
+
163
+ # Needed to make this an "outbacked" object.
164
+ include Outbacker
165
+
166
+ #
167
+ # An "outbacked" domain method, i.e., one that can
168
+ # process outcome callbacks passed into it—here via
169
+ # the &outcome_handlers parameter:
170
+ #
171
+ def book_appointment(params, &outcome_handlers)
172
+ with(outcome_handlers) do |outcomes|
173
+ if user_lacks_sufficient_credits?
174
+ outcomes.handle :insufficient_credits
175
+ return
176
+ end
177
+
178
+ appointment = Appointment.new(params)
179
+ if appointment.save
180
+ ledger.deduct_credits_for appointment
181
+
182
+ notify_user_about appointment
183
+ notify_office_about appointment
184
+
185
+ outcomes.handle :successful_booking, appointment
186
+ else
187
+ outcomes.handle :failed_validation, appointment
188
+ end
189
+ end
190
+ end
191
+
192
+ ...
193
+
194
+ private
195
+
196
+ def user_lacks_sufficient_credits?
197
+ # Check current user's credit balance is >= cost of appointment.
198
+ end
199
+
200
+ def ledger
201
+ # Return Ledger object that manages credit balances and transactions.
202
+ end
203
+
204
+ def notify_user_about(appointment)
205
+ # Enqueue background jobs to send emails, SMS, phone push notifications, etc.
206
+ end
207
+
208
+ def notify_office_about(appointment)
209
+ # Post office dashboard notification and activity feed entry, enqueue
210
+ # background jobs to send emails, SMS, phone push notifications, etc.
211
+ end
212
+
213
+ ...
214
+
215
+ end
216
+ ```
217
+
218
+ ### Including the Outbacker module
219
+
220
+ The first thing to point out here: our business-logic object is a PORO, i.e., a plain-old Ruby object. It can pretty much be whatever type of PORO you want: a domain object, a use case object, a DCI context, a service object—whatever.
221
+
222
+ Next, to enable Outbacker support in your business object, you have to `include` the `Outbacker` module in your class. For the most part, you can include Outbacker in any class, but to discourage you from putting business logic in your ActiveRecord models, by default Outbacker will actually raise an exception if you try to include it within an ActiveRecord (or ActiveController) subclass.
223
+
224
+ This is Outbacker's simple tactic to help keep our models skinny. It encourages us to move the bulk of our business logic into POROs that are easy to test in isolation. And our models can then be focused on persistence and simple validation rules—free of ailments like ActiveRecord callback spaghetti, brazen violations of the Single Responsibility Principle, etc.
225
+
226
+ #### Excluding/allowing other types of business objects
227
+
228
+ You can actually configure the policy regarding where Outbacker can be included. First, you can customize the blacklisted superclasses. Create a `config/initializers/outbacker.rb` file like this:
229
+
230
+ ```ruby
231
+ Outbacker.configure do |c|
232
+ c.blacklist = [ActiveRecord::Base, ActionController::Base, MyBlacklistedClass]
233
+ end
234
+ ```
235
+
236
+ This says that you cannot include Outbacker in any subclass of `ActiveRecord`, `ActionController`, or `MyBlacklistedClass`. If anybody on your team tries to include Outbacker within a subclass of any of these classes, an exception will be raised.
237
+
238
+ Alternatively, you can specify a whitelist:
239
+
240
+ ```ruby
241
+ Outbacker.configure do |c|
242
+ c.whitelist = [UseCase, ServiceObject, DomainObject]
243
+ end
244
+ ```
245
+
246
+ This says that you can only include Outbacker within subclasses of `UseCase`, `ServiceObject`, or `DomainObject`. If anybody on your team tries to include Outbacker within a subclass of any other class, an exception will be raised. This is the recommended way for configuring your policy.
247
+
248
+ ### Defining your "Outbacked" business logic method
249
+
250
+ Your business-logic method that uses Outbacker (or more conveniently, an "Outbacked" method) can of course take any number of arguments, as long as its last argument is a block—which as we've seen is where the outcome callbacks are provided. By convention, we name the argument for this block `outcome_handlers`. We immediately pass it to the `Outbacker::with(outcome_handlers)` method, which must wrap the entire body of your Outbacked method:
251
+
252
+ ```ruby
253
+ def book_appointment(params, &outcome_handlers)
254
+ with(outcome_handlers) do |outcomes|
255
+ # Business logic here.
256
+ end
257
+ end
258
+ ```
259
+
260
+ Within our business logic methods, when we know what the outcome is, we trigger the corresponding outcome callback as follows:
261
+
262
+ ```ruby
263
+ outcomes.handle :successful_booking, appointment
264
+ ```
265
+
266
+ In short, this says to process the outcome of :successful_booking with the corresponding handler callback passed in via the outcome_handlers block, and passing that callback the `appointment` object. Again as a side benefit, this makes the intent of the code explicit: we can unambiguously see that our code has determined the outcome of the method at this point, and what exactly that outcome is.
267
+
268
+ Of course, with any non-trivial business logic, you will have multiple calls to `outcome.handle` for your different outcomes. You might also have multiple paths to get to a specific outcome, or even trigger an outcome within a rescue clause. Outbacker has some protections to help ensure that your controller handles all your outcomes once (and only once), and that your business logic method triggers at least one (and only one) outcome:
269
+
270
+ * When you trigger an outcome, if you've already handled that outcome, (i.e., if your controller has provided multiple outcome callbacks for the triggered outcome), Outbacker raises an exception.
271
+ * When you trigger an outcome, if your controller hasn't provided a callback for that outcome, Outbacker raises an exception.
272
+ * If by the conclusion of your Outbacked method (i.e, when the `with(outcome_handlers)` method has finished executing your business logic block), if no outcome at all was triggered, Outbacker raises an exception.
273
+ * If your Outbacked method tries to trigger an outcome after one has already been triggered, Outbacker raises an exception.
274
+
275
+ ### Return values
276
+
277
+ The return value of an Outbacked method is the same as any Ruby method (i.e., the value of the last evaluated expression). However, we typically don't care about return values when using Outbacker. In a sense, the result/return values are the outcome, as well as any values passed as arguments to the outcome block.
278
+
279
+ However, sometimes you want to invoke your business-logic methods without having to provide a block of outcome callbacks. For example, when invoking these methods within a Rails console/REPL for debugging or support purposes, it can be inconvenient to have to provide callback blocks. You just want to execute the method, and don't need to act on the outcome—you only need to know the result.
280
+
281
+ If you don't provide a callback block to an Outbacked method, Outbacker simply returns the outcome key and any arguments, as passed to the `handle` method. So for the following call to the `handle` method:
282
+
283
+ ```ruby
284
+ outcomes.handle :successful_booking, appointment
285
+ ```
286
+
287
+ When you invoke `AppointmentCalendar#book_appointment(params)` with no outcome callback block, it would simply return `[:successful_booking, appointment]`.
288
+
289
+
290
+ ## Testing
291
+
292
+ When testing controllers (esp. in isolation as opposed to within integrated tests), you typically need to mock or stub your business logic methods in order to return a canned value. However, when using Outbacker, you don't want to simply stub a return value—you need to be able specify that a specific outcome is triggered, so that the corresponding outcome callback provided by your controller is executed. Because the standard mocking libraries can't help you with this, Outbacker provides its own testing support class, `OutbackerStub` to let you "stub" outcomes:
293
+
294
+ ```ruby
295
+
296
+ # Add this to your test_helper.rb
297
+ require 'test_support/outbacker_stub'
298
+
299
+ ...
300
+
301
+ test "user is redirected to the credits purchase page when they lack sufficient credits" do
302
+ calendar_stub = Outbacker::OutbackerStub.new
303
+ calendar_stub.stub('book_appointment', :insufficient_credits, stubbed_appointment)
304
+
305
+ # This is a method we added to our controller to inject dependencies:
306
+ @controller.inject_calendar(calendar_stub)
307
+
308
+ post :create, appointment: valid_appointment_params
309
+
310
+ assert_redirected_to new_credits_url
311
+ end
312
+
313
+ ...
314
+
315
+ ```
316
+
317
+ Here we've stubbed the `AppointmentCalendar#book_appointment` method, specifying that we want it to trigger the `insufficient_credits` outcome, passing along the `stubbed_appointment object` (created by whatever test double tools you're already using) to our outcome block. You can of course specify any number of objects to be passed by the stubbed method to the outcome block.
318
+
319
+ Note that this only provides stubbing functionality, with no support for mocking and setting/verifying expectations that methods are invoked by your object under your test. In practice, I haven't found this to be a problem, because these days I find this level of mocking often results in brittle, expensive-to-maintain tests. If you disagree, or have a valid need for mocks, then you can always use an existing mocking library—and write tests to set/verify expectations, distinct from your tests that depend on stubbing. You probably should be doing this anyway if you adhere to the practice of a single assertion per test.
320
+
321
+
322
+ ## Installation
323
+
324
+ Add this line to your application's Gemfile:
325
+
326
+ ```ruby
327
+ gem 'outbacker'
328
+ ```
329
+
330
+ And then execute:
331
+
332
+ $ bundle
333
+
334
+ Or install it yourself as:
335
+
336
+ $ gem install outbacker
337
+
338
+ ## Usage
339
+
340
+ Write business logic objects, controllers, and tests as described above.
341
+
342
+ ## Contributing
343
+
344
+ 1. Fork it ( https://github.com/polypressure/outbacker/fork )
345
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
346
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
347
+ 4. Push to the branch (`git push origin my-new-feature`)
348
+ 5. Create a new Pull Request
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ t.pattern = "test/*_test.rb"
7
+ end
8
+
9
+ task default: :test
@@ -0,0 +1,77 @@
1
+ #
2
+ # Start here, then look at:
3
+ # * examples/app/domain/appointment_calendar.rb
4
+ # * examples/app/config/outbacker.rb
5
+ # * examples/test/controllers/appointments_controller_test.rb
6
+ # * examples/test/test_helper.rb
7
+ #
8
+ class AppointmentsController < ApplicationController
9
+
10
+ #
11
+ # A conventional controller method:
12
+ #
13
+ def create
14
+ @appointment = Appointment.new(appointment_params)
15
+ if @appointment.save
16
+ redirect_to appointments_path,
17
+ notice: "Your appointment has been booked."
18
+ else
19
+ render :new
20
+ end
21
+ rescue InsufficientCredits => e
22
+ redirect_to new_credits_path,
23
+ alert: "You don't have enough credits, please purchase more."
24
+ end
25
+
26
+ #
27
+ # The same controller method with Outbacker:
28
+ #
29
+ def create
30
+ #
31
+ # We've replaced the call to Appointment#save with a method defined
32
+ # on a separate plain-old Ruby object (app/domain/appointment_calendar.rb)
33
+ # that reifies the business concept of booking an appointment.
34
+ #
35
+ # We pass a block to this method which declaratiely specifies
36
+ # how we want to to respond to each of the possible outcomes
37
+ # from the book_appointment method.
38
+ #
39
+ calendar.book_appointment(appointment_params) do |on_outcome|
40
+
41
+ on_outcome.of(:successful_booking) do |appointment|
42
+ redirect_to appointments_path,
43
+ notice: 'Your appointment has been booked.'
44
+ end
45
+
46
+ on_outcome.of(:insufficient_credits)
47
+ redirect_to new_credits_path,
48
+ alert: "You don't have enough credits, please purchase more."
49
+ end
50
+
51
+ on_outcome.of(:failed_validation) do |appointment|
52
+ @appointment = appointment
53
+ render :new
54
+ end
55
+
56
+ end
57
+ end
58
+
59
+ #
60
+ # This lets us inject a stubbed calendar to support testing:
61
+ #
62
+ def inject_calendar(appointment_calendar)
63
+ @calendar = appointment_calendar
64
+ end
65
+
66
+ private
67
+
68
+ #
69
+ # Instantiate your business-logic object as suits your project:
70
+ #
71
+ def calendar
72
+ @calendar ||= AppointmentCalendar.for_the current_user
73
+ end
74
+
75
+
76
+
77
+ end