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.
- 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 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
|
-
[![
|
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
|
-
[
|
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).
|