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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 25af1f3c4241dedbd2d60a9d627af897f1bbe0c0
4
- data.tar.gz: ca482c365183cf69145f15189f4ef717ae788088
3
+ metadata.gz: 33a8b3cbe124642d0826b788731ae4931e6b53b4
4
+ data.tar.gz: 3fc2848c7b95a28eb5a3759c04f9bfe95d3f817b
5
5
  SHA512:
6
- metadata.gz: f59fbd2c26a199000bdbb1bd3861eefc44da6b6066deac132c8e4fc926b351b1dd34e75d8c008dabfdfce44f8a1c89c0436c70c0aef054e3a9c20c9ead3395bb
7
- data.tar.gz: 3237d8c48890fd5a961a7b6b13154a3602292f8f510cd73702799763b40abe5144236b96d180ed5900d26e6d820acd8e923d85189c58424a2d00c0a7865429d2
6
+ metadata.gz: e012d8e1b27275d4f6ee02b5f27d5ed70444356359637127d771b094acddffdd9cf321df765f9747300955844f1f7e4dae868ba7cb6d02c22cf4e0beeb3ed4fd
7
+ data.tar.gz: 84fb4472010155b31d22db0b276bb789c506f6d1423e3ee4cf74e225ce1bd7b24afc62a3da1f0e8c62b5831b6040f8f6db81667390535eefcbd3b72b2c9da99e
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - "2.0"
4
+ - "2.1"
5
+ - "2.2"
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
- TODO: Write a gem description
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
- ## Installation
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
- Add this line to your application's Gemfile:
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
- gem 'servizio'
11
+ ## The basic ideas
10
12
 
11
- And then execute:
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
- $ bundle
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
- Or install it yourself as:
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
- $ gem install servizio
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
- TODO: Write usage instructions here
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
@@ -1,2 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
2
3
 
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Servizio
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/servizio.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require "servizio/version"
2
2
 
3
3
  module Servizio
4
- # Your code goes here...
4
+ require_relative "./servizio/service"
5
5
  end
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.add_development_dependency "bundler", "~> 1.6"
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
@@ -0,0 +1,2 @@
1
+ describe Servizio do
2
+ end
@@ -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.1
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: 2014-09-24 00:00:00.000000000 Z
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.6'
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.6'
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.2.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: