carrot_rpc 0.2.3.pre
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 +7 -0
- data/.gitignore +50 -0
- data/.rspec +2 -0
- data/.rubocop.yml +35 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +149 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +9 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +172 -0
- data/Rakefile +7 -0
- data/bin/carrot_rpc +19 -0
- data/bin/console +20 -0
- data/bin/setup +7 -0
- data/carrot_rpc.gemspec +49 -0
- data/circle.yml +8 -0
- data/lib/carrot_rpc/cli.rb +110 -0
- data/lib/carrot_rpc/client_server.rb +25 -0
- data/lib/carrot_rpc/configuration.rb +17 -0
- data/lib/carrot_rpc/error/code.rb +17 -0
- data/lib/carrot_rpc/error.rb +43 -0
- data/lib/carrot_rpc/hash_extensions.rb +22 -0
- data/lib/carrot_rpc/rpc_client.rb +117 -0
- data/lib/carrot_rpc/rpc_server/jsonapi_resources/actions.rb +108 -0
- data/lib/carrot_rpc/rpc_server/jsonapi_resources.rb +172 -0
- data/lib/carrot_rpc/rpc_server.rb +77 -0
- data/lib/carrot_rpc/server_runner/autoload_rails.rb +41 -0
- data/lib/carrot_rpc/server_runner/logger.rb +31 -0
- data/lib/carrot_rpc/server_runner/pid.rb +142 -0
- data/lib/carrot_rpc/server_runner/signals.rb +21 -0
- data/lib/carrot_rpc/server_runner.rb +156 -0
- data/lib/carrot_rpc/tagged_log.rb +24 -0
- data/lib/carrot_rpc/version.rb +3 -0
- data/lib/carrot_rpc.rb +46 -0
- data/logs/.gitkeep +0 -0
- metadata +182 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
# Base RPC Server class. Other Servers should inherit from this.
|
2
|
+
class CarrotRpc::RpcServer
|
3
|
+
autoload :JSONAPIResources, "carrot_rpc/rpc_server/jsonapi_resources"
|
4
|
+
|
5
|
+
using CarrotRpc::HashExtensions
|
6
|
+
|
7
|
+
attr_reader :channel, :server_queue, :logger
|
8
|
+
# method_reciver => object that receives the method. can be a class or anything responding to send
|
9
|
+
|
10
|
+
extend CarrotRpc::ClientServer
|
11
|
+
|
12
|
+
# Documentation advises not to share a channel connection. Create new channel for each server instance.
|
13
|
+
def initialize(config: nil, block: true)
|
14
|
+
# create a channel and exchange that both client and server know about
|
15
|
+
config ||= CarrotRpc.configuration
|
16
|
+
@channel = config.bunny.create_channel
|
17
|
+
@logger = config.logger
|
18
|
+
@block = block
|
19
|
+
@server_queue = @channel.queue(self.class.queue_name)
|
20
|
+
@exchange = @channel.default_exchange
|
21
|
+
end
|
22
|
+
|
23
|
+
# start da server!
|
24
|
+
# method => object that receives the method. can be a class or anything responding to send
|
25
|
+
def start
|
26
|
+
# subscribe is like a callback
|
27
|
+
@server_queue.subscribe(block: @block) do |_delivery_info, properties, payload|
|
28
|
+
logger.debug "Receiving message: #{payload}"
|
29
|
+
|
30
|
+
request_message = JSON.parse(payload).with_indifferent_access
|
31
|
+
|
32
|
+
process_request(request_message, properties: properties)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def process_request(request_message, properties:)
|
37
|
+
result = send(request_message[:method], request_message[:params])
|
38
|
+
rescue CarrotRpc::Error => rpc_server_error
|
39
|
+
logger.error(rpc_server_error)
|
40
|
+
|
41
|
+
reply_error rpc_server_error.serialized_message,
|
42
|
+
properties: properties,
|
43
|
+
request_message: request_message
|
44
|
+
else
|
45
|
+
reply_result result,
|
46
|
+
properties: properties,
|
47
|
+
request_message: request_message
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def reply(properties:, response_message:)
|
53
|
+
@exchange.publish response_message.to_json,
|
54
|
+
correlation_id: properties.correlation_id,
|
55
|
+
routing_key: properties.reply_to
|
56
|
+
end
|
57
|
+
|
58
|
+
# See http://www.jsonrpc.org/specification#error_object
|
59
|
+
def reply_error(error, properties:, request_message:)
|
60
|
+
response_message = { error: error, id: request_message[:id], jsonrpc: "2.0" }
|
61
|
+
|
62
|
+
logger.debug "Publish error: #{error} to #{response_message}"
|
63
|
+
|
64
|
+
reply properties: properties,
|
65
|
+
response_message: response_message
|
66
|
+
end
|
67
|
+
|
68
|
+
# See http://www.jsonrpc.org/specification#response_object
|
69
|
+
def reply_result(result, properties:, request_message:)
|
70
|
+
response_message = { id: request_message[:id], result: result, jsonrpc: "2.0" }
|
71
|
+
|
72
|
+
logger.debug "Publishing result: #{result} to #{response_message}"
|
73
|
+
|
74
|
+
reply properties: properties,
|
75
|
+
response_message: response_message
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# Loads the Rails application so that the servers can use the Rails gems and environment.
|
2
|
+
module CarrotRpc::ServerRunner::AutoloadRails
|
3
|
+
# Path to the `config/environment.rb`, which is the file that must actually be `require`d to load Rails.
|
4
|
+
#
|
5
|
+
# @param root [String] path to the root of the Rails app
|
6
|
+
# @return [String]
|
7
|
+
def self.environment_path(root)
|
8
|
+
File.join(root, "config/environment.rb")
|
9
|
+
end
|
10
|
+
|
11
|
+
# Attempts to load Rails app at `root`.
|
12
|
+
#
|
13
|
+
# @param root [String] path to the root of the Rails app
|
14
|
+
# @param logger [Logger] logger to print success to.
|
15
|
+
# @return [true]
|
16
|
+
# @raise [LoadError] if rails cannot be loaded
|
17
|
+
def self.load_root(root, logger:)
|
18
|
+
rails_path = environment_path(root)
|
19
|
+
|
20
|
+
if File.exist?(rails_path)
|
21
|
+
logger.info "Rails app found at: #{rails_path}"
|
22
|
+
ENV["RACK_ENV"] ||= ENV["RAILS_ENV"] || "development"
|
23
|
+
require rails_path
|
24
|
+
::Rails.application.eager_load!
|
25
|
+
true
|
26
|
+
else
|
27
|
+
require rails_path
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Loads Rails app at `root` if `CarrotRpc.configuration.autoload_rails`
|
32
|
+
#
|
33
|
+
# @param (see load_root)
|
34
|
+
# @return (see load_root)
|
35
|
+
# @raise (see load_root)
|
36
|
+
def self.conditionally_load_root(root, logger:)
|
37
|
+
if CarrotRpc.configuration.autoload_rails
|
38
|
+
load_root(root, logger: logger)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Constructs a logger based on the {CarrotRpc.configuration} and Rails environment
|
2
|
+
module CarrotRpc::ServerRunner::Logger
|
3
|
+
# A `Logger` configured based on `CarrotRpc.configuration.logfile` and `CarrotRpc.configuration.loglevel`
|
4
|
+
#
|
5
|
+
# Fallbacks:
|
6
|
+
# * `Rails.logger` if `Rails` is loaded
|
7
|
+
# * `Logger` to `STDOUT` if `Rails` is not loaded
|
8
|
+
#
|
9
|
+
# @return [Logger]
|
10
|
+
def self.configured
|
11
|
+
logger = from_file
|
12
|
+
|
13
|
+
if logger.nil?
|
14
|
+
logger = if defined?(::Rails)
|
15
|
+
CarrotRpc::TaggedLog.new(logger: Rails.logger, tags: ["Carrot RPC"])
|
16
|
+
else
|
17
|
+
Logger.new(STDOUT)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
logger.level = CarrotRpc.configuration.loglevel
|
22
|
+
|
23
|
+
logger
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.from_file
|
27
|
+
return nil unless CarrotRpc.configuration.logfile
|
28
|
+
|
29
|
+
::Logger.new(CarrotRpc.configuration.logfile)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# Pid and pid path for {CarrotRpc::ServerRunner}
|
2
|
+
class CarrotRpc::ServerRunner::Pid
|
3
|
+
# Attributes
|
4
|
+
|
5
|
+
# @return [Logger]
|
6
|
+
attr_reader :logger
|
7
|
+
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :path
|
10
|
+
|
11
|
+
# Methods
|
12
|
+
|
13
|
+
## Class Methods
|
14
|
+
|
15
|
+
# The status of the given `pid` number.
|
16
|
+
#
|
17
|
+
# @param pid [Integer] a 0 or positive PID
|
18
|
+
# @return [:dead] if `pid` is `0`
|
19
|
+
# @return (see number_error_check_status)
|
20
|
+
def self.number_status(pid)
|
21
|
+
if pid == 0
|
22
|
+
:dead
|
23
|
+
else
|
24
|
+
number_error_check_status(pid)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# The status of the given `non_zero_pid` number.
|
29
|
+
#
|
30
|
+
# @param non_zero_pid [Integer] a non-zero pid
|
31
|
+
# @return [:dead] if `pid` cannot be contacted
|
32
|
+
# @return [:not_owned] if interacting with `pid` raises a permission error
|
33
|
+
# @return [:running] if `pid` is running
|
34
|
+
def self.number_error_check_status(non_zero_pid)
|
35
|
+
# sending signal `0` just performs error checking
|
36
|
+
Process.kill(0, non_zero_pid)
|
37
|
+
rescue Errno::ESRCH
|
38
|
+
# Invalid pid
|
39
|
+
:dead
|
40
|
+
rescue Errno::EPERM
|
41
|
+
# no privilege to interact with process
|
42
|
+
:not_owned
|
43
|
+
else
|
44
|
+
:running
|
45
|
+
end
|
46
|
+
|
47
|
+
# Status of the `pid` inside `path`.
|
48
|
+
#
|
49
|
+
# @return [:exited] if `path` does not exist, which indicates the server never ran or exited cleanly
|
50
|
+
# @return [:not_owned] if `path` cannot be read
|
51
|
+
# @return (see number_status)
|
52
|
+
def self.path_status(path)
|
53
|
+
pid = File.read(path).to_i
|
54
|
+
rescue Errno::ENOENT
|
55
|
+
# File does not exist
|
56
|
+
:exited
|
57
|
+
rescue Errno::EPERM
|
58
|
+
# File cannot be read
|
59
|
+
:not_owned
|
60
|
+
else
|
61
|
+
number_status(pid)
|
62
|
+
end
|
63
|
+
|
64
|
+
## Initialize
|
65
|
+
|
66
|
+
# @param path [Path, nil] path to pid path
|
67
|
+
def initialize(path:, logger:)
|
68
|
+
unless path.nil?
|
69
|
+
# daemonization will change CWD so expand relative paths now
|
70
|
+
@path = File.expand_path(path)
|
71
|
+
end
|
72
|
+
|
73
|
+
@logger = logger
|
74
|
+
end
|
75
|
+
|
76
|
+
## Instance Methods
|
77
|
+
|
78
|
+
# Exits if {#status} indicates server is already running, otherwise deletes {#path}.
|
79
|
+
#
|
80
|
+
# @return [void]
|
81
|
+
def check
|
82
|
+
if path?
|
83
|
+
case self.class.path_status(path)
|
84
|
+
when :running, :not_owned
|
85
|
+
logger.warn "A server is already running. Check #{path}"
|
86
|
+
exit(1)
|
87
|
+
when :dead
|
88
|
+
delete
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Deletes `path` if it is set
|
94
|
+
#
|
95
|
+
# @return [void]
|
96
|
+
def delete
|
97
|
+
if path? && File.exist?(path)
|
98
|
+
File.delete(path)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Registers an `at_exit` handler to {#delete}.
|
103
|
+
#
|
104
|
+
# @return [void]
|
105
|
+
def delete_at_exit
|
106
|
+
at_exit do
|
107
|
+
delete
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Keeps trying to write {#path} until it succeeds
|
112
|
+
def ensure_written
|
113
|
+
if path?
|
114
|
+
begin
|
115
|
+
write
|
116
|
+
rescue Errno::EEXIST
|
117
|
+
check
|
118
|
+
|
119
|
+
retry
|
120
|
+
else
|
121
|
+
delete_at_exit
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Whether {#path} is set.
|
127
|
+
#
|
128
|
+
# @return [true] if {#path} is not `nil`.
|
129
|
+
# @return [false] otherwise
|
130
|
+
def path?
|
131
|
+
!path.nil?
|
132
|
+
end
|
133
|
+
|
134
|
+
# Write to process id path
|
135
|
+
#
|
136
|
+
# @return [void]
|
137
|
+
def write
|
138
|
+
File.open(path, File::CREAT | File::EXCL | File::WRONLY) do |f|
|
139
|
+
f.write(Process.pid.to_s)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Traps all {the signals NAMES} that should be intercepted by a long-running background process.
|
2
|
+
module CarrotRpc::ServerRunner::Signals
|
3
|
+
# CONSTANTS
|
4
|
+
|
5
|
+
# The name of the signals to trap.
|
6
|
+
NAMES = %w(HUP INT QUIT TERM).freeze
|
7
|
+
|
8
|
+
# Traps all {NAMES}.
|
9
|
+
#
|
10
|
+
# @yield [name] Block to call when the signal is trapped.
|
11
|
+
# @yieldparam name [String] the name of the signal that was trapped
|
12
|
+
# @yieldreturn [void]
|
13
|
+
# @return [void]
|
14
|
+
def self.trap
|
15
|
+
NAMES.each do |name|
|
16
|
+
Kernel.trap(name) do
|
17
|
+
yield name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require "active_support/core_ext/string/inflections"
|
2
|
+
|
3
|
+
# Automatically detects, loads, and runs all {CarrotRpc::RpcServer} subclasses under `app/servers` in the project root.
|
4
|
+
class CarrotRpc::ServerRunner
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
autoload :AutoloadRails
|
8
|
+
autoload :Logger
|
9
|
+
autoload :Pid
|
10
|
+
autoload :Signals
|
11
|
+
|
12
|
+
# Attributes
|
13
|
+
|
14
|
+
attr_reader :quit, :servers
|
15
|
+
|
16
|
+
# @return [CarrotRpc::ServerRunner::Pid]
|
17
|
+
attr_reader :pid
|
18
|
+
|
19
|
+
# Methods
|
20
|
+
|
21
|
+
# Instantiate the ServerRunner.
|
22
|
+
def initialize(rails_path: ".", pidfile: nil, runloop_sleep: 0, daemonize: false)
|
23
|
+
@runloop_sleep = runloop_sleep
|
24
|
+
@daemonize = daemonize
|
25
|
+
@servers = []
|
26
|
+
|
27
|
+
CarrotRpc::ServerRunner::AutoloadRails.conditionally_load_root(rails_path, logger: logger)
|
28
|
+
trap_signals
|
29
|
+
|
30
|
+
@pid = CarrotRpc::ServerRunner::Pid.new(
|
31
|
+
path: pidfile,
|
32
|
+
logger: logger
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Start the servers and the run loop.
|
37
|
+
def run!
|
38
|
+
pid.check
|
39
|
+
daemonize && suppress_output if daemonize?
|
40
|
+
pid.ensure_written
|
41
|
+
|
42
|
+
# Initialize the servers. Set logger.
|
43
|
+
run_servers
|
44
|
+
|
45
|
+
# Sleep for a split second.
|
46
|
+
until quit
|
47
|
+
sleep @runloop_sleep
|
48
|
+
end
|
49
|
+
# When runtime gets here, quit signal is received.
|
50
|
+
stop_servers
|
51
|
+
end
|
52
|
+
|
53
|
+
# Shutdown all servers defined.
|
54
|
+
def stop_servers
|
55
|
+
logger.info "#{@siginal_name} signal received!"
|
56
|
+
@servers.each do |s|
|
57
|
+
logger.info "Shutting Down Server Queue: #{s.server_queue.name}"
|
58
|
+
s.channel.close
|
59
|
+
end
|
60
|
+
# Close the connection once all the other servers are shutdown
|
61
|
+
CarrotRpc.configuration.bunny.close
|
62
|
+
end
|
63
|
+
|
64
|
+
# Find and require all servers in the app/servers dir.
|
65
|
+
# @param dirs [Array] directories relative to root of host application where RpcServers can be loaded
|
66
|
+
# @return [Array] of RpcServers loaded and initialized
|
67
|
+
def run_servers(dirs: %w(app servers))
|
68
|
+
files = server_files(dirs)
|
69
|
+
fail "No servers found!" if files.empty?
|
70
|
+
|
71
|
+
# Load each server defined in the project dir
|
72
|
+
files.each do |file|
|
73
|
+
@servers << run_server_file(file)
|
74
|
+
end
|
75
|
+
|
76
|
+
@servers
|
77
|
+
end
|
78
|
+
|
79
|
+
def run_server_file(file)
|
80
|
+
require file
|
81
|
+
server_klass_name = file.to_s.split("/").last.gsub(".rb", "").camelize
|
82
|
+
server_klass = server_klass_name.constantize
|
83
|
+
|
84
|
+
logger.info "Starting #{server_klass}..."
|
85
|
+
|
86
|
+
server = server_klass.new(block: false)
|
87
|
+
server.start
|
88
|
+
|
89
|
+
server
|
90
|
+
end
|
91
|
+
|
92
|
+
def server_files(dirs)
|
93
|
+
Dir[server_glob(dirs)]
|
94
|
+
end
|
95
|
+
|
96
|
+
def server_glob(dirs)
|
97
|
+
regex = %r{\A/.*/#{dirs.join("/")}\z}
|
98
|
+
$LOAD_PATH.find { |p|
|
99
|
+
p.match(regex)
|
100
|
+
} + "/*.rb"
|
101
|
+
end
|
102
|
+
|
103
|
+
# Convenience method to wrap the logger object.
|
104
|
+
def logger
|
105
|
+
@logger ||= set_logger
|
106
|
+
end
|
107
|
+
|
108
|
+
# attr_reader doesn't allow adding a `?` to the method name, so I think this is a false positive
|
109
|
+
# rubocop:disable Style/TrivialAccessors
|
110
|
+
|
111
|
+
# Attribute to determine when to daemonize the process.
|
112
|
+
def daemonize?
|
113
|
+
@daemonize
|
114
|
+
end
|
115
|
+
|
116
|
+
# rubocop:enable Style/TrivialAccessors
|
117
|
+
|
118
|
+
# Background the ruby process.
|
119
|
+
def daemonize
|
120
|
+
exit if fork
|
121
|
+
Process.setsid
|
122
|
+
exit if fork
|
123
|
+
Dir.chdir "/"
|
124
|
+
end
|
125
|
+
|
126
|
+
# Part of daemonizing process. Prevents application from outputting info to terminal.
|
127
|
+
def suppress_output
|
128
|
+
$stderr.reopen("/dev/null", "a")
|
129
|
+
$stdout.reopen($stderr)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Set a value to signal shutdown.
|
133
|
+
def shutdown(name)
|
134
|
+
@signal_name = name
|
135
|
+
@quit = true
|
136
|
+
end
|
137
|
+
|
138
|
+
# Handle signal events.
|
139
|
+
def trap_signals
|
140
|
+
CarrotRpc::ServerRunner::Signals.trap do |name|
|
141
|
+
# @note can't log from a trap context: since Ruby 2.0 traps don't allow mutexes as it could lead to a dead lock,
|
142
|
+
# so `logger.info` here would return "log writing failed. can't be called from trap context"
|
143
|
+
shutdown(name)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
# Determine how to create logger. Config can specify log file.
|
150
|
+
def set_logger
|
151
|
+
logger = CarrotRpc::ServerRunner::Logger.configured
|
152
|
+
CarrotRpc.configuration.logger = logger
|
153
|
+
|
154
|
+
logger
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Wrap the Logger object with convenience methods.
|
2
|
+
class CarrotRpc::TaggedLog
|
3
|
+
attr_reader :logger, :tags
|
4
|
+
|
5
|
+
def initialize(logger:, tags:)
|
6
|
+
@logger = logger
|
7
|
+
@tags = *tags
|
8
|
+
end
|
9
|
+
|
10
|
+
def level
|
11
|
+
logger.level
|
12
|
+
end
|
13
|
+
|
14
|
+
def level=(level)
|
15
|
+
logger.level = level
|
16
|
+
end
|
17
|
+
|
18
|
+
# Dyanmically define logger methods with a tagged reference. Makes filtering of logs possible.
|
19
|
+
%i(debug info warn error fatal unknown).each do |level|
|
20
|
+
define_method(level) do |msg|
|
21
|
+
logger.tagged(tags) { logger.send(level, msg) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/carrot_rpc.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# gem dependencies
|
2
|
+
require "active_support/core_ext/hash/indifferent_access"
|
3
|
+
require "active_support/core_ext/hash/except"
|
4
|
+
require "active_support/dependencies/autoload"
|
5
|
+
require "bunny"
|
6
|
+
|
7
|
+
# standard library
|
8
|
+
require "json"
|
9
|
+
require "optparse"
|
10
|
+
|
11
|
+
# project
|
12
|
+
require "carrot_rpc/version"
|
13
|
+
|
14
|
+
# An opinionated approach to doing Remote Procedure Call (RPC) with RabbitMQ and the bunny gem. CarrotRpc serves as a
|
15
|
+
# way to streamline the RPC workflow so developers can focus on the implementation and not the plumbing when working
|
16
|
+
# with RabbitMQ.
|
17
|
+
module CarrotRpc
|
18
|
+
extend ActiveSupport::Autoload
|
19
|
+
|
20
|
+
autoload :CLI
|
21
|
+
autoload :ClientServer
|
22
|
+
autoload :Configuration
|
23
|
+
autoload :Error
|
24
|
+
autoload :HashExtensions
|
25
|
+
autoload :RpcClient
|
26
|
+
autoload :RpcServer
|
27
|
+
autoload :ServerRunner
|
28
|
+
autoload :TaggedLog
|
29
|
+
|
30
|
+
class << self
|
31
|
+
attr_writer :configuration
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.configuration
|
35
|
+
@configuration ||= Configuration.new
|
36
|
+
end
|
37
|
+
|
38
|
+
# Resets the configuration back to a new instance. Should only be used in testing.
|
39
|
+
def self.reset
|
40
|
+
@configuration = Configuration.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.configure
|
44
|
+
yield configuration
|
45
|
+
end
|
46
|
+
end
|
data/logs/.gitkeep
ADDED
File without changes
|