service_objects 0.1.0 → 1.0.0

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -0
  3. data/.metrics +1 -0
  4. data/.travis.yml +9 -1
  5. data/.yardopts +1 -1
  6. data/Gemfile +1 -1
  7. data/Guardfile +29 -8
  8. data/LICENSE +1 -1
  9. data/README.md +179 -342
  10. data/Rakefile +3 -3
  11. data/config/metrics/churn.yml +1 -1
  12. data/config/metrics/flay.yml +1 -1
  13. data/config/metrics/metric_fu.yml +1 -0
  14. data/config/metrics/rubocop.yml +4 -4
  15. data/config/metrics/simplecov.yml +1 -1
  16. data/lib/service_objects.rb +6 -9
  17. data/lib/service_objects/base.rb +190 -17
  18. data/lib/service_objects/listener.rb +21 -75
  19. data/lib/service_objects/message.rb +15 -96
  20. data/lib/service_objects/version.rb +1 -1
  21. data/service_objects.gemspec +11 -9
  22. data/spec/lib/base_spec.rb +247 -0
  23. data/spec/lib/listener_spec.rb +96 -0
  24. data/spec/lib/message_spec.rb +48 -0
  25. data/spec/spec_helper.rb +8 -6
  26. metadata +56 -93
  27. data/bin/service +0 -17
  28. data/config/metrics/pippi.yml +0 -3
  29. data/lib/service_objects/cli.rb +0 -117
  30. data/lib/service_objects/cli/locale.erb +0 -20
  31. data/lib/service_objects/cli/service.erb +0 -125
  32. data/lib/service_objects/cli/spec.erb +0 -87
  33. data/lib/service_objects/helpers.rb +0 -17
  34. data/lib/service_objects/helpers/dependable.rb +0 -63
  35. data/lib/service_objects/helpers/exceptions.rb +0 -64
  36. data/lib/service_objects/helpers/messages.rb +0 -95
  37. data/lib/service_objects/helpers/parameterized.rb +0 -85
  38. data/lib/service_objects/helpers/parameters.rb +0 -71
  39. data/lib/service_objects/helpers/validations.rb +0 -54
  40. data/lib/service_objects/invalid.rb +0 -55
  41. data/lib/service_objects/null.rb +0 -26
  42. data/lib/service_objects/parsers.rb +0 -13
  43. data/lib/service_objects/parsers/dependency.rb +0 -69
  44. data/lib/service_objects/parsers/notification.rb +0 -85
  45. data/lib/service_objects/rspec.rb +0 -75
  46. data/lib/service_objects/utils/normal_hash.rb +0 -34
  47. data/spec/tests/base_spec.rb +0 -43
  48. data/spec/tests/bin/service_spec.rb +0 -18
  49. data/spec/tests/cli_spec.rb +0 -179
  50. data/spec/tests/helpers/dependable_spec.rb +0 -77
  51. data/spec/tests/helpers/exceptions_spec.rb +0 -112
  52. data/spec/tests/helpers/messages_spec.rb +0 -64
  53. data/spec/tests/helpers/parameterized_spec.rb +0 -136
  54. data/spec/tests/helpers/parameters_spec.rb +0 -71
  55. data/spec/tests/helpers/validations_spec.rb +0 -60
  56. data/spec/tests/invalid_spec.rb +0 -69
  57. data/spec/tests/listener_spec.rb +0 -73
  58. data/spec/tests/message_spec.rb +0 -191
  59. data/spec/tests/null_spec.rb +0 -17
  60. data/spec/tests/parsers/dependency_spec.rb +0 -29
  61. data/spec/tests/parsers/notification_spec.rb +0 -84
  62. data/spec/tests/rspec_spec.rb +0 -86
  63. data/spec/tests/utils/normal_hash_spec.rb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e743a38993231e20c909e567e8f743b43527ea3f
4
- data.tar.gz: 491a38180eccf6a4b8b884d23c6a9d9b84064ab4
3
+ metadata.gz: 5ba370e1aeac72b78228f84baf4d3e14e34111df
4
+ data.tar.gz: 5368bc9082efd433c6c76e522182c91f6c41f8b5
5
5
  SHA512:
