service_objects 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.metrics +5 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +2 -0
  6. data/.travis.yml +4 -0
  7. data/.yardopts +3 -0
  8. data/Gemfile +3 -0
  9. data/Guardfile +16 -0
  10. data/LICENSE +21 -0
  11. data/README.md +461 -0
  12. data/Rakefile +17 -0
  13. data/config/metrics/STYLEGUIDE +230 -0
  14. data/config/metrics/cane.yml +5 -0
  15. data/config/metrics/churn.yml +6 -0
  16. data/config/metrics/flay.yml +2 -0
  17. data/config/metrics/metric_fu.yml +14 -0
  18. data/config/metrics/pippi.yml +3 -0
  19. data/config/metrics/reek.yml +1 -0
  20. data/config/metrics/roodi.yml +24 -0
  21. data/config/metrics/rubocop.yml +75 -0
  22. data/config/metrics/saikuro.yml +3 -0
  23. data/config/metrics/simplecov.yml +6 -0
  24. data/config/metrics/yardstick.yml +37 -0
  25. data/lib/service_objects/base.rb +43 -0
  26. data/lib/service_objects/helpers/dependable.rb +63 -0
  27. data/lib/service_objects/helpers/exceptions.rb +64 -0
  28. data/lib/service_objects/helpers/messages.rb +96 -0
  29. data/lib/service_objects/helpers/parameterized.rb +85 -0
  30. data/lib/service_objects/helpers/parameters.rb +71 -0
  31. data/lib/service_objects/helpers/validations.rb +54 -0
  32. data/lib/service_objects/invalid.rb +55 -0
  33. data/lib/service_objects/listener.rb +97 -0
  34. data/lib/service_objects/message.rb +117 -0
  35. data/lib/service_objects/null.rb +26 -0
  36. data/lib/service_objects/utils/normal_hash.rb +34 -0
  37. data/lib/service_objects/version.rb +9 -0
  38. data/lib/service_objects.rb +12 -0
  39. data/service_objects.gemspec +28 -0
  40. data/spec/spec_helper.rb +15 -0
  41. data/spec/tests/base_spec.rb +43 -0
  42. data/spec/tests/helpers/dependable_spec.rb +77 -0
  43. data/spec/tests/helpers/exceptions_spec.rb +112 -0
  44. data/spec/tests/helpers/messages_spec.rb +64 -0
  45. data/spec/tests/helpers/parameterized_spec.rb +136 -0
  46. data/spec/tests/helpers/parameters_spec.rb +71 -0
  47. data/spec/tests/helpers/validations_spec.rb +60 -0
  48. data/spec/tests/invalid_spec.rb +69 -0
  49. data/spec/tests/listener_spec.rb +50 -0
  50. data/spec/tests/message_spec.rb +191 -0
  51. data/spec/tests/null_spec.rb +17 -0
  52. data/spec/tests/utils/normal_hash_spec.rb +16 -0
  53. metadata +182 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b235fa6809000778410a4af3b1844d7665a600b9
