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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5d81b81f1b90d4c408865594ca48558fe9469e7d
4
- data.tar.gz: a33b8e7d9007b638feddadd7270780756a083a0f
3
+ metadata.gz: b511fc23143b4bd0ddf9eef2574f76f3c0bf12cf
4
+ data.tar.gz: f34f312482dc3e0025b98d3acf0dae0c92d45275
5
5
  SHA512:
6
- metadata.gz: 28c253c6305653cd64b7fed56a905d6b4215f6ca579500ac3c0015f6ed0c682958478771b7a7b45bd90360b86b745bfa8c19b1d5814e845476d65bb1e107747f
7
- data.tar.gz: 0942c38383e122fdf186cde9fd278c684d1e2fe8211b82724fb306f6bd168bf8b587abe362c61927a87752326276ef488d331227e856ceabfb39def0453fcb72
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.
@@ -1,5 +1,7 @@
1
1
  module Services
2
2
  class BaseFinder < Services::Base
3
+ disable_call_logging
4
+
3
5
  def call(ids = [], conditions = {})
4
6
  ids, conditions = Array(ids), conditions.symbolize_keys
5
7
  special_conditions = conditions.extract!(:order, :limit, :page, :per_page)
@@ -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
- log "START with args #{args}"
8
- log "CALLED BY #{caller || '(not found)'}"
9
- start = Time.now
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
- log_exception e
29
+ log exception_message(e), {}, 'error'
14
30
  raise e
15
31
  ensure
16
- log "END after #{(Time.now - start).round(2)} seconds"
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, { service: self.class.to_s, id: @id }, severity
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 log_exception(e, cause = false)
28
- log "#{'caused by: ' if cause}#{e.class}: #{e.message}"
29
- if e.respond_to?(:cause) && e.cause
30
- e.backtrace.take(5).each do |line|
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!(*args, on_error: :fail)
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
@@ -1,3 +1,3 @@
1
1
  module Services
2
- VERSION = '1.3.0'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Services::BaseFinder do
4
+ it 'does not log start and end' do
5
+ expect { Services::Models::BaseFind.call }.to_not change { @logs }
6
+ end
7
+ end
@@ -1,54 +1,125 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Services::Base::CallLogger do
4
- it 'logs start with args and end with duration' do
5
- service = EmptyService.new
6
- logs = []
7
- allow(service).to receive(:log) do |message, *|
8
- logs << message
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
- it 'logs the caller' do
16
- service_calling_service, called_service = ServiceCallingService.new, EmptyService.new
17
- logs = []
18
- allow(called_service).to receive(:log) do |message, *|
19
- logs << message
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
- # When Rails is not defined, the complete caller path should be logged
23
- service_calling_service.call called_service
24
- expect(logs).to include(/\ACALLED BY #{Regexp.escape PROJECT_ROOT.join(TEST_SERVICES_PATH).to_s}:\d+/)
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
- # When Rails is defined, only the caller path relative to Rails.root is logged
27
- class Rails
28
- def self.root; PROJECT_ROOT; end
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 exceptions and exception causes' do
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
- expect(logs).to include(/\Acaused by: #{service.class}::#{error}/)
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
@@ -29,7 +29,6 @@ sidekiq_timeout = 20
29
29
 
30
30
  Services.configure do |config|
31
31
  config.redis = Redis.new
32
- config.logger = Services::Logger::File.new(log_dir)
33
32
  end
34
33
 
35
34
  Sidekiq.configure_client do |config|
@@ -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.new('I am a service 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! on_error: on_error
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! uniqueness_arg1, uniqueness_arg2, on_error: on_error
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! arg, on_error: on_error
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: 1.3.0
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