services 2.0.0 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/CHANGELOG.md +7 -0
- data/Guardfile +46 -17
- data/README.md +41 -15
- data/lib/services/modules/call_logger.rb +8 -0
- data/lib/services/version.rb +1 -1
- data/spec/services/base_finder_spec.rb +5 -2
- data/spec/services/modules/call_logger_spec.rb +10 -25
- data/spec/support/shared.rb +19 -0
- data/spec/support/test_services.rb +3 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 497d42516b9a0d6a95bdf84cc5387fddae35c1a0
|
4
|
+
data.tar.gz: 25e03e9ceea6e04dadac955e5027c654d0a95dda
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ea3bb811cc4b5f55e3e8eefda6b6d64731250b46b143ea2ed1a456819d605727e58fca08417cb15a3e3e9eb37741dee8684c225af5ba4ef83d6f3162cb821d9
|
7
|
+
data.tar.gz: 757435307628879d21fe2a7106630e39901c1f806a7e4b024215d4fbdb98f336a85d6752fb88055b74c60863d29d508228d51275c27ebf69c75b123dbe47f1ea
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
## 2.0.0
|
2
|
+
|
3
|
+
* Improve call logging
|
4
|
+
* Implement `disable_call_logging` and `enable_call_logging` to control call logging for specific services
|
5
|
+
* Disable call logging for `BaseFinder` by default
|
6
|
+
* Rename `check_uniqueness!` to `check_uniqueness`
|
7
|
+
|
1
8
|
## 1.3.0
|
2
9
|
|
3
10
|
* Allow only certain classes in Redis logger meta (NilClass, TrueClass, FalseClass, Symbol, String, Numeric)
|
data/Guardfile
CHANGED
@@ -1,25 +1,54 @@
|
|
1
|
-
|
2
|
-
def call(guard_class, event, *args)
|
3
|
-
puts guard_class
|
4
|
-
puts event
|
5
|
-
puts args
|
6
|
-
end
|
7
|
-
end
|
1
|
+
# require 'guard/rspec'
|
8
2
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
end
|
3
|
+
# module ::Guard
|
4
|
+
# class RSpec < Plugin
|
5
|
+
# # Add `stop` method if not defined
|
6
|
+
# # so that `stop_*` callbacks work.
|
7
|
+
# unless instance_methods.include?(:stop)
|
8
|
+
# def stop; end
|
9
|
+
# end
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
|
13
|
+
# class Guard::StartRedisAndSidekiq
|
14
|
+
# def call(guard_class, event, *args)
|
15
|
+
# # Start Redis
|
16
|
+
# redis_options = {
|
17
|
+
# daemonize: 'yes',
|
18
|
+
# port: redis_port,
|
19
|
+
# dir: support_dir,
|
20
|
+
# dbfilename: 'redis.rdb',
|
21
|
+
# logfile: log_dir.join('redis.log'),
|
22
|
+
# pidfile: redis_pidfile
|
23
|
+
# }
|
24
|
+
# redis = support_dir.join('redis-server')
|
25
|
+
# system "#{redis} #{options_hash_to_string(redis_options)}"
|
26
|
+
|
27
|
+
# # Copy call proxy
|
28
|
+
# FileUtils.cp CALL_PROXY_SOURCE, CALL_PROXY_DESTINATION
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
|
32
|
+
# class Guard::StopRedisAndSidekiq
|
33
|
+
# def call(guard_class, event, *args)
|
34
|
+
# # Delete call proxy
|
35
|
+
# FileUtils.rm CALL_PROXY_DESTINATION
|
36
|
+
|
37
|
+
# # Stop Redis
|
38
|
+
# redis_cli = support_dir.join('redis-cli')
|
39
|
+
# system "#{redis_cli} -p #{redis_port} shutdown"
|
40
|
+
|
41
|
+
# # Truncate log files
|
42
|
+
# max_len = 1024 * 1024 # 1 MB
|
43
|
+
# Dir[File.join(log_dir, '*.log')].each do |file|
|
44
|
+
# File.truncate file, max_len if File.size(file) > max_len
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
# end
|
17
48
|
|
18
49
|
guard 'rspec', cmd: 'bundle exec rspec' do
|
19
50
|
# callback StartRedisAndSidekiq.new, :start_begin
|
20
|
-
# callback StopRedisAndSidekiq.new, :stop
|
21
51
|
# callback StopRedisAndSidekiq.new, :stop_begin
|
22
|
-
# callback StopRedisAndSidekiq.new, :stop_end
|
23
52
|
|
24
53
|
# Specs
|
25
54
|
watch(%r(^spec/.+_spec\.rb$))
|
data/README.md
CHANGED
@@ -9,35 +9,37 @@ Services is a collection of modules and base classes that let you implement a ni
|
|
9
9
|
|
10
10
|
## Motivation
|
11
11
|
|
12
|
-
A lot has been written about service layers in Rails apps. There are advantages and disadvantages
|
12
|
+
A lot has been written about service layers in Rails apps. There are of course advantages and disadvantages, but after using Services since 2013 in several Rails apps, I must say that in my opinion the advantages far outweigh the disadvantages.
|
13
13
|
|
14
|
-
**The biggest benefit you get with a service layer is that it
|
14
|
+
**The biggest benefit you get with a service layer is that it gets much easier to reason about your application, find a bug, or implement new features, when all your business logic is in services, not scattered in models, controllers, helpers etc.**
|
15
15
|
|
16
16
|
## Usage
|
17
17
|
|
18
|
-
For disambiguation, we let's write Services with a uppercase S when we mean this gem, and services with a lowercase s when we mean, well, the plural of service.
|
18
|
+
For disambiguation, we let's write Services with a uppercase "S" when we mean this gem, and services with a lowercase "s" when we mean, well, the plural of service.
|
19
19
|
|
20
20
|
### Basic principles
|
21
21
|
|
22
|
-
Services is based on a couple of basic principles of what a service should be and do in your
|
22
|
+
Services is based on a couple of basic principles of what a service should be and do in your app:
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
*
|
27
|
-
*
|
28
|
-
*
|
29
|
-
*
|
24
|
+
A service...
|
25
|
+
|
26
|
+
* ...does one thing well (Unix philosophy)
|
27
|
+
* ...can be run synchronously (in the foreground) or asynchronously (in the background)
|
28
|
+
* ...can be unique, meaning only one instance of it should be run at a time
|
29
|
+
* ...logs all the things (start time, end time, duration, caller, exceptions etc.)
|
30
|
+
* ...has its own exception class(es) that all exceptions that it may raise inherit from
|
31
|
+
* ...can be called with one or multiple objects or one or multiple object IDs
|
30
32
|
|
31
33
|
Apart from these basic principles, you can implement the actual logic in a service any way you want.
|
32
34
|
|
33
35
|
### Conventions
|
34
36
|
|
35
|
-
Follow these conventions
|
37
|
+
Follow these conventions when using Services in your Rails app:
|
36
38
|
|
37
39
|
* services inherit from `Services::Base` (or `Services::BaseFinder`)
|
38
40
|
* services are located in `app/services/`
|
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::
|
40
|
-
*
|
41
|
+
* 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::DoStuff`) or namespace them by logical groups unrelated to models (`Services::Maintenance::CleanOldUsers`, `Services::Maintenance::SendDailySummary`, etc.)
|
42
|
+
* some services 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
43
|
|
42
44
|
### Rails autoload fix
|
43
45
|
|
@@ -50,9 +52,17 @@ config.autoload_paths += [config.root.join('app')]
|
|
50
52
|
|
51
53
|
### Dependence
|
52
54
|
|
53
|
-
|
55
|
+
#### Redis
|
56
|
+
|
57
|
+
to be described...
|
58
|
+
|
59
|
+
#### Sidekiq
|
60
|
+
|
61
|
+
To process services in the background, Services uses [Sidekiq](https://github.com/mperham/sidekiq). Sidekiq is not required to use Services though. If it's not present when Services is loaded, a service will raise an exception when you try to enqueue it for background processing. If you use Sidekiq, make sure to load the Services gem after the Sidekiq gem.
|
62
|
+
|
63
|
+
#### Postgres
|
54
64
|
|
55
|
-
The SQL `Services::BaseFinder` (discussed further down) generates is optimized for Postgres. It might work with other databases but it's not guaranteed. If you're not using Postgres, don't use `Services::BaseFinder` or, even better, submit a
|
65
|
+
The SQL that `Services::BaseFinder` (discussed further down) generates is optimized for Postgres. It might work with other databases but it's not guaranteed. If you're not using Postgres, don't use `Services::BaseFinder` or, even better, submit a [pull request](https://github.com/krautcomputing/services/issues) that fixes it to work with your database!
|
56
66
|
|
57
67
|
### Examples
|
58
68
|
|
@@ -75,6 +85,22 @@ module Services
|
|
75
85
|
end
|
76
86
|
```
|
77
87
|
|
88
|
+
This service can be called in several ways:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
# Execute synchronously/immediately
|
92
|
+
Services::Users::Delete.call User.find(1) # with a user object
|
93
|
+
Services::Users::Delete.call User.where(id: [1, 2, 3]) # with multiple user objects
|
94
|
+
Services::Users::Delete.call 1 # with a user ID
|
95
|
+
Services::Users::Delete.call [1, 2, 3] # with multiple user IDs
|
96
|
+
|
97
|
+
# Execute asynchronously/in the background
|
98
|
+
Services::Users::Delete.perform_async 1 # with a user ID
|
99
|
+
Services::Users::Delete.perform_async [1, 2, 3] # with multiple user IDs
|
100
|
+
```
|
101
|
+
|
102
|
+
As you can see, you cannot use objects when calling a service asynchronously since the arguments are serialized to Redis.
|
103
|
+
|
78
104
|
As you can see, the helper `find_objects` is used to make sure you are dealing with an array of users from that point on, no matter whether `ids_or_objects` is a single user ID or user, or an array of user IDs or users.
|
79
105
|
|
80
106
|
It's good practice to always return the objects a service has been operating on at the end of the service.
|
@@ -3,9 +3,17 @@ module Services
|
|
3
3
|
module CallLogger
|
4
4
|
def self.prepended(mod)
|
5
5
|
mod.extend ClassMethods
|
6
|
+
mod.instance_eval do
|
7
|
+
def inherited(subclass)
|
8
|
+
subclass.extend ClassMethods
|
9
|
+
subclass.disable_call_logging if self.call_logging_disabled
|
10
|
+
end
|
11
|
+
end
|
6
12
|
end
|
7
13
|
|
8
14
|
module ClassMethods
|
15
|
+
@call_logging_disabled = false
|
16
|
+
|
9
17
|
attr_accessor :call_logging_disabled
|
10
18
|
|
11
19
|
def disable_call_logging
|
data/lib/services/version.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Services::BaseFinder do
|
4
|
-
|
5
|
-
|
4
|
+
include_context 'capture logs'
|
5
|
+
|
6
|
+
it 'has call logging disabled by default' do
|
7
|
+
expect(Services::Models::BaseFind.call_logging_disabled).to eq(true)
|
8
|
+
expect { Services::Models::BaseFind.call }.to_not change { logs }
|
6
9
|
end
|
7
10
|
end
|
@@ -1,30 +1,14 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Services::Base::CallLogger do
|
4
|
-
|
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
|
-
}
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
after do
|
19
|
-
Services.configuration.logger = nil
|
20
|
-
end
|
4
|
+
include_context 'capture logs'
|
21
5
|
|
22
6
|
context 'when call logging is enabled' do
|
23
7
|
it 'logs start and end' do
|
24
8
|
service, args = EmptyService.new, %w(foo bar)
|
25
9
|
service.call *args
|
26
10
|
caller_regex = /\A#{Regexp.escape __FILE__}:\d+\z/
|
27
|
-
expect(
|
11
|
+
expect(logs.first).to match(
|
28
12
|
message: "START with args: #{args}",
|
29
13
|
meta: {
|
30
14
|
caller: a_string_matching(caller_regex),
|
@@ -33,7 +17,7 @@ describe Services::Base::CallLogger do
|
|
33
17
|
},
|
34
18
|
severity: 'info'
|
35
19
|
)
|
36
|
-
expect(
|
20
|
+
expect(logs.last).to match(
|
37
21
|
message: 'END',
|
38
22
|
meta: {
|
39
23
|
duration: 0.0,
|
@@ -53,7 +37,7 @@ describe Services::Base::CallLogger do
|
|
53
37
|
Services::CallProxy.call(called_service, :call)
|
54
38
|
caller_regex = /\A#{Regexp.escape __FILE__}:\d+/
|
55
39
|
expect(
|
56
|
-
|
40
|
+
logs.detect do |log|
|
57
41
|
log[:meta][:caller] =~ caller_regex
|
58
42
|
end
|
59
43
|
).to be_present
|
@@ -64,7 +48,7 @@ describe Services::Base::CallLogger do
|
|
64
48
|
service_calling_service.call called_service
|
65
49
|
caller_regex = /\A#{Regexp.escape PROJECT_ROOT.join(TEST_SERVICES_PATH).to_s}:\d+/
|
66
50
|
expect(
|
67
|
-
|
51
|
+
logs.detect do |log|
|
68
52
|
log[:meta][:caller] =~ caller_regex
|
69
53
|
end
|
70
54
|
).to be_present
|
@@ -88,7 +72,7 @@ describe Services::Base::CallLogger do
|
|
88
72
|
service_calling_service.call called_service
|
89
73
|
caller_regex = /\A#{Regexp.escape TEST_SERVICES_PATH.to_s}:\d+/
|
90
74
|
expect(
|
91
|
-
|
75
|
+
logs.detect do |log|
|
92
76
|
log[:meta][:caller] =~ caller_regex
|
93
77
|
end
|
94
78
|
).to be_present
|
@@ -99,13 +83,14 @@ describe Services::Base::CallLogger do
|
|
99
83
|
|
100
84
|
context 'when call logging is disabled' do
|
101
85
|
it 'does not log start and end' do
|
102
|
-
expect
|
86
|
+
expect(EmptyServiceWithoutCallLogging.call_logging_disabled).to eq(true)
|
87
|
+
expect { EmptyServiceWithoutCallLogging.call }.to_not change { logs }
|
103
88
|
end
|
104
89
|
end
|
105
90
|
|
106
91
|
it 'logs exceptions' do
|
107
92
|
[ErrorService, ErrorServiceWithoutCallLogging].each do |klass|
|
108
|
-
expect { klass.call rescue nil }.to change {
|
93
|
+
expect { klass.call rescue nil }.to change { logs }
|
109
94
|
end
|
110
95
|
end
|
111
96
|
|
@@ -116,7 +101,7 @@ describe Services::Base::CallLogger do
|
|
116
101
|
%w(NestedError1 NestedError2).each do |error|
|
117
102
|
message_regex = /caused by: #{service.class}::#{error}/
|
118
103
|
expect(
|
119
|
-
|
104
|
+
logs.detect do |log|
|
120
105
|
log[:message] =~ message_regex
|
121
106
|
end
|
122
107
|
).to be_present
|
@@ -0,0 +1,19 @@
|
|
1
|
+
shared_context 'capture logs' do
|
2
|
+
let(:logger) { spy('logger') }
|
3
|
+
let(:logs) { [] }
|
4
|
+
|
5
|
+
before do
|
6
|
+
Services.configuration.logger = logger
|
7
|
+
allow(logger).to receive(:log) do |message, meta, severity|
|
8
|
+
logs << {
|
9
|
+
message: message,
|
10
|
+
meta: meta,
|
11
|
+
severity: severity
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
after do
|
17
|
+
Services.configuration.logger = nil
|
18
|
+
end
|
19
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: services
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Manuel Meurer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-11-
|
11
|
+
date: 2014-11-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -174,6 +174,7 @@ files:
|
|
174
174
|
- spec/support/log/.gitkeep
|
175
175
|
- spec/support/redis-cli
|
176
176
|
- spec/support/redis-server
|
177
|
+
- spec/support/shared.rb
|
177
178
|
- spec/support/test_services.rb
|
178
179
|
homepage: http://krautcomputing.github.io/services
|
179
180
|
licenses:
|
@@ -212,5 +213,6 @@ test_files:
|
|
212
213
|
- spec/support/log/.gitkeep
|
213
214
|
- spec/support/redis-cli
|
214
215
|
- spec/support/redis-server
|
216
|
+
- spec/support/shared.rb
|
215
217
|
- spec/support/test_services.rb
|
216
218
|
has_rdoc:
|