servizio 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -383
- data/lib/servizio/service.rb +2 -0
- data/lib/servizio/version.rb +1 -1
- data/spec/servizio/service_spec.rb +9 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 813806f66607dba01c96b178f0ccde49ff28e845
|
4
|
+
data.tar.gz: 30fba7afa7b7d808ceb021b84e490215b698ad4a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3cf60f0f2d1fa2fb171ba95c40a1776e8eb4413d760e50895cfc9edf254009682c519950f43a43c799b981a2232af9aa555dc15cbe1859a77c660327605f177a
|
7
|
+
data.tar.gz: e77581b8dc72c5d940b21221972192e799c84bae43c996b4cbf44bf712b453e04ca997b8d5d39c97aef50f432b5d90dcbfba403293f7ca91c10b5d157f75fdb9
|
data/README.md
CHANGED
@@ -8,399 +8,27 @@ Servizio is a gem to support you creating service objects. It was created after
|
|
8
8
|
|
9
9
|
I liked the ideas presented there, so I began to use them. Quickly I realised, that combining the basic concepts presented in this post with something like ```ActiveModel``` would be awesome. So there was ```Servizio```.
|
10
10
|
|
11
|
-
##
|
11
|
+
## TL;DR
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
A service object *does one thing*. It should hold the business logic to perform one action, e.g. to change a users password. It should start with a verb (but your mileage may vary). When used with rails, the should be homed in ```app/services```.
|
16
|
-
|
17
|
-
In order to keep things organzied, subdirectories/modules should be used, e.g. ```app/services/user/change_password``` which corresponds to ```User::ChangePassword```. Generally subdirectories/modules holding services should be named with the singular noun, representing the object they manipulate, e.g. ```app/services/user/...``` not ```users```, resulting in ```User::ChangePassword``` not ```Users::...``` That way, things are consistent with the rails naming convention regarding models.
|
18
|
-
|
19
|
-
That does not there has to be a corresponding model if you create a service subdirectory. It's only a convention. So you are free to create something like ```app/services/statistic/create.rb```, allthough there is no ```Statistic``` model. It should be all about business logic. If there is a corresponding model, fine. If not, never mind.
|
20
|
-
|
21
|
-
A service object should respond to the ```call``` method. It's the way lambdas and procs are called, so its obvious to use it as a convention. At this point we have something like this
|
22
|
-
|
23
|
-
```ruby
|
24
|
-
# app/services/user/change_password
|
25
|
-
class User::ChangePassword
|
26
|
-
def call(user, old_password, new_password)
|
27
|
-
# check if the old_password is valid
|
28
|
-
# set the users password to new_password
|
29
|
-
...
|
30
|
-
end
|
31
|
-
end
|
32
|
-
```
|
33
|
-
|
34
|
-
### Why is this cool?!
|
35
|
-
|
36
|
-
#### Clean-up models and controllers
|
37
|
-
|
38
|
-
In your everyday's rails app, business logic is often cluttered over controllers, models and so on. In order to understand, what it means to *change a users password* you have to walk through a couple of files and pick the relevant parts.
|
39
|
-
|
40
|
-
Service objects concentrate this logic, so you can have a look at ```app/services/user/change_password``` and you know whats going on. On the other side, this means that your controllers/models get much simpler. They can do, what they should do. Models should only be concerned about associations, scopes, persistence etc. Controllers should handle incoming requests and pass them down to appropriate services.
|
41
|
-
|
42
|
-
#### Quick overview of what an app can do
|
43
|
-
|
44
|
-
So you want to know, what your app does/is able to do? Just look into ```app/services```. There you can see very quickly, which uses cases (services) are present and can be used.
|
45
|
-
|
46
|
-
#### Splitting up complex business logic is easy
|
47
|
-
|
48
|
-
It's easy for an service object to call another one. That way you can split up complex uses cases into simpler ones. Let's get back to our ```User::ChangePassword```. This could be involve two actions
|
49
|
-
* verify, that the *current_password* is correct
|
50
|
-
* set the users password to *new_password* if the former check passes
|
51
|
-
|
52
|
-
So there could be two services ```User::Authenticate``` and ```User::SetPassword``` and of course our ```User::ChangePassword``` service.
|
53
|
-
|
54
|
-
```ruby
|
55
|
-
class User::ChangePassword
|
56
|
-
def call(user, old_password, new_password)
|
57
|
-
if User::Authenticate.new.call(user, old_password)
|
58
|
-
User::SetPassword.new.call(user, new_password)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
```
|
63
|
-
|
64
|
-
This is a simple example, but the idea should be clear. Split up complex workflows in to many single one's.
|
65
|
-
|
66
|
-
#### Call them from anywhere
|
67
|
-
|
68
|
-
Have you ever been in a situation, where you find yourself duplicating functionality for an api or a rake task, allthough you knew you had this functionality allready in your app ? Well, with service objects, this functionality can be called from anywhere. No more duplication. Call your service objects from other service objects, from DelayedJob/Rescue/Sidekiq jobs, inside a rake task or simply the console.
|
69
|
-
|
70
|
-
```ruby
|
71
|
-
$> User::ChangePassword(my_user, "test", "123")
|
72
|
-
```
|
73
|
-
|
74
|
-
## What does servizio add to these basic ideas
|
75
|
-
|
76
|
-
If you read the *basic ideas* paragraph you noticed, you may noticed, that this can all be done just with POROs. So why Servizio? Well, it simple extends the basic ideas and goes some steps further.
|
77
|
-
|
78
|
-
### Common service class
|
79
|
-
|
80
|
-
Servizio provides a super class, your services can be inherited from. I will explain the benefits later.
|
81
|
-
|
82
|
-
```ruby
|
83
|
-
class User::ChangePassword < Servizio::Service
|
84
|
-
...
|
85
|
-
end
|
86
|
-
```
|
87
|
-
|
88
|
-
### Service objects are activemodel objects
|
89
|
-
|
90
|
-
Because ```Servizio::Service``` includes ```ActiveModel::Model``` all subclass are activemodel objects. This is especially cool, if you work with rails, as you can use activemodel objects similarly to activerecord objects. Besides ```ActiveModel::Model```, ```Servizio::Service``` includes some other activemodel modules.
|
91
|
-
|
92
|
-
```ruby
|
93
|
-
class Servizio::Service
|
94
|
-
...
|
95
|
-
extend ActiveModel::Callbacks
|
96
|
-
include ActiveModel::Model
|
97
|
-
include ActiveModel::Validations
|
98
|
-
...
|
99
|
-
end
|
100
|
-
```
|
101
|
-
|
102
|
-
#### Calling semantic
|
103
|
-
|
104
|
-
Because every service is an activemodel object, the calling semantic changes slightly. With servizio, you create an instance of an service with all necessary parameters as a hash and call the resulting instance (an operation).
|
105
|
-
|
106
|
-
```ruby
|
107
|
-
operation = User::ChangePassword.new(user: current_user, current_password: "123", new_password: "test")
|
108
|
-
operation.call
|
109
|
-
```
|
110
|
-
|
111
|
-
#### Validations
|
112
|
-
|
113
|
-
Because ```Servizio::Service``` also includes ```ActiveModel::Validations```, you can validate your operation before calling it, using activerecord-style validators.
|
114
|
-
|
115
|
-
```ruby
|
116
|
-
class User::ChangePassword < Servizio::Service
|
117
|
-
attr_accessor :current_password
|
118
|
-
attr_accessor :new_password
|
119
|
-
attr_accessor :new_password_confirmation
|
120
|
-
attr_accessor :user
|
121
|
-
|
122
|
-
validates_presence_of :current_password
|
123
|
-
validates_presence_of :new_password
|
124
|
-
validates_presence_of :new_password_confirmation
|
125
|
-
validates_confirmation_of :new_password
|
126
|
-
validates_presence_of :user
|
127
|
-
|
128
|
-
def call
|
129
|
-
Some::External::Service.change_user_password(user.id, current_password, new_password)
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
operation = User::ChangePassword.new(user: current_user, current_password: "123", new_password: "test")
|
134
|
-
operation.call if operation.valid?
|
135
|
-
```
|
136
|
-
|
137
|
-
#### Generic error reporting
|
138
|
-
|
139
|
-
As a side-effect of including ```ActiveModel::Validations```, any service has an ```errors``` object. If something goes wrong inside your call, you can add an entry there to signal this event to the caller. This solves the problem, how to get notified/react on errors.
|
140
|
-
|
141
|
-
One could also simply return ```nil``` from a call in case of an error. But this is not a good solution, because maybe the call returns without a result in any case. ```true``` or ```false``` are also ambiguous. Thatswhy error reporting should be separate from the call result.
|
142
|
-
|
143
|
-
```ruby
|
144
|
-
class User::ChangePassword < Servizio::Service
|
145
|
-
...
|
146
|
-
def call
|
147
|
-
begin
|
148
|
-
Some::External::Service.change_user_password(user.id, current_password, new_password)
|
149
|
-
rescue
|
150
|
-
errors.add(:call, "The call failed!")
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|
154
|
-
```
|
155
|
-
|
156
|
-
### Operation state accessors
|
157
|
-
|
158
|
-
If you subclass ```Servizio::Service``` your service has several state accessors
|
159
|
-
* called?
|
160
|
-
* error? (alias failed?)
|
161
|
-
* success? (alias succeeded?)
|
162
|
-
* valid?
|
163
|
-
|
164
|
-
You can use them to react on the result of an operation.
|
165
|
-
|
166
|
-
```ruby
|
167
|
-
operation = User::ChangePassword.new(user: current_user, current_password: "123", new_password: "test")
|
168
|
-
operation.call if operation.valid?
|
169
|
-
|
170
|
-
if operation.failed?
|
171
|
-
render "..."
|
172
|
-
elsif operation.succeeded?
|
173
|
-
redirect_to "..."
|
174
|
-
end
|
175
|
-
```
|
176
|
-
|
177
|
-
### Some meta programming to ease service creation
|
178
|
-
|
179
|
-
Have you wondered, where ```operation.result``` comes from ? In no example above it's explictly set. Well let's dig into how ```call``` actualy is implemented.
|
180
|
-
|
181
|
-
When you execute the call method of an operation, you actually call ```Servizio::Service:Call.call```, which in fact calls the operations call method later, but wraps it, so that callbacks can be triggered, validations can take place in front of an call and the state of the operation changes automatically.
|
182
|
-
|
183
|
-
That's the reason you don't have to do anything but implement your ```call``` method for most simple use cases. Everything else is handled for you automatically.
|
184
|
-
|
185
|
-
This is achived by using ruby's ```prepend``` in association with ```inherited```. If you want to know how it works exactly, have a look.
|
186
|
-
|
187
|
-
```ruby
|
188
|
-
module Servizio::Service::Call
|
189
|
-
|
190
|
-
def call
|
191
|
-
run_callbacks :call do
|
192
|
-
if authorized? && valid?
|
193
|
-
@called = true
|
194
|
-
self.result = super
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|
198
|
-
|
199
|
-
...
|
200
|
-
end
|
201
|
-
|
202
|
-
class Servizio::Service
|
203
|
-
require_relative "./service/call"
|
204
|
-
|
205
|
-
def self.inherited(subclass)
|
206
|
-
subclass.prepend(Servizio::Service::Call)
|
207
|
-
end
|
208
|
-
|
209
|
-
...
|
210
|
-
```
|
211
|
-
|
212
|
-
As you can see, before a operation is called, it's validated. So an operation, that's invalid, will not execute the acutal call method.
|
213
|
-
|
214
|
-
|
215
|
-
### Service objects can be validated
|
216
|
-
|
217
|
-
Because each service is an activemodel object, you can
|
218
|
-
|
219
|
-
|
220
|
-
## Terminology, conventions and background
|
221
|
-
|
222
|
-
Let's clear some terms first, so that they can be used later without further explanation.
|
223
|
-
|
224
|
-
### Service
|
225
|
-
|
226
|
-
A service is a subclass of ```Servizio::Service```. It has to implement a method named ```call```. It may implement ```ActiveModel```-style validations.
|
13
|
+
Servizio is a class you can derive your service classes from. It includes ```ActiveModel::Model``` and ```ActiveModel::Validations``` and wraps the ```call``` method of the derived class by prepending some code to the derived class' singleton class. The main purpose is to provide some conventions for and to ease the creation of service classes.
|
227
14
|
|
228
15
|
```ruby
|
229
|
-
|
230
|
-
|
231
|
-
class ChangePassword < Servizio::Service
|
232
|
-
attr_accessor :current_password
|
233
|
-
attr_accessor :new_password
|
234
|
-
attr_accessor :new_password_confirmation
|
235
|
-
attr_accessor :user
|
16
|
+
class MyService < Servizio::Service
|
17
|
+
attr_accessor :summands
|
236
18
|
|
237
|
-
validates_presence_of :
|
238
|
-
validates_presence_of :new_password
|
239
|
-
validates_presence_of :new_password_confirmation
|
240
|
-
validates_confirmation_of :new_password
|
241
|
-
validates_presence_of :user
|
19
|
+
validates_presence_of :summands
|
242
20
|
|
243
21
|
def call
|
244
|
-
|
22
|
+
summands.reduce(:+)
|
245
23
|
end
|
246
24
|
end
|
247
|
-
```
|
248
|
-
|
249
|
-
### Operation
|
250
25
|
|
251
|
-
|
252
|
-
|
253
|
-
```ruby
|
254
|
-
operation = ChangePassword.new(
|
255
|
-
user: current_user,
|
256
|
-
current_password: "test",
|
257
|
-
new_password: "123",
|
258
|
-
new_password_confirmation: "123"
|
259
|
-
)
|
260
|
-
```
|
261
|
-
|
262
|
-
### Errors
|
263
|
-
|
264
|
-
Due to the fact, that ```Servizio::Service``` inludes ```ActiveModel::Validations``` we already got an error store in each derived class in form of an ```errors``` object. One point, all errors can happily reside. You can add entries there, e.g. if you call fails.
|
26
|
+
# create an instance of a service (a.k.a. an operation)
|
27
|
+
operation = MyService.new(operands: [1,2,3])
|
265
28
|
|
29
|
+
# call the operation and get it's result
|
30
|
+
operation.call.result # => 6
|
266
31
|
```
|
267
|
-
class ChangePassword < Servizio::Service
|
268
|
-
attr_accessor :current_password
|
269
|
-
...
|
270
|
-
|
271
|
-
def call
|
272
|
-
begin
|
273
|
-
Some::External::Service.change_user_password(user.id, current_password, new_password)
|
274
|
-
rescue
|
275
|
-
errors.add(:call, "Call went wrong!")
|
276
|
-
end
|
277
|
-
end
|
278
|
-
end
|
279
|
-
```
|
280
|
-
|
281
|
-
***Convention***
|
282
|
-
If the call fails without an result, e.g. if you are calling an external webservice and get an 500, you should set errors[:call].
|
283
|
-
|
284
|
-
### States and callbacks
|
285
|
-
|
286
|
-
Servizio knows various states an operation can be in, namely```(denied)```, ```invalid```, ```error```, ```success```. You can hook on to those states by using callbacks.
|
287
|
-
|
288
|
-
An call is assumed to be successfull, if the operation was called and ```errors``` is empty. An operation is invalid if it was tried to be called, but didn't validated.
|
289
|
-
|
290
|
-
```ruby
|
291
|
-
operation.on_invalid -> (operation) do
|
292
|
-
render change_password_user_path
|
293
|
-
end
|
294
|
-
|
295
|
-
# there can be more than one
|
296
|
-
operation.on_invalid -> (operation) do
|
297
|
-
log "Somebody failed to change it's password!"
|
298
|
-
end
|
299
|
-
|
300
|
-
operation.call
|
301
|
-
|
302
|
-
# will be executed immediately (if the call was successfull)
|
303
|
-
operation.on_success -> (operation) do
|
304
|
-
flash[:success] = "Password changed!"
|
305
|
-
redirect_to :user_path
|
306
|
-
end
|
307
|
-
```
|
308
|
-
|
309
|
-
Callbacks work like jQuery promises. You can even add callbacks after an operation was called, which will trigger the corresponding callback immediately.
|
310
|
-
|
311
|
-
### Call it magic
|
312
|
-
|
313
|
-
When you execute the call method of an operation, you actually call ```Servizio::Service:Call.call```, which in fact calls the operations call method later, but wraps it, so that callbacks can be triggered, validations can take place in front of an call and the state of the operation changes automatically.
|
314
|
-
|
315
|
-
That's the reason you don't have to do anything but implement your ```call``` method for most simple use cases. Everything else is handled for you automatically.
|
316
|
-
|
317
|
-
This is achived by using ruby's ```prepend``` in association with ```inherited```. If you want to know how it works exactly, have a look.
|
318
|
-
|
319
|
-
```ruby
|
320
|
-
module Servizio::Service::Call
|
321
|
-
|
322
|
-
def call
|
323
|
-
run_callbacks :call do
|
324
|
-
if authorized? && valid?
|
325
|
-
@called = true
|
326
|
-
self.result = super
|
327
|
-
end
|
328
|
-
end
|
329
|
-
end
|
330
|
-
|
331
|
-
...
|
332
|
-
end
|
333
|
-
|
334
|
-
class Servizio::Service
|
335
|
-
require_relative "./service/call"
|
336
|
-
|
337
|
-
def self.inherited(subclass)
|
338
|
-
subclass.prepend(Servizio::Service::Call)
|
339
|
-
end
|
340
|
-
|
341
|
-
...
|
342
|
-
```
|
343
|
-
|
344
|
-
## Usage
|
345
|
-
|
346
|
-
### Basic example
|
347
|
-
|
348
|
-
```ruby
|
349
|
-
require "servizio"
|
350
|
-
|
351
|
-
class ChangePassword < Servizio::Service
|
352
|
-
attr_accessor :current_password
|
353
|
-
attr_accessor :new_password
|
354
|
-
attr_accessor :new_password_confirmation
|
355
|
-
attr_accessor :user
|
356
|
-
|
357
|
-
validates_presence_of :current_password
|
358
|
-
validates_presence_of :new_password
|
359
|
-
validates_presence_of :new_password_confirmation
|
360
|
-
validates_confirmation_of :new_password
|
361
|
-
validates_presence_of :user
|
362
|
-
|
363
|
-
def call
|
364
|
-
Some::External::WebService.change_user_password(user.id, current_password, new_password)
|
365
|
-
end
|
366
|
-
end
|
367
|
-
|
368
|
-
operation = ChangePassword.new(
|
369
|
-
user: current_user,
|
370
|
-
current_password: "test",
|
371
|
-
new_password: "123",
|
372
|
-
new_password_confirmation: "123"
|
373
|
-
)
|
374
|
-
|
375
|
-
operation.on_invalid -> (operation) do
|
376
|
-
render change_password_user_path
|
377
|
-
end
|
378
|
-
|
379
|
-
operation.on_success -> (operation) do
|
380
|
-
flash[:success] = "Password changed!"
|
381
|
-
redirect_to :user_path
|
382
|
-
end
|
383
|
-
|
384
|
-
operation.call
|
385
|
-
```
|
386
|
-
|
387
|
-
### Why is it cool?!
|
388
|
-
|
389
|
-
#### Validations included
|
390
|
-
|
391
|
-
Most operations need some kind of input to operate. If we take the ```ChangePassword``` service from the basic example, it needs
|
392
|
-
* *current_password*
|
393
|
-
* *new_password*
|
394
|
-
* *new_password_confirmation* (which must match *new_password*)
|
395
|
-
|
396
|
-
The external service which actually changes the password should only be called, if the requirements are met. Because ```Servizio::Service``` is in fact an ```ActiveModel``` class and includes ```ActiveModel::Validations``` you can write ```ActiveRecord``` style validators right into your service object. Than you can call ```valid?``` to check, if everything is ready. Or you simply hook up with an ```on_invalid``` callback.
|
397
|
-
|
398
|
-
More than this, because validations work like in ```ActiveRecord```, you can simply build forms for your services, which will exactly behave like for an ```ActiveRecord``` model. That means you can have a ```ChangePassword``` form, not an ```User``` form and it will work with gems like ```simple_form``` out-of-the-box.
|
399
|
-
|
400
|
-
#### Callbacks
|
401
|
-
|
402
|
-
There is nothing asynchronous in ```Servizio``` till now, but you can register callbacks for various states of an service object instance. Known states are ```(denied)```, ```invalid```, ```error```, ```success```. ```denied``` only works, if the service was instantiated with an ```cancan(can)```-like ability, else an operation is never denied.
|
403
|
-
|
404
32
|
|
405
33
|
## Additional readings
|
406
34
|
* http://brewhouse.io/blog/2014/04/30/gourmet-service-objects.html
|
@@ -408,7 +36,7 @@ There is nothing asynchronous in ```Servizio``` till now, but you can register c
|
|
408
36
|
|
409
37
|
## Contributing
|
410
38
|
|
411
|
-
1. Fork it ( https://github.com/
|
39
|
+
1. Fork it ( https://github.com/msievers/servizio/fork )
|
412
40
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
413
41
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
414
42
|
4. Push to the branch (`git push origin my-new-feature`)
|
data/lib/servizio/service.rb
CHANGED
data/lib/servizio/version.rb
CHANGED
@@ -58,6 +58,15 @@ describe Servizio::Service do
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
+
#
|
62
|
+
# call!
|
63
|
+
#
|
64
|
+
describe "#call!" do
|
65
|
+
it "is an alias for call" do
|
66
|
+
expect(succeeding_operation.call.result).to eq(service.new(summands: summands).call!.result)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
61
70
|
#
|
62
71
|
# called?
|
63
72
|
#
|