4
+ data.tar.gz: 0f352e8303b2a39fd6e7ad72750b95f14b53c6dc
5
+ SHA512:
6
+ metadata.gz: 34c1d9c63afd08d42d7d37102bd85acb98e8e49f53024d469ef66ce6ed1be0b5ea0522a6b86009e694539635c134b94a49484d9bd8a9be6c919c7da1dd75b5d8
7
+ data.tar.gz: 2ede947fcf3c189e3ff82cd275ad43e5aefdc83bf7de3bee4cc0e2746aa425aed85e8d4b21e608b23c9dc098de3a2fcb3c76415674e160f89212a052a96615ae
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.metrics ADDED
@@ -0,0 +1,5 @@
1
+ # Settings for metric_fu and its packages are collected in the `config/metrics`
2
+ # and loaded by the Hexx::Suit::Metrics::MetricFu.
3
+
4
+ require "hexx-suit"
5
+ Hexx::Suit::Metrics::MetricFu.load
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ ---
2
+ inherit_from: "./config/metrics/rubocop.yml"
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1
4
+ - 2.2
data/.yardopts ADDED
@@ -0,0 +1,3 @@
1
+ --asset LICENSE
2
+ --exclude lib/service_objects/version.rb
3
+ --out doc/api
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+
3
+ guard :rspec, cmd: "bundle exec rspec" do
4
+
5
+ watch("lib/service_objects.rb") { "spec" }
6
+
7
+ watch(%r{^lib/service_objects/(.+)\.rb$}) do |m|
8
+ "spec/tests/#{ m[1] }_spec.rb"
9
+ end
10
+
11
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
12
+
13
+ watch(/^spec\/spec_helper\w*\.rb$/) { "spec" }
14
+
15
+ watch(%r{^spec/tests/.+_spec\.rb$})
16
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2014 Andrew Kozin, https://github.com/nepalez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,461 @@
1
+ # ServiceObjects
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/service_objects.svg?style=flat)][gem]
4
+ [![Build Status](https://img.shields.io/travis/nepalez/service_objects/master.svg?style=flat)][travis]
5
+ [![Dependency Status](https://img.shields.io/gemnasium/nepalez/service_objects.svg?style=flat)][gemnasium]
6
+ [![Code Climate](https://img.shields.io/codeclimate/github/nepalez/service_objects.svg?style=flat)][codeclimate]
7
+ [![Coverage](https://img.shields.io/coveralls/nepalez/service_objects.svg?style=flat)][coveralls]
8
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)][license]
9
+
10
+ [gem]: https://rubygems.org/gems/service_objects
11
+ [travis]: https://travis-ci.org/nepalez/service_objects
12
+ [gemnasium]: https://gemnasium.com/nepalez/service_objects
13
+ [codeclimate]: https://codeclimate.com/github/nepalez/service_objects
14
+ [coveralls]: https://coveralls.io/r/nepalez/service_objects
15
+ [license]: file:LICENSE
16
+
17
+ The module implements two design patterns:
18
+
19
+ * The [Interactor pattern] to decouple business logics from both models and delivery mechanisms, such as [Rails].
20
+ * The [Observer pattern] to follow the [Tell, don't Ask] design princible.
21
+ The pattern is implemented with the help of [wisper] gem by [Kris Leech].
22
+
23
+ [Rails]: http://rubyonrails.org/
24
+ [Interactor pattern]: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
25
+ [Observer pattern]: http://reefpoints.dockyard.com/2013/08/20/design-patterns-observer-pattern.html
26
+ [Tell, don't Ask]: http://martinfowler.com/bliki/TellDontAsk.html
27
+ [wisper]: http://www.github.com/krisleech/wisper
28
+ [Kris Leech]: http://www.github.com/krisleech
29
+
30
+ The module API provides 3 classes:
31
+
32
+ * `ServiceObjects::Base` - for service objects.
33
+ * `ServiceObjects::Listener` - for decorating objects with methods called by service notificiations.
34
+ * `ServiceObjects::Message` - for messages published by service objects.
35
+
36
+ ## Installation
37
+
38
+ Add this line to your application's Gemfile:
39
+
40
+ ```ruby
41
+ gem "service_objects"
42
+ ```
43
+
44
+ And then execute:
45
+
46
+ ```
47
+ bundle
48
+ ```
49
+
50
+ Or install it yourself as:
51
+
52
+ ```
53
+ gem install service_objects
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ The basic usage of the services by example of Rails controller:
59
+
60
+ ```ruby
61
+ # lib/my_gem.rb
62
+ require "service_objects"
63
+
64
+ # app/services/add_foo.rb
65
+ class AddFoo < ServiceObjects::Base
66
+
67
+ # whitelists parameters and defines #params, #bar and #baz
68
+ allows_params :bar, :baz
69
+
70
+ # declares external dependencies
71
+ depends_on :find_foo, default: FindFoo
72
+
73
+ # calls the service, sorts out and reports its results
74
+ def run
75
+ run!
76
+ rescue Invalid => err
77
+ publish :error, err.messages
78
+ else
79
+ publish :created, @foo, messages
80
+ ensure
81
+ self
82
+ end
83
+
84
+ private
85
+
86
+ # business logic lives here
87
+ def run!
88
+ # ... usage of the external service find_foo postponed
89
+ add_foo
90
+ end
91
+
92
+ # rescues errors and re-raises them as Invalid with list of #messages
93
+ def add_foo
94
+ escape { @foo ||= Foo.create! bar: bar, baz: baz }
95
+ end
96
+ end
97
+ ```
98
+
99
+ ```ruby
100
+ # app/controllers/foos_controller.rb
101
+ class FoosController < ApplicationController
102
+
103
+ def create
104
+ # Create the service object with necessary parameters.
105
+ # All the business logic is encapsulated inside the service,
106
+ # and the controller knows nothing what the service will do.
107
+ service = AddFoo.new params.allow(:bar, :baz)
108
+ # Subscribe the listener for the service's notifications
109
+ service.subscribe listener, prefix: :on
110
+ # Run the service
111
+ service.run
112
+ # If the service doesn't call any listener method,
113
+ # then a listener provides some default actions
114
+ listener.finalize
115
+ end
116
+
117
+ private
118
+
119
+ # The listener decorates the controller with methods
120
+ # to listen the service object's notifications
121
+ # (see FoosListener#otherwise method below).
122
+ def listener
123
+ @listener ||= FoosListener.new self
124
+ end
125
+
126
+ # The class to work out service object notifications
127
+ # The #render method is delegated to the controller
128
+ class FoosListener < ServiceObjects::Listener
129
+
130
+ # The method to be called when a service publishes
131
+ # the 'added' notification.
132
+ def on_added(foo, *, messages)
133
+ render "created", locals: { foo: foo, messages: messages }
134
+ end
135
+
136
+ # The method to be called when a service publishes
137
+ # the 'error' notification.
138
+ def on_error(*, messages)
139
+ render "error", locals: { messages: messages }
140
+ end
141
+
142
+ # The method is called by the #finalize in case no methods has been called
143
+ # by the service object.
144
+ #
145
+ # This allows to focuse only on a subset of service notifications above.
146
+ def otherwise
147
+ render "no_result"
148
+ end
149
+ end
150
+ end
151
+ ```
152
+
153
+ The service can notify several listeners (controller itself, mailer etc.).
154
+
155
+ ## Base
156
+
157
+ The `ServiceObjects::Base` provides base class for services.
158
+
159
+ ```ruby
160
+ require "service_objects"
161
+
162
+ class AddFoo < ServiceObjects::Base
163
+ end
164
+ ```
165
+
166
+ ### Parameters declaration
167
+
168
+ Define allowed parameters for objects:
169
+
170
+ ```ruby
171
+ class AddFoo < ServiceObjects::Base
172
+ allows_params :bar, :baz
173
+ end
174
+ ```
175
+
176
+ Parameters are whitelisted and assigned to `#params` hash (all keys are *symbolized*).
177
+
178
+ Attributes are also defined as aliases for corresponding params, so that `#bar` and `#bar=` are equivalent to `#params[:bar]`, `#params[:bar]=`.
179
+
180
+ **Note**:
181
+ The service ignores parameters except for explicitly declared. The client can give relevant data to the service, and leave the latter to figure them out by itself.
182
+
183
+ ### Validation
184
+
185
+ The `ServiceObject::Base` includes [ActiveModel::Validations] with methods `.validates`, `.validate`, `#errors`, `#valid?` and `#invalid?`. Use them to add action context - specific validations.
186
+
187
+ The method `#validate!` raises the `ServiceObject::Invalid` if validation fails.
188
+
189
+ ```ruby
190
+ class AddFoo < ServiceObjects::Base
191
+ allows_params :bar, :baz
192
+
193
+ validates :bar, presence: true
194
+ validates :baz, numericality: { greater_than: 1 }, allow_nil: true
195
+
196
+ # ...
197
+
198
+ def run!
199
+ # ...
200
+ validate!
201
+ # ...
202
+ end
203
+ end
204
+ ```
205
+
206
+ **Note:** You aren't restricted in selecting time for validation. Prepare attributes (either "real" or [virtual]) and run `#validate!` when necessary.
207
+
208
+ [ActiveModel::Validations]: http://api.rubyonrails.org/classes/ActiveModel/Validations.html
209
+ [virtual]: http://railscasts.com/episodes/16-virtual-attributes?view=asciicast
210
+
211
+ ### Dependencies declaration
212
+
213
+ As a rule, services uses each other to keep the code DRY. For example, the service that *adds* a new foo (whatever it means) can use another service to *find* an existing foo.
214
+
215
+ To made all that dependencies injectable via [setter injection], define them explicitly:
216
+
217
+ ```ruby
218
+ class AddFoo < ServiceObjects::Base
219
+ # ...
220
+
221
+ # Providing the FindFoo is available at the moment AddFoo being defined:
222
+ depends_on :find_foo, default: FindFoo
223
+ end
224
+ ```
225
+
226
+ Default value can be either assigned or skipped. In the last case the [Null Object] will be assigned by default.
227
+
228
+ The class method is public to postpone the default implementation until it is defined:
229
+
230
+ ```ruby
231
+ class AddFoo < ServiceObjects::Base
232
+ # ...
233
+ depends_on :find_foo
234
+ end
235
+
236
+ # later
237
+ FindFoo = Class.new
238
+ AddFoo.depends_on :find_foo, default: FindFoo
239
+ ```
240
+
241
+ This provides the instance attribute `#find_foo`. You can inject the dependency to the via setter:
242
+
243
+ ```ruby
244
+ service = AddFoo.new bar: "bar", baz: "baz"
245
+ service.find_foo = GetFoo
246
+ ```
247
+
248
+ [setter injection]: http://brandonhilkert.com/blog/a-ruby-refactor-exploring-dependency-injection-options/
249
+ [Null Object]: https://robots.thoughtbot.com/rails-refactoring-example-introduce-null-object
250
+
251
+ ### Run method
252
+
253
+ It is expected the `run` method to provide all the necessary staff and notify listeners via `#publish` method.
254
+
255
+ See [wisper] for details on `#publish` and `#subscribe` methods.
256
+
257
+ ```ruby
258
+ class AddFoo < ServiceObjects::Base
259
+ # ...
260
+
261
+ # The method contains the reporting logic only
262
+ def run
263
+ run!
264
+ rescue Found
265
+ publish :found, @foo, messages
266
+ rescue Invalid => err
267
+ publish :error, err.messages
268
+ else
269
+ publish :added, @foo, messages
270
+ ensure
271
+ self
272
+ end
273
+
274
+ # ...
275
+
276
+ private
277
+
278
+ Found = Class.new(RuntimeError) # the internal message
279
+
280
+ # Business logic lives here
281
+ def run!
282
+ get_foo
283
+ create_foo
284
+ end
285
+
286
+ def get_foo
287
+ # ... finds and assigns @foo somehow
288
+ fail Found if @foo
289
+ end
290
+
291
+ def add_foo
292
+ # ...
293
+ end
294
+ end
295
+ ```
296
+
297
+ There are some helper available:
298
+ * `messages` - an array of collected service messages
299
+ * `add_message` - adds the new message to the array
300
+ * `escape` - rescues from `StandardErrors` and re-raises them as `ServiceObject::Invalid` with collection of `#messages`.
301
+
302
+ **Note** Following [command-query separation] the `#run` method (being a command) returns `self`.
303
+
304
+ [command-query separation]: http://en.wikipedia.org/wiki/Command-query_separation
305
+
306
+ ### External services
307
+
308
+ External services should be used in just the same way as in the controller example.
309
+
310
+ ```ruby
311
+ class AddFoo < ServiceObjects::Base
312
+ depends_on :find_foo, default: FindFoo
313
+
314
+ # ...
315
+
316
+ def get_foo
317
+ service = find_foo.new params
318
+ service.subscribe listener, prefix: :on
319
+ service.run
320
+
321
+ # the method runs #otherwise callback in case
322
+ # no other notificaton has been received
323
+ listener.finalize
324
+ end
325
+
326
+ # decorates the service with methods to listen to external service
327
+ def listener
328
+ @listener ||= FindListener.new self
329
+ end
330
+
331
+ class FindListener < ServiceObjects::Listener
332
+ def on_found(foo, *)
333
+ __getobj__.foo = foo
334
+ end
335
+
336
+ def on_error(*, messages)
337
+ __getobj__.messages = messages
338
+ end
339
+
340
+ def otherwise
341
+ add_message "complain", "haven't been informed"
342
+ end
343
+ end
344
+ end
345
+ ```
346
+
347
+ Here the `#get_foo` runs the external service and listens to its notifications. Instead of the long syntax above, you can use a shortcut:
348
+
349
+ ```ruby
350
+ # ...
351
+ def get_foo
352
+ run_service find_foo.new(params), listener, prefix: :on
353
+ end
354
+ ```
355
+
356
+ ## Listener
357
+
358
+ The listener is a [decorator] that:
359
+
360
+ * defines callbacks to listen to service notifications.
361
+ * delegates all undefined methods to the encapsulated object (available via [__getobj__] instance method).
362
+ * defines the `#finalize` method to run `#otherwise` callback in case no other methods has been checked (via `#respond_to?` method).
363
+
364
+ ```ruby
365
+ class FooListener < ServiceObjects::Listener
366
+ def on_success(*)
367
+ "Notified on success"
368
+ end
369
+
370
+ def otherwise
371
+ "Hasn't been notified"
372
+ end
373
+ end
374
+
375
+ listener = FooListener.new
376
+ listener.finalize
377
+ # => "Hasn't been notified"
378
+
379
+ listener.respond_to? :on_error
380
+ # => false
381
+ listener.finalize
382
+ # => "Hasn't been notified"
383
+
384
+ listener.respond_to? :on_success
385
+ # => true
386
+ listener.finalize
387
+ # => nil
388
+ ```
389
+
390
+ [decorator]: http://nithinbekal.com/posts/ruby-decorators/
391
+ [__getobj__]: http://ruby-doc.org//stdlib-2.1.0/libdoc/delegate/rdoc/SimpleDelegator.html#method-i-__getobj__
392
+
393
+ ## Message
394
+
395
+ The `ServiceObjects::Base#messages` collects messages with text, type and optional priority:
396
+
397
+ ```ruby
398
+ message = ServiceObjects::Message.new priority: 0, type: "bar", text: "foo"
399
+ message.priority # => 0.0
400
+ message.type # => "info"
401
+ message.text # => "some text"
402
+ message.to_h # => { type: "bar", type: "foo" }
403
+ message.to_json # => "{\"type\":\"bar\",\"text\":"\foo\"}"
404
+ ```
405
+
406
+ When a priority hasn't been defined explicitly, it is set to `-1.0` for errors, and to `0.0` otherwise. Messages are sorted by priority, type and text in a "natural" order.
407
+
408
+ Use the `#add_message` helper to add a message to the collection:
409
+
410
+ ```ruby
411
+ service = ServiceObjects::Base.new
412
+ service.send :add_message type: "info", text: "some text"
413
+ service.send :messages
414
+ # => [<Message type="info" text="some text" priority=0.0>]
415
+ ```
416
+
417
+ When a `text:` value is a symbol, it is translated in the scope of current service class:
418
+
419
+ ```yaml
420
+ # config/locales/en.yml
421
+ ---
422
+ en:
423
+ activemodel:
424
+ messages:
425
+ models:
426
+ foo:
427
+ excuse: # the type of the message
428
+ not_informed: "I haven't been informed on the %{subject}"
429
+ ```
430
+
431
+ ```ruby
432
+ service.send :add_message type: "excuse", text: :not_informed, subject: "issue"
433
+ # => [<Message text="I haven't been informed on the issue" ...>]
434
+ ```
435
+
436
+ ## Compatibility
437
+
438
+ Tested under MRI rubies >= 2.1
439
+
440
+ RSpec 3.0+ used for testing
441
+
442
+ Collection of testing, debugging and code metrics is defined
443
+ in the [hexx-suit](https://github.com/nepalez/hexx-suit) gem.
444
+
445
+ To run tests use `rake test`, to run code metrics use `rake check`. All the metric settings are collected in the `config/metrics` folder.
446
+
447
+ ## Contributing
448
+
449
+ * Fork the project.
450
+ * Read the [Styleguide](file:config/metrics/STYLEGUIDE).
451
+ * Make your feature addition or bug fix.
452
+ * Add tests for it. This is important so I don't break it in a
453
+ future version unintentionally.
454
+ * Commit, do not mess with Rakefile or version
455
+ (if you want to have your own version, that is fine but bump version
456
+ in a commit by itself I can ignore when I pull)
457
+ * Send me a pull request. Bonus points for topic branches.
458
+
459
+ ## License
460
+
461
+ See [MIT LICENSE][license]