services 1.3.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Guardfile +22 -0
- data/README.md +9 -0
- data/lib/services/base_finder.rb +2 -0
- data/lib/services/modules/call_logger.rb +30 -19
- data/lib/services/modules/uniqueness_checker.rb +1 -1
- data/lib/services/version.rb +1 -1
- data/spec/services/base_finder_spec.rb +7 -0
- data/spec/services/modules/call_logger_spec.rb +106 -35
- data/spec/services/modules/exception_wrapper_spec.rb +1 -1
- data/spec/spec_helper.rb +0 -1
- data/spec/support/test_services.rb +40 -4
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b511fc23143b4bd0ddf9eef2574f76f3c0bf12cf
|
4
|
+
data.tar.gz: f34f312482dc3e0025b98d3acf0dae0c92d45275
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c7f9e9850c3435b6e525dc7708521bef50682544088b93afa8fb81554730b4b7faf9d8d92a71eb06ad56b4a2c55ef8e9a404055dd3b5161e40daa5f0e571fa54
|
7
|
+
data.tar.gz: ee3ec81535144e849340bc75b037c829731da0edc1bebe0ac666704dbd8c384be2fb5daf8f51c65251851e30c7222f8cd68c109cc5a8f90ef79c9be89c03c8e3
|
data/Guardfile
CHANGED
@@ -1,4 +1,26 @@
|
|
1
|
+
class Guard::StartRedisAndSidekiq
|
2
|
+
def call(guard_class, event, *args)
|
3
|
+
puts guard_class
|
4
|
+
puts event
|
5
|
+
puts args
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Guard::StopRedisAndSidekiq
|
10
|
+
def call(guard_class, event, *args)
|
11
|
+
sleep 5
|
12
|
+
puts guard_class
|
13
|
+
puts event
|
14
|
+
puts args
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
1
18
|
guard 'rspec', cmd: 'bundle exec rspec' do
|
19
|
+
# callback StartRedisAndSidekiq.new, :start_begin
|
20
|
+
# callback StopRedisAndSidekiq.new, :stop
|
21
|
+
# callback StopRedisAndSidekiq.new, :stop_begin
|
22
|
+
# callback StopRedisAndSidekiq.new, :stop_end
|
23
|
+
|
2
24
|
# Specs
|
3
25
|
watch(%r(^spec/.+_spec\.rb$))
|
4
26
|
watch('spec/spec_helper.rb') { 'spec' }
|
data/README.md
CHANGED
@@ -39,6 +39,15 @@ Follow these conventions that Services expects/recommends:
|
|
39
39
|
* services are namespaced with the model they operate on and their names are verbs, e.g. `app/services/users/delete.rb` defines `Services::Users::Delete`. If a service operates on multiple models or no models at all, don't namespace them (`Services::DoLotsOfStuff`) or namespace them by logical groups unrelated to models (`Services::Maintenance::CleanOldUsers`, `Services::Maintenance::SendDailySummary`, etc.)
|
40
40
|
* Sometimes services must call other services. Try to not combine multiple calls to other services and business logic in one service. Instead, some services should contain only business logic and other services only a bunch of service calls but no (or little) business logic. This keeps your services nice and modular.
|
41
41
|
|
42
|
+
### Rails autoload fix
|
43
|
+
|
44
|
+
By default, Rails expects `app/services/users/delete.rb` to define `Users::Delete`, but we want it to expect `Services::Users::Delete`. To make this work, add the `app` folder to the autoload path:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
# config/application.rb
|
48
|
+
config.autoload_paths += [config.root.join('app')]
|
49
|
+
```
|
50
|
+
|
42
51
|
### Dependence
|
43
52
|
|
44
53
|
To process services in the background, Services uses [Sidekiq](https://github.com/mperham/sidekiq). Sidekiq is not absolutely required to use Services though, if it's not present, a service will raise an exception when you try to enqueue it for background processing. If you're using Sidekiq, make sure to load the Services gem after the Sidekiq gem.
|
data/lib/services/base_finder.rb
CHANGED
@@ -1,41 +1,52 @@
|
|
1
1
|
module Services
|
2
2
|
class Base
|
3
3
|
module CallLogger
|
4
|
+
def self.prepended(mod)
|
5
|
+
mod.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
attr_accessor :call_logging_disabled
|
10
|
+
|
11
|
+
def disable_call_logging
|
12
|
+
@call_logging_disabled = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def enable_call_logging
|
16
|
+
@call_logging_disabled = false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
4
20
|
def call(*args)
|
5
21
|
return super if Services.configuration.logger.nil?
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
22
|
+
unless self.class.call_logging_disabled
|
23
|
+
log "START with args: #{args}", caller: caller
|
24
|
+
start = Time.now
|
25
|
+
end
|
10
26
|
begin
|
11
27
|
result = super
|
12
28
|
rescue => e
|
13
|
-
|
29
|
+
log exception_message(e), {}, 'error'
|
14
30
|
raise e
|
15
31
|
ensure
|
16
|
-
log
|
32
|
+
log 'END', duration: (Time.now - start).round(2) unless self.class.call_logging_disabled
|
17
33
|
result
|
18
34
|
end
|
19
35
|
end
|
20
36
|
|
21
37
|
private
|
22
38
|
|
23
|
-
def log(message, severity = 'info')
|
24
|
-
Services.configuration.logger.log message,
|
39
|
+
def log(message, meta = {}, severity = 'info')
|
40
|
+
Services.configuration.logger.log message, meta.merge(service: self.class.to_s, id: @id), severity
|
25
41
|
end
|
26
42
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
log " #{line}"
|
32
|
-
end
|
33
|
-
log_exception(e.cause, true)
|
34
|
-
else
|
35
|
-
e.backtrace.each do |line|
|
36
|
-
log " #{line}"
|
37
|
-
end
|
43
|
+
def exception_message(e)
|
44
|
+
message = "#{e.class}: #{e.message}"
|
45
|
+
e.backtrace.each do |line|
|
46
|
+
message << "\n #{line}"
|
38
47
|
end
|
48
|
+
message << "\ncaused by: #{exception_message(e.cause)}" if e.respond_to?(:cause) && e.cause
|
49
|
+
message
|
39
50
|
end
|
40
51
|
|
41
52
|
def caller
|
@@ -19,7 +19,7 @@ module Services
|
|
19
19
|
mod.const_set :NotUniqueError, Class.new(mod::Error)
|
20
20
|
end
|
21
21
|
|
22
|
-
def check_uniqueness
|
22
|
+
def check_uniqueness(*args, on_error: :fail)
|
23
23
|
raise "on_error must be one of #{ON_ERROR.join(', ')}, but was #{on_error}" unless ON_ERROR.include?(on_error.to_sym)
|
24
24
|
raise 'Service args not found.' if @service_args.nil?
|
25
25
|
@uniqueness_args = args.empty? ? @service_args : args
|
data/lib/services/version.rb
CHANGED
@@ -1,54 +1,125 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Services::Base::CallLogger do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
let(:logger) { spy('logger') }
|
5
|
+
|
6
|
+
before do
|
7
|
+
Services.configuration.logger = logger
|
8
|
+
@logs = []
|
9
|
+
allow(logger).to receive(:log) do |message, meta, severity|
|
10
|
+
@logs << {
|
11
|
+
message: message,
|
12
|
+
meta: meta,
|
13
|
+
severity: severity
|
14
|
+
}
|
9
15
|
end
|
10
|
-
service.call 'foo', 'bar'
|
11
|
-
expect(logs.first).to eq('START with args ["foo", "bar"]')
|
12
|
-
expect(logs.last).to eq('END after 0.0 seconds')
|
13
16
|
end
|
14
17
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
after do
|
19
|
+
Services.configuration.logger = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'when call logging is enabled' do
|
23
|
+
it 'logs start and end' do
|
24
|
+
service, args = EmptyService.new, %w(foo bar)
|
25
|
+
service.call *args
|
26
|
+
caller_regex = /\A#{Regexp.escape __FILE__}:\d+\z/
|
27
|
+
expect(@logs.first).to match(
|
28
|
+
message: "START with args: #{args}",
|
29
|
+
meta: {
|
30
|
+
caller: a_string_matching(caller_regex),
|
31
|
+
service: service.class.to_s,
|
32
|
+
id: an_instance_of(String)
|
33
|
+
},
|
34
|
+
severity: 'info'
|
35
|
+
)
|
36
|
+
expect(@logs.last).to match(
|
37
|
+
message: 'END',
|
38
|
+
meta: {
|
39
|
+
duration: 0.0,
|
40
|
+
service: service.class.to_s,
|
41
|
+
id: an_instance_of(String)
|
42
|
+
},
|
43
|
+
severity: 'info'
|
44
|
+
)
|
20
45
|
end
|
21
46
|
|
22
|
-
|
23
|
-
|
24
|
-
|
47
|
+
describe 'logging the caller' do
|
48
|
+
let(:service_calling_service) { ServiceCallingService.new }
|
49
|
+
let(:called_service) { EmptyService.new }
|
50
|
+
|
51
|
+
it 'filters out caller paths from lib folder' do
|
52
|
+
require 'services/call_proxy'
|
53
|
+
Services::CallProxy.call(called_service, :call)
|
54
|
+
caller_regex = /\A#{Regexp.escape __FILE__}:\d+/
|
55
|
+
expect(
|
56
|
+
@logs.detect do |log|
|
57
|
+
log[:meta][:caller] =~ caller_regex
|
58
|
+
end
|
59
|
+
).to be_present
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'when Rails is not defined' do
|
63
|
+
it 'logs the complete caller path' do
|
64
|
+
service_calling_service.call called_service
|
65
|
+
caller_regex = /\A#{Regexp.escape PROJECT_ROOT.join(TEST_SERVICES_PATH).to_s}:\d+/
|
66
|
+
expect(
|
67
|
+
@logs.detect do |log|
|
68
|
+
log[:meta][:caller] =~ caller_regex
|
69
|
+
end
|
70
|
+
).to be_present
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'when Rails is defined' do
|
75
|
+
before do
|
76
|
+
class Rails
|
77
|
+
def self.root
|
78
|
+
PROJECT_ROOT
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
after do
|
84
|
+
Object.send :remove_const, :Rails
|
85
|
+
end
|
25
86
|
|
26
|
-
|
27
|
-
|
28
|
-
|
87
|
+
it 'logs the caller path relative to `Rails.root`' do
|
88
|
+
service_calling_service.call called_service
|
89
|
+
caller_regex = /\A#{Regexp.escape TEST_SERVICES_PATH.to_s}:\d+/
|
90
|
+
expect(
|
91
|
+
@logs.detect do |log|
|
92
|
+
log[:meta][:caller] =~ caller_regex
|
93
|
+
end
|
94
|
+
).to be_present
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'when call logging is disabled' do
|
101
|
+
it 'does not log start and end' do
|
102
|
+
expect { EmptyServiceWithoutCallLogging.call }.to_not change { @logs }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'logs exceptions' do
|
107
|
+
[ErrorService, ErrorServiceWithoutCallLogging].each do |klass|
|
108
|
+
expect { klass.call rescue nil }.to change { @logs }
|
29
109
|
end
|
30
|
-
logs = []
|
31
|
-
service_calling_service.call called_service
|
32
|
-
expect(logs).to include(/\ACALLED BY #{Regexp.escape TEST_SERVICES_PATH.to_s}:\d+/)
|
33
|
-
Object.send :remove_const, :Rails
|
34
|
-
|
35
|
-
# Caller paths from services lib folder should be filtered
|
36
|
-
require 'services/call_proxy'
|
37
|
-
logs = []
|
38
|
-
Services::CallProxy.call(called_service, :call)
|
39
|
-
expect(logs).to include(/\ACALLED BY #{Regexp.escape __FILE__}:\d+/)
|
40
110
|
end
|
41
111
|
|
42
112
|
if RUBY_VERSION > '2.1'
|
43
|
-
it 'logs
|
113
|
+
it 'logs exception causes' do
|
44
114
|
service = NestedExceptionService.new
|
45
|
-
logs = []
|
46
|
-
allow(service).to receive(:log) do |message, *|
|
47
|
-
logs << message
|
48
|
-
end
|
49
115
|
expect { service.call }.to raise_error(service.class::Error)
|
50
116
|
%w(NestedError1 NestedError2).each do |error|
|
51
|
-
|
117
|
+
message_regex = /caused by: #{service.class}::#{error}/
|
118
|
+
expect(
|
119
|
+
@logs.detect do |log|
|
120
|
+
log[:message] =~ message_regex
|
121
|
+
end
|
122
|
+
).to be_present
|
52
123
|
end
|
53
124
|
end
|
54
125
|
end
|
@@ -7,7 +7,7 @@ describe Services::Base::ExceptionWrapper do
|
|
7
7
|
ErrorService.call
|
8
8
|
end.to raise_error do |error|
|
9
9
|
expect(error).to be_a(ErrorService::Error)
|
10
|
-
expect(error.message).to eq('I am a service error.')
|
10
|
+
expect(error.message).to eq('I am a service error raised by ErrorService.')
|
11
11
|
expect(error.cause).to be_nil
|
12
12
|
end
|
13
13
|
|
data/spec/spec_helper.rb
CHANGED
@@ -1,4 +1,19 @@
|
|
1
|
+
require 'services/base_finder'
|
2
|
+
|
1
3
|
class Model
|
4
|
+
class << self
|
5
|
+
def table_name
|
6
|
+
'models'
|
7
|
+
end
|
8
|
+
|
9
|
+
# Stub ActiveRecord methods
|
10
|
+
%i(select order where limit page per).each do |m|
|
11
|
+
define_method m do |*args|
|
12
|
+
self
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
2
17
|
attr_reader :id
|
3
18
|
|
4
19
|
def initialize(id)
|
@@ -27,6 +42,12 @@ end
|
|
27
42
|
|
28
43
|
module Services
|
29
44
|
module Models
|
45
|
+
class BaseFind < Services::BaseFinder
|
46
|
+
private def process(scope, conditions)
|
47
|
+
scope
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
30
51
|
class Find < Services::Base
|
31
52
|
def call(ids)
|
32
53
|
ids.map { |id| ModelRepository.find id }.compact
|
@@ -52,9 +73,24 @@ class EmptyService < Services::Base
|
|
52
73
|
end
|
53
74
|
end
|
54
75
|
|
76
|
+
class EmptyServiceWithoutCallLogging < Services::Base
|
77
|
+
disable_call_logging
|
78
|
+
|
79
|
+
def call(*args)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
55
83
|
class ErrorService < Services::Base
|
56
84
|
def call
|
57
|
-
raise Error
|
85
|
+
raise Error, "I am a service error raised by #{self.class}."
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class ErrorServiceWithoutCallLogging < Services::Base
|
90
|
+
disable_call_logging
|
91
|
+
|
92
|
+
def call
|
93
|
+
raise Error, "I am a service error raised by #{self.class}."
|
58
94
|
end
|
59
95
|
end
|
60
96
|
|
@@ -66,14 +102,14 @@ end
|
|
66
102
|
|
67
103
|
class UniqueService < Services::Base
|
68
104
|
def call(on_error, sleep)
|
69
|
-
check_uniqueness
|
105
|
+
check_uniqueness on_error: on_error
|
70
106
|
sleep 0.5 if sleep
|
71
107
|
end
|
72
108
|
end
|
73
109
|
|
74
110
|
class UniqueWithCustomArgsService < Services::Base
|
75
111
|
def call(uniqueness_arg1, uniqueness_arg2, ignore_arg, on_error, sleep)
|
76
|
-
check_uniqueness
|
112
|
+
check_uniqueness uniqueness_arg1, uniqueness_arg2, on_error: on_error
|
77
113
|
sleep 0.5 if sleep
|
78
114
|
end
|
79
115
|
end
|
@@ -81,7 +117,7 @@ end
|
|
81
117
|
class UniqueMultipleService < Services::Base
|
82
118
|
def call(*args, on_error, sleep)
|
83
119
|
args.each do |arg|
|
84
|
-
check_uniqueness
|
120
|
+
check_uniqueness arg, on_error: on_error
|
85
121
|
end
|
86
122
|
sleep 0.5 if sleep
|
87
123
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: services
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Manuel Meurer
|
@@ -162,6 +162,7 @@ files:
|
|
162
162
|
- lib/services/railtie.rb
|
163
163
|
- lib/services/version.rb
|
164
164
|
- services.gemspec
|
165
|
+
- spec/services/base_finder_spec.rb
|
165
166
|
- spec/services/base_spec.rb
|
166
167
|
- spec/services/logger/redis_spec.rb
|
167
168
|
- spec/services/modules/call_logger_spec.rb
|
@@ -199,6 +200,7 @@ signing_key:
|
|
199
200
|
specification_version: 4
|
200
201
|
summary: A nifty service layer for your Rails app
|
201
202
|
test_files:
|
203
|
+
- spec/services/base_finder_spec.rb
|
202
204
|
- spec/services/base_spec.rb
|
203
205
|
- spec/services/logger/redis_spec.rb
|
204
206
|
- spec/services/modules/call_logger_spec.rb
|