servizio 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +7 -0
- data/README.md +395 -9
- data/Rakefile +4 -0
- data/lib/servizio/service/define_column_type.rb +39 -0
- data/lib/servizio/service.rb +68 -0
- data/lib/servizio/version.rb +1 -1
- data/lib/servizio.rb +1 -1
- data/servizio.gemspec +5 -1
- data/spec/servizio/service_spec.rb +135 -0
- data/spec/servizio_spec.rb +2 -0
- data/spec/spec_helper.rb +26 -0
- metadata +65 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 33a8b3cbe124642d0826b788731ae4931e6b53b4
|
4
|
+
data.tar.gz: 3fc2848c7b95a28eb5a3759c04f9bfe95d3f817b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e012d8e1b27275d4f6ee02b5f27d5ed70444356359637127d771b094acddffdd9cf321df765f9747300955844f1f7e4dae868ba7cb6d02c22cf4e0beeb3ed4fd
|
7
|
+
data.tar.gz: 84fb4472010155b31d22db0b276bb789c506f6d1423e3ee4cf74e225ce1bd7b24afc62a3da1f0e8c62b5831b6040f8f6db81667390535eefcbd3b72b2c9da99e
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
@@ -2,3 +2,10 @@ source "https://rubygems.org"
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in servizio.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
gem "codeclimate-test-reporter", group: :test, require: nil
|
7
|
+
|
8
|
+
gem "pry", "~> 0.9.12.6"
|
9
|
+
gem 'pry-byebug', "<= 1.3.2"
|
10
|
+
gem "pry-stack_explorer", "~> 0.4.9.1"
|
11
|
+
gem "pry-syntax-hacks", "~> 0.0.6"
|
data/README.md
CHANGED
@@ -1,24 +1,410 @@
|
|
1
1
|
# Servizio
|
2
2
|
|
3
|
-
|
3
|
+
[![Build Status](https://travis-ci.org/msievers/servizio.svg?branch=master)](https://travis-ci.org/msievers/servizio)
|
4
|
+
[![Test Coverage](https://codeclimate.com/github/msievers/servizio/badges/coverage.svg)](https://codeclimate.com/github/msievers/servizio)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/msievers/servizio/badges/gpa.svg)](https://codeclimate.com/github/msievers/servizio)
|
4
6
|
|
5
|
-
|
7
|
+
Servizio is a gem to support you creating service objects. It was created after I read a blog post about [service objects](http://brewhouse.io/blog/2014/04/30/gourmet-service-objects.html) from [Philippe Creux](https://twitter.com/pcreux). Realy great post, check it out.
|
6
8
|
|
7
|
-
|
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```.
|
8
10
|
|
9
|
-
|
11
|
+
## The basic ideas
|
10
12
|
|
11
|
-
|
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.
|
12
14
|
|
13
|
-
|
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```.
|
14
16
|
|
15
|
-
|
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.
|
16
18
|
|
17
|
-
|
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.
|
227
|
+
|
228
|
+
```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
|
236
|
+
|
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
|
242
|
+
|
243
|
+
def call
|
244
|
+
Some::External::Service.change_user_password(user.id, current_password, new_password)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
```
|
248
|
+
|
249
|
+
### Operation
|
250
|
+
|
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.
|
265
|
+
|
266
|
+
```
|
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
|
+
```
|
18
343
|
|
19
344
|
## Usage
|
20
345
|
|
21
|
-
|
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
|
+
|
405
|
+
## Additional readings
|
406
|
+
* http://brewhouse.io/blog/2014/04/30/gourmet-service-objects.html
|
407
|
+
* https://netguru.co/blog/service-objects-in-rails-will-help
|
22
408
|
|
23
409
|
## Contributing
|
24
410
|
|
data/Rakefile
CHANGED
@@ -0,0 +1,39 @@
|
|
1
|
+
#
|
2
|
+
# In order to make service objects more compatible with simple_form, one should
|
3
|
+
# define column types for attributes, so that the input types can be determined
|
4
|
+
# by simple_form automatically. Simple include this into your service class.
|
5
|
+
#
|
6
|
+
module Servizio::Service::DefineColumnType
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
base.include InstanceMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.column_type_getter_name(attribute)
|
13
|
+
"defined_column_type_for_#{attribute}"
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def define_column_type(attribute, type)
|
18
|
+
define_method(Servizio::Service::DefineColumnType.column_type_getter_name(attribute)) do
|
19
|
+
type.to_sym
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module InstanceMethods
|
25
|
+
def column_for_attribute(attribute)
|
26
|
+
if has_attribute?(attribute)
|
27
|
+
Struct.new(:limit, :name, :type).new(
|
28
|
+
nil,
|
29
|
+
attribute,
|
30
|
+
send(Servizio::Service::DefineColumnType.column_type_getter_name(attribute))
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def has_attribute?(attribute)
|
36
|
+
respond_to?(Servizio::Service::DefineColumnType.column_type_getter_name(attribute))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "active_model"
|
2
|
+
|
3
|
+
class Servizio::Service
|
4
|
+
include ActiveModel::Model
|
5
|
+
include ActiveModel::Validations
|
6
|
+
|
7
|
+
attr_accessor :result
|
8
|
+
|
9
|
+
OperationNotCalledError = Class.new(StandardError)
|
10
|
+
|
11
|
+
# http://stackoverflow.com/questions/14431723/activemodelvalidations-on-anonymous-class
|
12
|
+
def self.name
|
13
|
+
super ? super : "__anonymous_servizio_service_class__"
|
14
|
+
end
|
15
|
+
|
16
|
+
def result
|
17
|
+
called? ? @result : (raise OperationNotCalledError)
|
18
|
+
end
|
19
|
+
|
20
|
+
def called?
|
21
|
+
@called == true
|
22
|
+
end
|
23
|
+
|
24
|
+
def failed?
|
25
|
+
called? && errors.present?
|
26
|
+
end
|
27
|
+
|
28
|
+
def succeeded?
|
29
|
+
called? && errors.blank?
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# This code does some metaprogramming magic. It overwrites .new, so that every
|
34
|
+
# instance of a class derived from Servizio::Service, gets a module prepended
|
35
|
+
# automatically. This way, one can easily "wrap" the methods, e.g. #call.
|
36
|
+
#
|
37
|
+
module MethodDecorators
|
38
|
+
module Call
|
39
|
+
def call
|
40
|
+
if valid?
|
41
|
+
self.result = super
|
42
|
+
@called = true
|
43
|
+
else
|
44
|
+
@called = false
|
45
|
+
end
|
46
|
+
|
47
|
+
self
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def inherited(subclass)
|
52
|
+
subclass.instance_eval do
|
53
|
+
alias :original_new :new
|
54
|
+
|
55
|
+
def self.inherited(subsubclass)
|
56
|
+
subsubclass.extend(Servizio::Service::MethodDecorators)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.new(*args, &block)
|
60
|
+
(obj = original_new(*args, &block)).singleton_class.send(:prepend, Servizio::Service::MethodDecorators::Call)
|
61
|
+
return obj
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
extend MethodDecorators
|
68
|
+
end
|
data/lib/servizio/version.rb
CHANGED
data/lib/servizio.rb
CHANGED
data/servizio.gemspec
CHANGED
@@ -16,6 +16,10 @@ Gem::Specification.new do |spec|
|
|
16
16
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
17
17
|
spec.require_paths = ["lib"]
|
18
18
|
|
19
|
-
spec.
|
19
|
+
spec.add_dependency "activemodel", ">= 4.0.0"
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
20
22
|
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec", ">= 3.0.0", "< 4.0.0"
|
24
|
+
spec.add_development_dependency "simplecov", ">= 0.8.0"
|
21
25
|
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
describe Servizio::Service do
|
2
|
+
context "if derived" do
|
3
|
+
let(:service) do
|
4
|
+
Class.new(described_class) do
|
5
|
+
attr_accessor :should_fail
|
6
|
+
attr_accessor :summands
|
7
|
+
|
8
|
+
validates_presence_of :summands
|
9
|
+
|
10
|
+
def should_fail?
|
11
|
+
@should_fail == true
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
if should_fail?
|
16
|
+
errors.add(:call, "failed")
|
17
|
+
else
|
18
|
+
summands.reduce(:+)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:summands) { [1,2,3] }
|
25
|
+
let(:succeeding_operation) { service.new summands: summands }
|
26
|
+
let(:failing_operation) { service.new summands: summands, should_fail: true }
|
27
|
+
let(:invalid_operation) { service.new }
|
28
|
+
|
29
|
+
context "if derived as anonymous class" do
|
30
|
+
# http://stackoverflow.com/questions/14431723/activemodelvalidations-on-anonymous-class
|
31
|
+
it "sets the class name to something non-blank to allow validations" do
|
32
|
+
expect(service.name).not_to be_blank
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "if derived from a derived class" do
|
37
|
+
let(:derived_service) { Class.new(service) }
|
38
|
+
|
39
|
+
it "works as expected" do
|
40
|
+
expect(derived_service.new(summands: summands).call.result).to eq(summands.reduce(:+))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# call
|
46
|
+
#
|
47
|
+
describe "#call" do
|
48
|
+
context "if the operation is valid with respect to its validators" do
|
49
|
+
it "makes #called? return true" do
|
50
|
+
expect(succeeding_operation.call.called?).to eq(true)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "if the operation is not valid with respect to its validators" do
|
55
|
+
it "makes #called? return false" do
|
56
|
+
expect(invalid_operation.call.called?).to eq(false)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# called?
|
63
|
+
#
|
64
|
+
describe "#called?" do
|
65
|
+
context "if the operation was called (whether there were failures or not)" do
|
66
|
+
it "returns true" do
|
67
|
+
expect(succeeding_operation.call.called?).to be(true)
|
68
|
+
expect(failing_operation.call.called?).to be(true)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#failed?" do
|
74
|
+
context "if the operation was called without errors" do
|
75
|
+
it "returns false" do
|
76
|
+
expect(succeeding_operation.call.failed?).to be(false)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "if the operation added something to \"errors\"" do
|
81
|
+
it "returns true" do
|
82
|
+
expect(failing_operation.call.failed?).to be(true)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "if the operation was not called" do
|
87
|
+
it "returns false" do
|
88
|
+
expect(succeeding_operation.failed?).to be(false)
|
89
|
+
expect(failing_operation.failed?).to be(false)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# result
|
96
|
+
#
|
97
|
+
describe "result" do
|
98
|
+
context "if the operation was called" do
|
99
|
+
it "provides the return value of the call method" do
|
100
|
+
expect(succeeding_operation.call.result).to eq(summands.reduce(:+))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "if the operation was not called" do
|
105
|
+
it "raises an error" do
|
106
|
+
expect { succeeding_operation.result}.to raise_error(described_class::OperationNotCalledError)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# succeeded?
|
113
|
+
#
|
114
|
+
describe "#succeeded?" do
|
115
|
+
context "if the operation was called without errors" do
|
116
|
+
it "returns true" do
|
117
|
+
expect(succeeding_operation.call.succeeded?).to be(true)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "if the operation added something to \"errors\"" do
|
122
|
+
it "returns false" do
|
123
|
+
expect(failing_operation.call.succeeded?).to be(false)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context "if the operation was not called" do
|
128
|
+
it "returns false" do
|
129
|
+
expect(succeeding_operation.succeeded?).to be(false)
|
130
|
+
expect(failing_operation.succeeded?).to be(false)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
if ENV["TRAVIS"]
|
2
|
+
require "codeclimate-test-reporter"
|
3
|
+
CodeClimate::TestReporter.start
|
4
|
+
else
|
5
|
+
require "simplecov"
|
6
|
+
SimpleCov.start
|
7
|
+
end
|
8
|
+
|
9
|
+
begin
|
10
|
+
require "pry"
|
11
|
+
rescue LoadError
|
12
|
+
end
|
13
|
+
|
14
|
+
require "servizio"
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
# begin --- rspec 3.1 generator
|
18
|
+
config.expect_with :rspec do |expectations|
|
19
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
20
|
+
end
|
21
|
+
|
22
|
+
config.mock_with :rspec do |mocks|
|
23
|
+
mocks.verify_partial_doubles = true
|
24
|
+
end
|
25
|
+
# end --- rspec 3.1 generator
|
26
|
+
end
|
metadata
CHANGED
@@ -1,29 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: servizio
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Sievers
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-02-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activemodel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.0.0
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: bundler
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
16
30
|
requirements:
|
17
31
|
- - "~>"
|
18
32
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
33
|
+
version: '1.7'
|
20
34
|
type: :development
|
21
35
|
prerelease: false
|
22
36
|
version_requirements: !ruby/object:Gem::Requirement
|
23
37
|
requirements:
|
24
38
|
- - "~>"
|
25
39
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
40
|
+
version: '1.7'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: rake
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +52,40 @@ dependencies:
|
|
38
52
|
- - ">="
|
39
53
|
- !ruby/object:Gem::Version
|
40
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.0.0
|
62
|
+
- - "<"
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 4.0.0
|
65
|
+
type: :development
|
66
|
+
prerelease: false
|
67
|
+
version_requirements: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: 3.0.0
|
72
|
+
- - "<"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 4.0.0
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: simplecov
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 0.8.0
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 0.8.0
|
41
89
|
description:
|
42
90
|
email:
|
43
91
|
executables: []
|
@@ -45,13 +93,20 @@ extensions: []
|
|
45
93
|
extra_rdoc_files: []
|
46
94
|
files:
|
47
95
|
- ".gitignore"
|
96
|
+
- ".rspec"
|
97
|
+
- ".travis.yml"
|
48
98
|
- Gemfile
|
49
99
|
- LICENSE.txt
|
50
100
|
- README.md
|
51
101
|
- Rakefile
|
52
102
|
- lib/servizio.rb
|
103
|
+
- lib/servizio/service.rb
|
104
|
+
- lib/servizio/service/define_column_type.rb
|
53
105
|
- lib/servizio/version.rb
|
54
106
|
- servizio.gemspec
|
107
|
+
- spec/servizio/service_spec.rb
|
108
|
+
- spec/servizio_spec.rb
|
109
|
+
- spec/spec_helper.rb
|
55
110
|
homepage: https://github.com/msievers/servizio
|
56
111
|
licenses:
|
57
112
|
- MIT
|
@@ -72,8 +127,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
127
|
version: '0'
|
73
128
|
requirements: []
|
74
129
|
rubyforge_project:
|
75
|
-
rubygems_version: 2.
|
130
|
+
rubygems_version: 2.4.2
|
76
131
|
signing_key:
|
77
132
|
specification_version: 4
|
78
133
|
summary: Yet another service object support library
|
79
|
-
test_files:
|
134
|
+
test_files:
|
135
|
+
- spec/servizio/service_spec.rb
|
136
|
+
- spec/servizio_spec.rb
|
137
|
+
- spec/spec_helper.rb
|
138
|
+
has_rdoc:
|