6
- metadata.gz: fed24fa452deb99ef88a2a8325cc01fea50b91169578b1189c1634a8abcaab984f9d0594caa803feed6fbfb727a59028431b13d8d38791ad0a3d4e1c10154567
7
- data.tar.gz: 20c5f8aa9fbca82761bd42df214cb95404c336f16dc3fce588b3d4396c589241b1444b5519d6b82e3280f62c211ab5f49785d7113c8a89a61bc38a4b2a3a83e5
6
+ metadata.gz: 069ac046cf072437fa8d340b0b5a91d39b8d73fd5ab5aed179946d7351400856269e361b49a41a19501a37278903527662731e39c5d42b1bea7d281b12c25b42
7
+ data.tar.gz: 747521693e08a5644fee7862c4cb076c31d08a0dd967a8abfabbbc900f390b9b979eebf2fd961fc1ac32e6db0ae1d26bcda7477c8a1294a8f08a09cfab6c4631
@@ -1 +1,2 @@
1
+ ---
1
2
  service_name: travis-ci
data/.metrics CHANGED
@@ -5,4 +5,5 @@ begin
5
5
  require "hexx-suit"
6
6
  Hexx::Suit::Metrics::MetricFu.load
7
7
  rescue LoadError
8
+ puts "The 'hexx-suit' gem is not installed"
8
9
  end
@@ -1,9 +1,17 @@
1
1
  ---
2
2
  language: ruby
3
- bundler_args: --without metrics
3
+ bundler_args: --without=metrics
4
4
  script: rake test:coverage:run
5
5
  rvm:
6
6
  - '2.0'
7
7
  - '2.1'
8
8
  - '2.2'
9
9
  - ruby-head
10
+ - rbx-2 --2.0
11
+ - rbx-head
12
+ - jruby-9.0.0.0.pre1
13
+ - jruby-head
14
+ allow_failures:
15
+ - rvm: ruby-head
16
+ - rvm: rbx-head
17
+ - rvm: jruby-head
data/.yardopts CHANGED
@@ -1,3 +1,3 @@
1
1
  --asset LICENSE
2
2
  --exclude lib/service_objects/version.rb
3
- --out doc/api
3
+ --output doc/api
data/Gemfile CHANGED
@@ -2,4 +2,4 @@ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
- gem "hexx-suit", "~> 2.0", group: :metrics if RUBY_ENGINE == "ruby"
5
+ gem "hexx-suit", "~> 2.2", group: :metrics if RUBY_ENGINE == "ruby"
data/Guardfile CHANGED
@@ -2,17 +2,38 @@
2
2
 
3
3
  guard :rspec, cmd: "bundle exec rspec" do
4
4
 
5
- watch("lib/service_objects.rb") { "spec" }
5
+ # runs current spec
6
+ watch(%r{^spec/.+_spec\.rb$})
7
+
8
+ # runs a bin's spec
9
+ watch(%r{^bin/(.+)$}) do |m|
10
+ "spec/bin/#{ m[1] }_spec.rb"
11
+ end
12
+
13
+ # runs a lib's spec
14
+ watch(%r{^lib/service_objects/(\w+)\.rb$}) do |m|
15
+ "spec/lib/#{ m[1] }_spec.rb"
16
+ end
6
17
 
7
- watch("lib/service_objects/cli/*.*") { "spec/tests/cli_spec.rb" }
18
+ # runs a generator's spec when the generator or its template changed
19
+ watch(%r{^lib/service_objects/generators/(\w+)}) do |m|
20
+ "spec/lib/generators/#{ m[1] }_spec.rb"
21
+ end
8
22
 
9
- watch(%r{^lib/service_objects/(.+)\.rb$}) do |m|
10
- "spec/tests/#{ m[1] }_spec.rb"
23
+ # runs all the generators' specs
24
+ watch("lib/service_objects/generators.rb") do
25
+ "spec/lib/generators"
11
26
  end
12
27
 
13
- watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
28
+ # runs all the generators' specs
29
+ watch(%r{^lib/service_objects/generators}) do
30
+ "spec/bin"
31
+ end
14
32
 
15
- watch(/^spec\/spec_helper\w*\.rb$/) { "spec" }
33
+ # runs all specs when core files changed
34
+ watch("lib/service_objects.rb") { "spec" }
35
+ watch("spec/spec_helper.rb") { "spec" }
36
+ watch(%r{^spec/support}) { "spec" }
37
+ watch(/^\w+$/) { "spec" }
16
38
 
17
- watch(%r{^spec/tests/.+_spec\.rb$})
18
- end
39
+ end # guard :rspec
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License
2
2
 
3
- Copyright (c) 2014 Andrew Kozin, https://github.com/nepalez
3
+ Copyright (c) 2015 Andrew Kozin (nepalez), andrew.kozin@gmail.com
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,467 +1,303 @@
1
- # ServiceObjects
1
+ Service Objects
2
+ ===============
3
+
4
+ **The version 1.0 has been re-written from scratch without ActiveModel dependency.**
5
+
6
+ **See v0.0.1 at the [legacy] git branch**
7
+
8
+ [legacy]: https://github.com/nepalez/service_objects/tree/legacy
2
9
 
3
10
  [![Gem Version](https://img.shields.io/gem/v/service_objects.svg?style=flat)][gem]
4
11
  [![Build Status](https://img.shields.io/travis/nepalez/service_objects/master.svg?style=flat)][travis]
5
12
  [![Dependency Status](https://img.shields.io/gemnasium/nepalez/service_objects.svg?style=flat)][gemnasium]
6
13
  [![Code Climate](https://img.shields.io/codeclimate/github/nepalez/service_objects.svg?style=flat)][codeclimate]
7
14
  [![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]
15
+ [![Inline docs](http://inch-ci.org/github/nepalez/service_objects.svg)][inch]
9
16
 
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
17
  [codeclimate]: https://codeclimate.com/github/nepalez/service_objects
14
18
  [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
19
+ [gem]: https://rubygems.org/gems/service_objects
20
+ [gemnasium]: https://gemnasium.com/nepalez/service_objects
21
+ [travis]: https://travis-ci.org/nepalez/service_objects
22
+ [inch]: https://inch-ci.org/github/nepalez/service_objects
29
23
 
30
- The module API provides 3 classes:
24
+ Base classes for [services] and their listeners following [Publish/Subscribe] design pattern.
31
25
 
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.
26
+ [services]: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
27
+ [Publish/Subscribe]: http://reefpoints.dockyard.com/2013/08/20/design-patterns-observer-pattern.html
35
28
 
36
- ## Installation
29
+ Installation
30
+ ------------
37
31
 
38
32
  Add this line to your application's Gemfile:
39
33
 
40
34
  ```ruby
41
- gem "service_objects"
35
+ # Gemfile
36
+ gem "service_objects"
42
37
  ```
43
38
 
44
- And then execute:
39
+ Then execute:
45
40
 
46
41
  ```
47
- bundle
42
+ bundle
48
43
  ```
49
44
 
50
- Or install it yourself as:
45
+ Or add it manually:
51
46
 
52
47
  ```
53
- gem install service_objects
48
+ gem install service_objects
54
49
  ```
55
50
 
56
- ## Usage
51
+ Introduction
52
+ ------------
57
53
 
58
- The basic usage of the services by example of Rails controller:
54
+ The API contains 3 classes:
59
55
 
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
56
+ * `ServiceObjects::Base` - for service objects.
57
+ * `ServiceObjects::Listener` - for service objects' listeners.
58
+ * `ServiceObjects::Message` - for messages published by service objects.
116
59
 
117
- private
60
+ The module is backed on 3 gems:
118
61
 
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
62
+ * [wisper] that provides [Publish/Subscribe] features of the service;
63
+ * [virtus] for service object's attributes and dependencies declaration;
64
+ * [attestor] for object's validation.
125
65
 
126
- # The class to work out service object notifications
127
- # The #render method is delegated to the controller
128
- class FoosListener < ServiceObjects::Listener
66
+ [wisper]: https://github.com/krisleech/wisper
67
+ [virtus]: https://github.com/solnic/virtus
68
+ [attestor]: https://github.com/nepalez/attestor
69
+ [attr_coerced]: https://github.com/nepalez/attr_coerced
129
70
 
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
71
+ Basic Use
72
+ ---------
135
73
 
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
74
+ Define the service object and describe its attributes (via [virtus]).
141
75
 
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
76
+ ```ruby
77
+ class DeleteFoo < ServiceObjects::Base
78
+ attribute :id, Integer
150
79
  end
151
80
  ```
152
81
 
153
- The service can notify several listeners (controller itself, mailer etc.).
154
-
155
- ## Base
156
-
157
- The `ServiceObjects::Base` provides base class for services.
82
+ You can also use another coersion mechanism (via [attr_coerced]).
158
83
 
159
84
  ```ruby
160
- require "service_objects"
161
-
162
- class AddFoo < ServiceObjects::Base
85
+ class DeleteFoo < ServiceObjects::Base
86
+ attribute :id
87
+ attr_coerced :id, Integer
163
88
  end
164
89
  ```
165
90
 
166
- ### Parameters declaration
167
-
168
- Define allowed parameters for objects:
91
+ Declare dependencies from other service objects. This allows [setter injection] of dependencies in a runtime (for example, in unit tests).
169
92
 
170
93
  ```ruby
171
- class AddFoo < ServiceObjects::Base
172
- allows_params :bar, :baz
94
+ class DeleteFoo < ServiceObjects::Base
95
+ # ...
96
+ dependency :get_foo, default: GetFoo # another service objects class
173
97
  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
98
 
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.
99
+ # This defines an attribute of Class type
100
+ service = DeleteFoo.new
101
+ service.get_foo # => GetFoo
102
+ service.get_foo = FindFoo
103
+ service.get_foo # => FindFoo
104
+ ```
182
105
 
183
- ### Validation
106
+ Notice, the dependency is not an attribute. You cannot set it via hash arguments of the service object constructor.
184
107
 
185
- The `ServiceObject::Base` includes [ActiveModel::Validations] with methods `.validates`, `.validate`, `#errors`, `#valid?` and `#invalid?`. Use them to add action context - specific validations.
108
+ [setter injection]: @todo
186
109
 
187
- The method `#validate!` raises the `ServiceObject::Invalid` if validation fails.
110
+ Declare validations for attributes and dependencies using the `validate` and `validates` [attestor] methods.
188
111
 
189
112
  ```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
-
113
+ class DeleteFoo < ServiceObjects::Base
196
114
  # ...
197
-
198
- def run!
199
- # ...
200
- validate!
201
- # ...
202
- end
115
+ validate { invalid :blank_id unless id }
116
+ validate { invalid :get_foo unless get_foo.is_a? ServiceObjects::Base }
203
117
  end
204
118
  ```
205
119
 
206
- **Note:** You aren't restricted in selecting time for validation. Prepare attributes (either "real" or [virtual]) and run `#validate!` when necessary.
120
+ Define the `#run!` **private** method.
207
121
 
208
- [ActiveModel::Validations]: http://api.rubyonrails.org/classes/ActiveModel/Validations.html
209
- [virtual]: http://railscasts.com/episodes/16-virtual-attributes?view=asciicast
122
+ It is expected the method to publish notifications to listeners (via [wisper]). The [wisper] `#publish` method is reloaded so that after publication it throws `:published` to be caught by public `#run` method. That's how `#publish` stops running the service.
210
123
 
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:
124
+ If you need to publish something and keep running, use the [wisper] `#broadcast` method instead of the `#publish`.
216
125
 
217
126
  ```ruby
218
- class AddFoo < ServiceObjects::Base
127
+ class DeleteFoo < ServiceObjects::Base
219
128
  # ...
129
+ private
130
+
131
+ def run!
132
+ validate
133
+ find_foo
134
+ delete_foo
135
+ end
220
136
 
221
- # Providing the FindFoo is available at the moment AddFoo being defined:
222
- depends_on :find_foo, default: FindFoo
223
137
  end
224
138
  ```
225
139
 
226
- Default value can be either assigned or skipped. In the last case the [Null Object] will be assigned by default.
140
+ Notice the `#validate` method (via [attestor]). It publishes `:error` notification when a validation fails.
227
141
 
228
- The class method is public to postpone the default implementation until it is defined:
142
+ Call the external service and listen to its notifications with a `#run_service` helper:
229
143
 
230
144
  ```ruby
231
- class AddFoo < ServiceObjects::Base
145
+ class DeleteFoo < ServiceObjects::Base
232
146
  # ...
233
- depends_on :find_foo
147
+ def find_foo
148
+ run_service get_foo.new(id: id), Listener.new(self)
149
+ end
234
150
  end
235
-
236
- # later
237
- FindFoo = Class.new
238
- AddFoo.depends_on :find_foo, default: FindFoo
239
151
  ```
240
152
 
241
- This provides the instance attribute `#find_foo`. You can inject the dependency to the via setter:
153
+ You should provide a listener for the service. The `ServiceObjects::Listener` method decorates its argument (`self`) with necessary callbacks.
242
154
 
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.
155
+ It should also define the `#otherwise` callback for the case when an external service publishes no expected notifications.
254
156
 
255
- See [wisper] for details on `#publish` and `#subscribe` methods.
157
+ All undefined methods are forwarded to the listener argument, so you can modify attributes of mutable services from their listeners.
256
158
 
257
159
  ```ruby
258
- class AddFoo < ServiceObjects::Base
160
+ class DeleteFoo < ServiceObjects::Base
259
161
  # ...
162
+ private
260
163
 
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
164
+ attr_accessor :foo
273
165
 
274
- # ...
166
+ class Listener < ServiceObjects::Listener
167
+ def on_found(foo)
168
+ self.foo = foo
169
+ end
275
170
 
276
- private
171
+ # the method will be called by #run_service
172
+ # unless #on_found received
173
+ def otherwise
174
+ publish :not_found, message(:error, :not_found, id: id)
175
+ end
176
+ end
177
+ end
178
+ ```
277
179
 
278
- Found = Class.new(RuntimeError) # the internal message
180
+ The `#message` helper returns a translated message with 2 attributes: `type` and `text`. When the text is set as a `Symbol`, it is translated by `I18n` with given options.
279
181
 
280
- # Business logic lives here
281
- def run!
282
- get_foo
283
- create_foo
284
- end
182
+ ```yaml
183
+ # config/locales/en.yml
184
+ ---
185
+ en:
186
+ service_objects:
187
+ delete_foo: # the service class name like ActiveModule's one
188
+ error: # the type of the message
189
+ not_found: "The foo with id %{id} hasn't been found"
190
+ success:
191
+ deleted: "The foo with id %{id} has been deleted"
192
+ ```
285
193
 
286
- def get_foo
287
- # ... finds and assigns @foo somehow
288
- fail Found if @foo
289
- end
194
+ It is also recommended to publish successful message for the service not to end up silently:
290
195
 
291
- def add_foo
292
- # ...
196
+ ```ruby
197
+ class DeleteFoo < ServiceObjects::Base
198
+ # ...
199
+ def delete_foo
200
+ # do something to delete foo
201
+ publish :deleted, foo, message(:success, :deleted, id: id)
293
202
  end
294
203
  end
295
204
  ```
296
205
 
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`.
206
+ Full Example
207
+ ----------------
301
208
 
302
- **Note** Following [command-query separation] the `#run` method (being a command) returns `self`.
209
+ ```ruby
210
+ class DeleteFoo < ServiceObjects::Base
303
211
 
304
- [command-query separation]: http://en.wikipedia.org/wiki/Command-query_separation
212
+ attribute :id, Integer
305
213
 
306
- ### External services
214
+ dependency :get_foo, GetFoo # another service objects class
307
215
 
308
- External services should be used in just the same way as in the controller example.
216
+ validate { invalid :blank_id unless id }
217
+ validate { invalid :get_foo unless get_foo.is_a? ServiceObjects::Base }
309
218
 
310
- ```ruby
311
- class AddFoo < ServiceObjects::Base
312
- depends_on :find_foo, default: FindFoo
219
+ private
313
220
 
314
- # ...
221
+ def run!
222
+ validate # publishes :error
223
+ find_foo # publishes :not_found
224
+ delete_foo # publishes :deleted
225
+ end
315
226
 
316
- def get_foo
317
- service = find_foo.new params
318
- service.subscribe listener, prefix: :on
319
- service.run
227
+ attr_accessor :foo
320
228
 
321
- # the method runs #otherwise callback in case
322
- # no other notificaton has been received
323
- listener.finalize
229
+ def find_foo
230
+ run_service get_foo.new(id: id), Listener.new(self)
324
231
  end
325
232
 
326
- # decorates the service with methods to listen to external service
327
- def listener
328
- @listener ||= FindListener.new self
233
+ def delete_foo
234
+ #... do something
235
+ publish :deleted, message(:error, :not_found, id: id)
329
236
  end
330
237
 
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
238
+ # @private
239
+ class Listener < ServiceObjects::Listener
240
+ def on_found(foo)
241
+ self.foo = foo
338
242
  end
339
243
 
340
244
  def otherwise
341
- add_message "complain", "haven't been informed"
245
+ publish :not_found, message(:error, :not_found, id: id)
342
246
  end
343
247
  end
344
248
  end
345
249
  ```
346
250
 
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
251
 
349
- ```ruby
350
- # ...
351
- def get_foo
352
- run_service find_foo.new(params), listener, prefix: :on
353
- end
354
- ```
355
-
356
- ## Listener
252
+ Calling the Service
253
+ -------------------
357
254
 
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).
255
+ The service can be called like any other [wisper] publisher. For example, from the Rails controller:
363
256
 
364
257
  ```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"
258
+ class FooController < ActionController::Base
259
+ def delete
260
+ listener = DeleteListener.new(self)
261
+ service = DeleteFoo.new(id: params[:id])
262
+ service.subscribe(listener, prefix: :on)
263
+ service.run
372
264
  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
265
 
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
- ## Scaffolding
437
-
438
- Use CLI command to scaffold a service object with its specification and translations:
439
-
440
- ```
441
- service new my_service
442
- ```
266
+ class DeleteListener < ServiceObjects::Listener
267
+ def on_deleted
268
+ redirect_to :home
269
+ end
443
270
 
444
- To see available options run the command with `-h` option:
271
+ def on_not_found
272
+ #...
273
+ end
445
274
 
446
- ```
447
- service new -h
275
+ def otherwise
276
+ #...
277
+ end
278
+ end
279
+ end
448
280
  ```
449
281
 
450
- ## Compatibility
282
+ Compatibility
283
+ -------------
451
284
 
452
- Tested under MRI rubies >= 2.1
285
+ Tested under rubies compatible to rubies compatible to API 2.0+:
453
286
 
454
- RSpec 3.0+ used for testing
287
+ * MRI 2.0+
288
+ * Rubinius (mode 2.0+)
289
+ * JRuby 9.0.0.0 (mode 2.0+)
455
290
 
456
- Collection of testing, debugging and code metrics is defined
457
- in the [hexx-suit](https://github.com/nepalez/hexx-suit) gem.
291
+ Uses [RSpec] 3.0+ for testing and [hexx-suit] for dev/test tools collection.
458
292
 
459
- To run tests use `rake test`, to run code metrics use `rake check`. All the metric settings are collected in the `config/metrics` folder.
293
+ [RSpec]: http://rspec.info/
294
+ [hexx-suit]: http://github.com/nepalez/hexx-suit
460
295
 
461
- ## Contributing
296
+ Contributing
297
+ ------------
462
298
 
463
299
  * Fork the project.
464
- * Read the [Styleguide](file:config/metrics/STYLEGUIDE).
300
+ * Read the [STYLEGUIDE](config/metrics/STYLEGUIDE).
465
301
  * Make your feature addition or bug fix.
466
302
  * Add tests for it. This is important so I don't break it in a
467
303
  future version unintentionally.
@@ -470,6 +306,7 @@ To run tests use `rake test`, to run code metrics use `rake check`. All the metr
470
306
  in a commit by itself I can ignore when I pull)
471
307
  * Send me a pull request. Bonus points for topic branches.
472
308
 
473
- ## License
309
+ License
310
+ -------
474
311
 
475
- See [MIT LICENSE][license]
312
+ See the [MIT LICENSE](LICENSE).