service_objects 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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).