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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 33a8b3cbe124642d0826b788731ae4931e6b53b4
4
- data.tar.gz: 3fc2848c7b95a28eb5a3759c04f9bfe95d3f817b
3
+ metadata.gz: 813806f66607dba01c96b178f0ccde49ff28e845
4
+ data.tar.gz: 30fba7afa7b7d808ceb021b84e490215b698ad4a
5
5
  SHA512:
6
- metadata.gz: e012d8e1b27275d4f6ee02b5f27d5ed70444356359637127d771b094acddffdd9cf321df765f9747300955844f1f7e4dae868ba7cb6d02c22cf4e0beeb3ed4fd
7
- data.tar.gz: 84fb4472010155b31d22db0b276bb789c506f6d1423e3ee4cf74e225ce1bd7b24afc62a3da1f0e8c62b5831b6040f8f6db81667390535eefcbd3b72b2c9da99e
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
- ## The basic ideas
11
+ ## TL;DR
12
12
 
13
- For those who haven't read the original [post](http://brewhouse.io/blog/2014/04/30/gourmet-service-objects.html), let's sum up it's basic thoughts.
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
- require "servizio"
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 :current_password
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
- Some::External::Service.change_user_password(user.id, current_password, new_password)
22
+ summands.reduce(:+)
245
23
  end
246
24
  end
247
- ```
248
-
249
- ### Operation
250
25
 
251
- An operation is an instance of an service. Let's assume you have a service called ```ChangePassword```, then ```operation = ChangePassword.new```
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/[my-github-username]/servizio/fork )
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`)
@@ -46,6 +46,8 @@ class Servizio::Service
46
46
 
47
47
  self
48
48
  end
49
+
50
+ alias_method :call!, :call
49
51
  end
50
52
 
51
53
  def inherited(subclass)
@@ -1,3 +1,3 @@
1
1
  module Servizio
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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
  #
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: servizio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Sievers