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.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.metrics +1 -0
- data/.travis.yml +9 -1
- data/.yardopts +1 -1
- data/Gemfile +1 -1
- data/Guardfile +29 -8
- data/LICENSE +1 -1
- data/README.md +179 -342
- data/Rakefile +3 -3
- data/config/metrics/churn.yml +1 -1
- data/config/metrics/flay.yml +1 -1
- data/config/metrics/metric_fu.yml +1 -0
- data/config/metrics/rubocop.yml +4 -4
- data/config/metrics/simplecov.yml +1 -1
- data/lib/service_objects.rb +6 -9
- data/lib/service_objects/base.rb +190 -17
- data/lib/service_objects/listener.rb +21 -75
- data/lib/service_objects/message.rb +15 -96
- data/lib/service_objects/version.rb +1 -1
- data/service_objects.gemspec +11 -9
- data/spec/lib/base_spec.rb +247 -0
- data/spec/lib/listener_spec.rb +96 -0
- data/spec/lib/message_spec.rb +48 -0
- data/spec/spec_helper.rb +8 -6
- metadata +56 -93
- data/bin/service +0 -17
- data/config/metrics/pippi.yml +0 -3
- data/lib/service_objects/cli.rb +0 -117
- data/lib/service_objects/cli/locale.erb +0 -20
- data/lib/service_objects/cli/service.erb +0 -125
- data/lib/service_objects/cli/spec.erb +0 -87
- data/lib/service_objects/helpers.rb +0 -17
- data/lib/service_objects/helpers/dependable.rb +0 -63
- data/lib/service_objects/helpers/exceptions.rb +0 -64
- data/lib/service_objects/helpers/messages.rb +0 -95
- data/lib/service_objects/helpers/parameterized.rb +0 -85
- data/lib/service_objects/helpers/parameters.rb +0 -71
- data/lib/service_objects/helpers/validations.rb +0 -54
- data/lib/service_objects/invalid.rb +0 -55
- data/lib/service_objects/null.rb +0 -26
- data/lib/service_objects/parsers.rb +0 -13
- data/lib/service_objects/parsers/dependency.rb +0 -69
- data/lib/service_objects/parsers/notification.rb +0 -85
- data/lib/service_objects/rspec.rb +0 -75
- data/lib/service_objects/utils/normal_hash.rb +0 -34
- data/spec/tests/base_spec.rb +0 -43
- data/spec/tests/bin/service_spec.rb +0 -18
- data/spec/tests/cli_spec.rb +0 -179
- data/spec/tests/helpers/dependable_spec.rb +0 -77
- data/spec/tests/helpers/exceptions_spec.rb +0 -112
- data/spec/tests/helpers/messages_spec.rb +0 -64
- data/spec/tests/helpers/parameterized_spec.rb +0 -136
- data/spec/tests/helpers/parameters_spec.rb +0 -71
- data/spec/tests/helpers/validations_spec.rb +0 -60
- data/spec/tests/invalid_spec.rb +0 -69
- data/spec/tests/listener_spec.rb +0 -73
- data/spec/tests/message_spec.rb +0 -191
- data/spec/tests/null_spec.rb +0 -17
- data/spec/tests/parsers/dependency_spec.rb +0 -29
- data/spec/tests/parsers/notification_spec.rb +0 -84
- data/spec/tests/rspec_spec.rb +0 -86
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ba370e1aeac72b78228f84baf4d3e14e34111df
|
4
|
+
data.tar.gz: 5368bc9082efd433c6c76e522182c91f6c41f8b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 069ac046cf072437fa8d340b0b5a91d39b8d73fd5ab5aed179946d7351400856269e361b49a41a19501a37278903527662731e39c5d42b1bea7d281b12c25b42
|
7
|
+
data.tar.gz: 747521693e08a5644fee7862c4cb076c31d08a0dd967a8abfabbbc900f390b9b979eebf2fd961fc1ac32e6db0ae1d26bcda7477c8a1294a8f08a09cfab6c4631
|
data/.coveralls.yml
CHANGED
data/.metrics
CHANGED
data/.travis.yml
CHANGED
@@ -1,9 +1,17 @@
|
|
1
1
|
---
|
2
2
|
language: ruby
|
3
|
-
bundler_args: --without
|
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
data/Gemfile
CHANGED
data/Guardfile
CHANGED
@@ -2,17 +2,38 @@
|
|
2
2
|
|
3
3
|
guard :rspec, cmd: "bundle exec rspec" do
|
4
4
|
|
5
|
-
|
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
|
-
|
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
|
-
|
10
|
-
|
23
|
+
# runs all the generators' specs
|
24
|
+
watch("lib/service_objects/generators.rb") do
|
25
|
+
"spec/lib/generators"
|
11
26
|
end
|
12
27
|
|
13
|
-
|
28
|
+
# runs all the generators' specs
|
29
|
+
watch(%r{^lib/service_objects/generators}) do
|
30
|
+
"spec/bin"
|
31
|
+
end
|
14
32
|
|
15
|
-
|
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
|
-
|
18
|
-
end
|
39
|
+
end # guard :rspec
|
data/LICENSE
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License
|
2
2
|
|
3
|
-
Copyright (c)
|
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
|
-
|
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]
|
4
11
|
[][travis]
|
5
12
|
[][gemnasium]
|
6
13
|
[][codeclimate]
|
7
14
|
[][coveralls]
|
8
|
-
[][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
|
-
[
|
16
|
-
|
17
|
-
|
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
|
-
|
24
|
+
Base classes for [services] and their listeners following [Publish/Subscribe] design pattern.
|
31
25
|
|
32
|
-
|
33
|
-
|
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
|
-
|
29
|
+
Installation
|
30
|
+
------------
|
37
31
|
|
38
32
|
Add this line to your application's Gemfile:
|
39
33
|
|
40
34
|
```ruby
|
41
|
-
|
35
|
+
# Gemfile
|
36
|
+
gem "service_objects"
|
42
37
|
```
|
43
38
|
|
44
|
-
|
39
|
+
Then execute:
|
45
40
|
|
46
41
|
```
|
47
|
-
|
42
|
+
bundle
|
48
43
|
```
|
49
44
|
|
50
|
-
Or
|
45
|
+
Or add it manually:
|
51
46
|
|
52
47
|
```
|
53
|
-
|
48
|
+
gem install service_objects
|
54
49
|
```
|
55
50
|
|
56
|
-
|
51
|
+
Introduction
|
52
|
+
------------
|
57
53
|
|
58
|
-
The
|
54
|
+
The API contains 3 classes:
|
59
55
|
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
60
|
+
The module is backed on 3 gems:
|
118
61
|
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
131
|
-
|
132
|
-
def on_added(foo, *, messages)
|
133
|
-
render "created", locals: { foo: foo, messages: messages }
|
134
|
-
end
|
71
|
+
Basic Use
|
72
|
+
---------
|
135
73
|
|
136
|
-
|
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
|
-
|
143
|
-
|
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
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
85
|
+
class DeleteFoo < ServiceObjects::Base
|
86
|
+
attribute :id
|
87
|
+
attr_coerced :id, Integer
|
163
88
|
end
|
164
89
|
```
|
165
90
|
|
166
|
-
|
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
|
172
|
-
|
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
|
-
|
181
|
-
|
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
|
-
|
106
|
+
Notice, the dependency is not an attribute. You cannot set it via hash arguments of the service object constructor.
|
184
107
|
|
185
|
-
|
108
|
+
[setter injection]: @todo
|
186
109
|
|
187
|
-
|
110
|
+
Declare validations for attributes and dependencies using the `validate` and `validates` [attestor] methods.
|
188
111
|
|
189
112
|
```ruby
|
190
|
-
class
|
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
|
-
|
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
|
-
|
120
|
+
Define the `#run!` **private** method.
|
207
121
|
|
208
|
-
[
|
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
|
-
|
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
|
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
|
-
|
140
|
+
Notice the `#validate` method (via [attestor]). It publishes `:error` notification when a validation fails.
|
227
141
|
|
228
|
-
|
142
|
+
Call the external service and listen to its notifications with a `#run_service` helper:
|
229
143
|
|
230
144
|
```ruby
|
231
|
-
class
|
145
|
+
class DeleteFoo < ServiceObjects::Base
|
232
146
|
# ...
|
233
|
-
|
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
|
-
|
153
|
+
You should provide a listener for the service. The `ServiceObjects::Listener` method decorates its argument (`self`) with necessary callbacks.
|
242
154
|
|
243
|
-
|
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
|
-
|
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
|
160
|
+
class DeleteFoo < ServiceObjects::Base
|
259
161
|
# ...
|
162
|
+
private
|
260
163
|
|
261
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
298
|
-
|
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
|
-
|
209
|
+
```ruby
|
210
|
+
class DeleteFoo < ServiceObjects::Base
|
303
211
|
|
304
|
-
|
212
|
+
attribute :id, Integer
|
305
213
|
|
306
|
-
|
214
|
+
dependency :get_foo, GetFoo # another service objects class
|
307
215
|
|
308
|
-
|
216
|
+
validate { invalid :blank_id unless id }
|
217
|
+
validate { invalid :get_foo unless get_foo.is_a? ServiceObjects::Base }
|
309
218
|
|
310
|
-
|
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
|
-
|
317
|
-
service = find_foo.new params
|
318
|
-
service.subscribe listener, prefix: :on
|
319
|
-
service.run
|
227
|
+
attr_accessor :foo
|
320
228
|
|
321
|
-
|
322
|
-
|
323
|
-
listener.finalize
|
229
|
+
def find_foo
|
230
|
+
run_service get_foo.new(id: id), Listener.new(self)
|
324
231
|
end
|
325
232
|
|
326
|
-
|
327
|
-
|
328
|
-
|
233
|
+
def delete_foo
|
234
|
+
#... do something
|
235
|
+
publish :deleted, message(:error, :not_found, id: id)
|
329
236
|
end
|
330
237
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
366
|
-
def
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
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
|
-
|
271
|
+
def on_not_found
|
272
|
+
#...
|
273
|
+
end
|
445
274
|
|
446
|
-
|
447
|
-
|
275
|
+
def otherwise
|
276
|
+
#...
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
448
280
|
```
|
449
281
|
|
450
|
-
|
282
|
+
Compatibility
|
283
|
+
-------------
|
451
284
|
|
452
|
-
Tested under
|
285
|
+
Tested under rubies compatible to rubies compatible to API 2.0+:
|
453
286
|
|
454
|
-
|
287
|
+
* MRI 2.0+
|
288
|
+
* Rubinius (mode 2.0+)
|
289
|
+
* JRuby 9.0.0.0 (mode 2.0+)
|
455
290
|
|
456
|
-
|
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
|
-
|
293
|
+
[RSpec]: http://rspec.info/
|
294
|
+
[hexx-suit]: http://github.com/nepalez/hexx-suit
|
460
295
|
|
461
|
-
|
296
|
+
Contributing
|
297
|
+
------------
|
462
298
|
|
463
299
|
* Fork the project.
|
464
|
-
* Read the [
|
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
|
-
|
309
|
+
License
|
310
|
+
-------
|
474
311
|
|
475
|
-
See [MIT LICENSE]
|
312
|
+
See the [MIT LICENSE](LICENSE).
|