appydays 0.1.2
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/README.md +0 -0
- data/lib/appydays.rb +5 -0
- data/lib/appydays/configurable.rb +160 -0
- data/lib/appydays/dotenviable.rb +30 -0
- data/lib/appydays/loggable.rb +92 -0
- data/lib/appydays/loggable/request_logger.rb +94 -0
- data/lib/appydays/loggable/sequel_logger.rb +44 -0
- data/lib/appydays/loggable/spec_helpers.rb +51 -0
- data/lib/appydays/spec_helpers.rb +105 -0
- data/lib/appydays/version.rb +5 -0
- data/spec/appydays/configurable_spec.rb +197 -0
- data/spec/appydays/dotenviable_spec.rb +37 -0
- data/spec/appydays/loggable_spec.rb +184 -0
- data/spec/spec_helper.rb +38 -0
- metadata +212 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: eab73741b9b38b5e71b173859d4a21da866a0ed0d6ac2f2d336c535b60772cad
|
4
|
+
data.tar.gz: c801ace1467194743e5c30c2a08b91f6f735fc61cde35cbc142cae35267eaef0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dad5bd7318ec9bfe7cf73011da874787a9b38dd60305944ea3024473bf3e4eca3aaba59ad0878b6bd37f9bd65c4ffb941e88219a3bfbbd029567d1edc33ebb2d
|
7
|
+
data.tar.gz: 8cfb4768365e9e1c5b1b0052be6b42246bfac6379e712903ef7ff76cf5e882e7d593c65757ac34673704f3b78b0800a25adc6dff578f5e64b75c2a37d6a0daa6
|
data/README.md
ADDED
File without changes
|
data/lib/appydays.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "appydays/version"
|
4
|
+
|
5
|
+
##
|
6
|
+
# Define the configuration for a module or class.
|
7
|
+
#
|
8
|
+
# Usage:
|
9
|
+
# include Appydays::Configurable
|
10
|
+
# configurable(:myapp) do
|
11
|
+
# setting :log_level, 'debug', env: 'LOG_LEVEL'
|
12
|
+
# setting :app_url, 'http://localhost:3000'
|
13
|
+
# setting :workers, 4
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# The module or class that extends Configurable will get two singleton methods,
|
17
|
+
# `log_level` and `app_url`.
|
18
|
+
# The first will be be given a value of `ENV.fetch('LOG_LEVEL', 'debug')`,
|
19
|
+
# because the env key is provided.
|
20
|
+
#
|
21
|
+
# The second will be given a value of `ENV.fetch('MYAPP_APP_URL', 'http:://localhost:3000')`,
|
22
|
+
# because the env key is defaulted.
|
23
|
+
#
|
24
|
+
# The second will be given a value of `4.class(ENV.fetch('MYAPP_WORKERS', 4))`.
|
25
|
+
# Note it will coerce the type of the env value to the type of the default.
|
26
|
+
# Empty strings will be coerced to nil.
|
27
|
+
#
|
28
|
+
# The `setting` method has several other options;
|
29
|
+
# see its documentation for more details.
|
30
|
+
#
|
31
|
+
# The target will also get a `reset_configuration` method that will restore defaults,
|
32
|
+
# and `run_after_configured_hooks`. See their docs for more details.
|
33
|
+
#
|
34
|
+
module Appydays::Configurable
|
35
|
+
def self.included(target)
|
36
|
+
target.extend(ClassMethods)
|
37
|
+
end
|
38
|
+
|
39
|
+
module ClassMethods
|
40
|
+
def configurable(key, &block)
|
41
|
+
raise LocalJumpError unless block
|
42
|
+
|
43
|
+
installer = Installer.new(self, key)
|
44
|
+
|
45
|
+
self.define_singleton_method(:_configuration_installer) { installer }
|
46
|
+
|
47
|
+
installer.instance_eval(&block)
|
48
|
+
installer._run_after_configured
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Restore all settings back to the values they were at config time.
|
53
|
+
# Ie, undoes any manual attribute writes.
|
54
|
+
def reset_configuration
|
55
|
+
self._configuration_installer._reset
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Explicitly run after configuration hooks.
|
60
|
+
# This may need to be run explicitly after reset,
|
61
|
+
# if the `after_configured` hook involves side effects.
|
62
|
+
# Side effects are gnarly so we don't make assumptions.
|
63
|
+
def run_after_configured_hooks
|
64
|
+
self._configuration_installer._run_after_configured
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Installer
|
69
|
+
def initialize(target, group_key)
|
70
|
+
@target = target
|
71
|
+
@group_key = group_key
|
72
|
+
@settings = {}
|
73
|
+
@after_config_hooks = []
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Define a setting for the receiver,
|
78
|
+
# which acts as an attribute accessor.
|
79
|
+
#
|
80
|
+
# Params:
|
81
|
+
#
|
82
|
+
# name: The name of the accessor/setting.
|
83
|
+
# default: The default value.
|
84
|
+
# If `convert` is not supplied, this must be nil, or a string, int, float, or boolean,
|
85
|
+
# so the parsed environment value can be converted/coerced into the same type as 'default'.
|
86
|
+
# If convert is passed, that is used as the converter so the default value can be any type.
|
87
|
+
# key: The key to lookup the config value from the environment.
|
88
|
+
# If nil, use an auto-generated combo of the configuration key and method name.
|
89
|
+
# convert: If provided, call it with the string value so it can be parsed.
|
90
|
+
# For example, you can parse a string JSON value here.
|
91
|
+
# Convert will not be called with the default value.
|
92
|
+
# side_effect: If this setting should have a side effect,
|
93
|
+
# like configuring an external system, it should happen in this proc/lambda.
|
94
|
+
# It is called with the parsed/processed config value.
|
95
|
+
#
|
96
|
+
# Note that only ONE conversion will happen, and
|
97
|
+
# - If converter is provided, it will be used with the environment value.
|
98
|
+
def setting(name, default, key: nil, convert: nil, side_effect: nil)
|
99
|
+
installer = self
|
100
|
+
|
101
|
+
@target.define_singleton_method(name) do
|
102
|
+
self.class_variable_get("@@#{name}")
|
103
|
+
end
|
104
|
+
@target.define_singleton_method("#{name}=".to_sym) do |v|
|
105
|
+
installer._set_value(name, v, side_effect)
|
106
|
+
end
|
107
|
+
|
108
|
+
key ||= "#{@group_key}_#{name}".upcase
|
109
|
+
env_value = ENV.fetch(key, nil)
|
110
|
+
converter = self._converter(default, convert)
|
111
|
+
value = env_value.nil? ? default : converter[env_value]
|
112
|
+
value = installer._set_value(name, value, side_effect)
|
113
|
+
@settings[name] = value
|
114
|
+
end
|
115
|
+
|
116
|
+
def _set_value(name, value, side_effect)
|
117
|
+
value = nil if value == ""
|
118
|
+
# rubocop:disable Style/ClassVars
|
119
|
+
self._target.class_variable_set("@@#{name}", value)
|
120
|
+
# rubocop:enable Style/ClassVars
|
121
|
+
self._target.instance_exec(value, &side_effect) if side_effect
|
122
|
+
return value
|
123
|
+
end
|
124
|
+
|
125
|
+
def after_configured(&block)
|
126
|
+
@after_config_hooks << block
|
127
|
+
end
|
128
|
+
|
129
|
+
def _converter(default, converter)
|
130
|
+
return converter if converter
|
131
|
+
|
132
|
+
return ->(v) { v.to_s } if default.nil? || default.is_a?(String)
|
133
|
+
return ->(v) { v.to_i } if default.is_a?(Integer)
|
134
|
+
return ->(v) { v.to_f } if default.is_a?(Float)
|
135
|
+
return ->(v) { v.casecmp("true").zero? } if [TrueClass, FalseClass].include?(default.class)
|
136
|
+
raise TypeError, "Uncoercable type %p" % [default.class]
|
137
|
+
end
|
138
|
+
|
139
|
+
def _target
|
140
|
+
return @target
|
141
|
+
end
|
142
|
+
|
143
|
+
def _group_key
|
144
|
+
return @group_key
|
145
|
+
end
|
146
|
+
|
147
|
+
def _run_after_configured
|
148
|
+
@after_config_hooks.each do |h|
|
149
|
+
@target.instance_eval(&h)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def _reset
|
154
|
+
@settings.each do |k, v|
|
155
|
+
@target.send("#{k}=".to_sym, v)
|
156
|
+
end
|
157
|
+
self._run_after_configured
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dotenv"
|
4
|
+
|
5
|
+
require "appydays/version"
|
6
|
+
|
7
|
+
##
|
8
|
+
# Wrapper over dotenv that will load the standard .env files for an environment
|
9
|
+
# (by convention, .env.<env>.local, .env.<env>, and .env).
|
10
|
+
#
|
11
|
+
# It can be called multiple times for the same environment.
|
12
|
+
#
|
13
|
+
# NOTE: Foreman assigns the $PORT environment variable BEFORE we load config
|
14
|
+
# (get to what is defined in worker, like puma.rb), so even if we have it in the .env files,
|
15
|
+
# it won't get used, because .env files don't stomp what is already in the environment
|
16
|
+
# (we don't want to use `overload`).
|
17
|
+
# So we have some trickery to overwrite only PORT.
|
18
|
+
module Appydays::Dotenviable
|
19
|
+
def self.load(rack_env: nil, default_rack_env: "development", env: ENV)
|
20
|
+
original_port = env.delete("PORT")
|
21
|
+
rack_env ||= env["RACK_ENV"] || default_rack_env
|
22
|
+
paths = [
|
23
|
+
".env.#{rack_env}.local",
|
24
|
+
".env.#{rack_env}",
|
25
|
+
".env",
|
26
|
+
]
|
27
|
+
Dotenv.load(*paths)
|
28
|
+
env["PORT"] ||= original_port
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "semantic_logger"
|
4
|
+
|
5
|
+
require "appydays/version"
|
6
|
+
|
7
|
+
##
|
8
|
+
# Override SemanticLogger's keys for tags, named_tags, and payload.
|
9
|
+
# It is emminently unhelpful to have three places the same data may be-
|
10
|
+
# ie, in some cases, we may have a customer_id in a named tag,
|
11
|
+
# sometimes in payload, etc. Callsite behavior should not vary
|
12
|
+
# the shape/content of the log message.
|
13
|
+
class SemanticLogger::Formatters::Raw
|
14
|
+
alias original_call call
|
15
|
+
|
16
|
+
def call(log, logger)
|
17
|
+
h = self.original_call(log, logger)
|
18
|
+
ctx = h[:context] ||= {}
|
19
|
+
ctx[:_tags] = h.delete(:tags) if h.key?(:tags)
|
20
|
+
|
21
|
+
[:named_tags, :payload].each do |hash_key|
|
22
|
+
next unless h.key?(hash_key)
|
23
|
+
h.delete(hash_key).each do |k, v|
|
24
|
+
ctx[k] = v
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
return h
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Helpers for working with structured logging.
|
34
|
+
# Use this instead of calling semantic_logger directly.
|
35
|
+
# Generally you `include Appydays::Loggable`
|
36
|
+
module Appydays::Loggable
|
37
|
+
def self.included(target)
|
38
|
+
target.include(SemanticLogger::Loggable)
|
39
|
+
|
40
|
+
target.extend(Methods)
|
41
|
+
target.include(Methods)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.default_level=(v)
|
45
|
+
self.set_default_level(v)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.set_default_level(v, warning: true)
|
49
|
+
return if v == SemanticLogger.default_level
|
50
|
+
self[self].warn "Overriding log level to %p" % v if warning
|
51
|
+
SemanticLogger.default_level = v
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Return the logger for a key/object.
|
56
|
+
def self.[](key)
|
57
|
+
return key.logger if key.respond_to?(:logger)
|
58
|
+
(key = key.class) unless [Module, Class].include?(key.class)
|
59
|
+
return SemanticLogger[key]
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Configure logging for 12 factor applications.
|
64
|
+
# Specifically, that means setting STDOUT to synchronous,
|
65
|
+
# using STDOUT as the log output,
|
66
|
+
# and also conveniently using color formatting if using a tty or json otherwise
|
67
|
+
# (ie, you want to use json logging on a server).
|
68
|
+
def self.configure_12factor(format: nil, application: nil)
|
69
|
+
format ||= $stdout.isatty ? :color : :json
|
70
|
+
$stdout.sync = true
|
71
|
+
SemanticLogger.application = application if application
|
72
|
+
SemanticLogger.add_appender(io: $stdout, formatter: format.to_sym)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.with_log_tags(tags, &block)
|
76
|
+
return SemanticLogger.named_tagged(tags, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
@stderr_appended = false
|
80
|
+
|
81
|
+
def self.ensure_stderr_appender
|
82
|
+
return if @stderr_appended
|
83
|
+
SemanticLogger.add_appender(io: $stderr)
|
84
|
+
@stderr_appended = true
|
85
|
+
end
|
86
|
+
|
87
|
+
module Methods
|
88
|
+
def with_log_tags(tags, &block)
|
89
|
+
return SemanticLogger.named_tagged(tags, &block)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
require "securerandom"
|
5
|
+
|
6
|
+
require "appydays/loggable"
|
7
|
+
|
8
|
+
##
|
9
|
+
# Rack middleware for request logging, to replace Rack::CommonLogger.
|
10
|
+
# NOTE: To get additional log fields, you can subclass this and override `_request_tags`.
|
11
|
+
# If you want authenticated user info, make sure you install the middleware
|
12
|
+
# after your auth middleware.
|
13
|
+
#
|
14
|
+
# Params:
|
15
|
+
#
|
16
|
+
# error_code: If an unhandled exception reaches the request logger.
|
17
|
+
# or happens in the request logger, this status code will be used for easay identification.
|
18
|
+
# Generally unhandled exceptions in an application should be handled further downstream,
|
19
|
+
# and already be returning the right error code.
|
20
|
+
# slow_request_seconds: Normally request_finished is logged at info.
|
21
|
+
# Requests that take >= this many seconds are logged at warn.
|
22
|
+
# reraise: Reraise unhandled errors (after logging them).
|
23
|
+
# In some cases you may want this to be true, but in some it may bring down the whole process.
|
24
|
+
class Appydays::Loggable::RequestLogger
|
25
|
+
include Appydays::Loggable
|
26
|
+
|
27
|
+
def initialize(app, error_code: 599, slow_request_seconds: 10, reraise: true)
|
28
|
+
@app = app
|
29
|
+
@error_code = error_code
|
30
|
+
@slow_request_seconds = slow_request_seconds
|
31
|
+
@reraise = reraise
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(env)
|
35
|
+
began_at = Time.now
|
36
|
+
request_fields = self._request_tags(env, began_at)
|
37
|
+
status, header, body = SemanticLogger.named_tagged(request_fields) { @app.call(env) }
|
38
|
+
header = Rack::Utils::HeaderHash.new(header)
|
39
|
+
body = Rack::BodyProxy.new(body) { self.log_finished(request_fields, began_at, status, header) }
|
40
|
+
[status, header, body]
|
41
|
+
rescue StandardError => e
|
42
|
+
began_at ||= nil
|
43
|
+
request_fields ||= {}
|
44
|
+
self.log_finished(request_fields, began_at, 599, {}, e)
|
45
|
+
raise if @reraise
|
46
|
+
end
|
47
|
+
|
48
|
+
protected def _request_tags(env, began_at)
|
49
|
+
req_id = SecureRandom.uuid.to_s
|
50
|
+
env["HTTP_TRACE_ID"] ||= req_id
|
51
|
+
return {
|
52
|
+
remote_addr: env["HTTP_X_FORWARDED_FOR"] || env["REMOTE_ADDR"] || "-",
|
53
|
+
request_started_at: began_at.to_s,
|
54
|
+
request_method: env[Rack::REQUEST_METHOD],
|
55
|
+
request_path: env[Rack::PATH_INFO],
|
56
|
+
request_query: env.fetch(Rack::QUERY_STRING, "").empty? ? "" : "?#{env[Rack::QUERY_STRING]}",
|
57
|
+
request_id: req_id,
|
58
|
+
trace_id: env["HTTP_TRACE_ID"],
|
59
|
+
}.merge(self.request_tags(env))
|
60
|
+
end
|
61
|
+
|
62
|
+
def request_tags(_env)
|
63
|
+
return {}
|
64
|
+
end
|
65
|
+
|
66
|
+
protected def log_finished(request_tags, began_at, status, header, exc=nil)
|
67
|
+
elapsed = (Time.now - began_at).to_f
|
68
|
+
|
69
|
+
all_tags = request_tags.merge(
|
70
|
+
response_finished_at: Time.now.to_s,
|
71
|
+
response_status: status,
|
72
|
+
response_content_length: extract_content_length(header),
|
73
|
+
response_ms: elapsed * 1000,
|
74
|
+
)
|
75
|
+
|
76
|
+
level = if status >= 500
|
77
|
+
"error"
|
78
|
+
elsif elapsed >= @slow_request_seconds
|
79
|
+
"warn"
|
80
|
+
else
|
81
|
+
"info"
|
82
|
+
end
|
83
|
+
|
84
|
+
self.logger.tagged(all_tags) do
|
85
|
+
self.logger.send(level, "request_finished", exc)
|
86
|
+
end
|
87
|
+
SemanticLogger.flush
|
88
|
+
end
|
89
|
+
|
90
|
+
protected def extract_content_length(headers)
|
91
|
+
value = headers[Rack::CONTENT_LENGTH]
|
92
|
+
return !value || value.to_s == "0" ? "-" : value
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Patch Sequel::Database logging methods for structured logging
|
4
|
+
require "sequel/database/logging"
|
5
|
+
|
6
|
+
class Sequel::Database
|
7
|
+
def log_exception(exception, message)
|
8
|
+
log_each(
|
9
|
+
:error,
|
10
|
+
proc { "#{exception.class}: #{exception.message.strip if exception.message}: #{message}" },
|
11
|
+
proc { ["sequel_exception", {sequel_message: message}, exception] },
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Log a message at level info to all loggers.
|
16
|
+
def log_info(message, args=nil)
|
17
|
+
log_each(
|
18
|
+
:info,
|
19
|
+
proc { args ? "#{message}; #{args.inspect}" : message },
|
20
|
+
proc { ["sequel_log", {message: message, args: args}] },
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Log message with message prefixed by duration at info level, or
|
25
|
+
# warn level if duration is greater than log_warn_duration.
|
26
|
+
def log_duration(duration, message)
|
27
|
+
lwd = log_warn_duration
|
28
|
+
log_each(
|
29
|
+
lwd && (duration >= lwd) ? :warn : sql_log_level,
|
30
|
+
proc { "(#{'%0.6fs' % duration}) #{message}" },
|
31
|
+
proc { ["sequel_query", {duration: duration, query: message}] },
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def log_each(level, std, semantic)
|
36
|
+
@loggers.each do |logger|
|
37
|
+
if logger.is_a?(SemanticLogger::Base)
|
38
|
+
logger.public_send(level, *semantic.call)
|
39
|
+
else
|
40
|
+
logger.public_send(level, std.call)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "appydays/loggable"
|
4
|
+
|
5
|
+
module Appydays::Loggable::SpecHelpers
|
6
|
+
def self.included(context)
|
7
|
+
# Appydays::Loggable.ensure_stderr_appender
|
8
|
+
|
9
|
+
context.around(:each) do |example|
|
10
|
+
override_level = (example.metadata[:log] || example.metadata[:logging])
|
11
|
+
if override_level
|
12
|
+
orig_level = SemanticLogger.default_level
|
13
|
+
SemanticLogger.default_level = override_level
|
14
|
+
end
|
15
|
+
example.run
|
16
|
+
SemanticLogger.default_level = orig_level if override_level
|
17
|
+
end
|
18
|
+
|
19
|
+
context.before(:all) do
|
20
|
+
Appydays::Loggable.set_default_level(:fatal, warning: false)
|
21
|
+
end
|
22
|
+
|
23
|
+
context.after(:all) do
|
24
|
+
Appydays::Loggable.set_default_level(:fatal, warning: false)
|
25
|
+
end
|
26
|
+
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def capture_logs_from(loggers, level: "debug", formatter: nil)
|
31
|
+
(loggers = [loggers]) unless loggers.respond_to?(:to_ary)
|
32
|
+
|
33
|
+
existing_appenders_and_lvls = SemanticLogger.appenders.map { |app| [app, app.level] }
|
34
|
+
SemanticLogger.appenders.each { |app| app.level = :fatal }
|
35
|
+
original_levels_and_loggers = loggers.map { |log| [log, log.level] }
|
36
|
+
loggers.each { |log| log.level = level }
|
37
|
+
|
38
|
+
io = StringIO.new
|
39
|
+
appender = SemanticLogger.add_appender(io: io, level: level)
|
40
|
+
appender.formatter = formatter if formatter
|
41
|
+
begin
|
42
|
+
yield
|
43
|
+
ensure
|
44
|
+
SemanticLogger.flush
|
45
|
+
SemanticLogger.remove_appender(appender)
|
46
|
+
original_levels_and_loggers.each { |(log, lvl)| log.level = lvl }
|
47
|
+
existing_appenders_and_lvls.each { |(app, lvl)| app.level = lvl }
|
48
|
+
end
|
49
|
+
return io.string.lines
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "appydays/version"
|
4
|
+
|
5
|
+
RSpec::Matchers.define_negated_matcher(:exclude, :include)
|
6
|
+
RSpec::Matchers.define_negated_matcher(:not_include, :include)
|
7
|
+
RSpec::Matchers.define_negated_matcher(:not_change, :change)
|
8
|
+
RSpec::Matchers.define_negated_matcher(:not_be_nil, :be_nil)
|
9
|
+
RSpec::Matchers.define_negated_matcher(:not_be_empty, :be_empty)
|
10
|
+
|
11
|
+
module Appydays::SpecHelpers
|
12
|
+
# Zero out nsecs to t can be compared to one from the database.
|
13
|
+
module_function def trunc_time(t)
|
14
|
+
return t.change(nsec: t.usec * 1000)
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# :section: Matchers
|
19
|
+
#
|
20
|
+
|
21
|
+
class HaveALineMatching
|
22
|
+
def initialize(regexp)
|
23
|
+
@regexp = regexp
|
24
|
+
end
|
25
|
+
|
26
|
+
def matches?(target)
|
27
|
+
@target = target
|
28
|
+
return @target.find do |obj|
|
29
|
+
obj.to_s.match(@regexp)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def failure_message
|
34
|
+
return "expected %p to have at least one line matching %p" % [@target, @regexp]
|
35
|
+
end
|
36
|
+
|
37
|
+
alias failure_message_for_should failure_message
|
38
|
+
|
39
|
+
def failure_message_when_negated
|
40
|
+
return "expected %p not to have any lines matching %p, but it has at least one" % [@target, @regexp]
|
41
|
+
end
|
42
|
+
|
43
|
+
alias failure_message_for_should_not failure_message_when_negated
|
44
|
+
end
|
45
|
+
|
46
|
+
### RSpec matcher -- set up the expectation that the lefthand side
|
47
|
+
### is Enumerable, and that at least one of the objects yielded
|
48
|
+
### while iterating matches +regexp+ when converted to a String.
|
49
|
+
module_function def have_a_line_matching(regexp)
|
50
|
+
return HaveALineMatching.new(regexp)
|
51
|
+
end
|
52
|
+
|
53
|
+
module_function def have_length(x)
|
54
|
+
return RSpec::Matchers::BuiltIn::HaveAttributes.new(length: x)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Matcher that will compare a string or time expected against a string or time actual,
|
58
|
+
# within a tolerance (default to 1 millisecond).
|
59
|
+
#
|
60
|
+
# expect(last_response).to have_json_body.that_includes(
|
61
|
+
# closes_at: match_time('2025-12-01T00:00:00.000+00:00').within(1.second))
|
62
|
+
#
|
63
|
+
RSpec::Matchers.define(:match_time) do |expected|
|
64
|
+
match do |actual|
|
65
|
+
@tolerance ||= 0.001
|
66
|
+
RSpec::Matchers::BuiltIn::BeWithin.new(@tolerance).of(self.time(expected)).matches?(self.time(actual))
|
67
|
+
end
|
68
|
+
|
69
|
+
failure_message do |actual|
|
70
|
+
"expected ids %s to be within %s of %s" % [self.time(actual), @tolerance, self.time(expected)]
|
71
|
+
end
|
72
|
+
|
73
|
+
chain :within do |tolerance|
|
74
|
+
@tolerance = tolerance
|
75
|
+
end
|
76
|
+
|
77
|
+
def time(s)
|
78
|
+
return Time.parse(s) if s.is_a?(String)
|
79
|
+
return s.to_time
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Matcher that will compare a string or Money expected against a string or Money actual.
|
84
|
+
#
|
85
|
+
# expect(order.total).to cost('$25')
|
86
|
+
#
|
87
|
+
RSpec::Matchers.define(:cost) do |expected|
|
88
|
+
match do |actual|
|
89
|
+
@base = RSpec::Matchers::BuiltIn::Eq.new(self.money(expected))
|
90
|
+
@base.matches?(self.money(actual))
|
91
|
+
end
|
92
|
+
|
93
|
+
failure_message do |_actual|
|
94
|
+
@base.failure_message
|
95
|
+
end
|
96
|
+
|
97
|
+
def money(s)
|
98
|
+
return Monetize.parse(s) if s.is_a?(String)
|
99
|
+
return s if s.is_a?(Money)
|
100
|
+
return Money.new(s) if s.is_a?(Integer)
|
101
|
+
return Money.new(s[:cents], s[:currency]) if s.respond_to?(:key?) && s.key?(:cents) && s.key?(:currency)
|
102
|
+
raise "#{s} type #{s.class.name} not convertable to Money (add support or use supported type)"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Appydays::Configurable do
|
4
|
+
describe "configurable" do
|
5
|
+
it "raises if no block is given" do
|
6
|
+
expect do
|
7
|
+
Class.new do
|
8
|
+
include Appydays::Configurable
|
9
|
+
configurable(:hello)
|
10
|
+
end
|
11
|
+
end.to raise_error(LocalJumpError)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "setting" do
|
15
|
+
it "creates an attr accessor with the given name and default value" do
|
16
|
+
cls = Class.new do
|
17
|
+
include Appydays::Configurable
|
18
|
+
configurable(:hello) do
|
19
|
+
setting :knob, "zero"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
expect(cls).to have_attributes(knob: "zero")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "pulls the value from the environment" do
|
26
|
+
ENV["ENVTEST_KNOB"] = "one"
|
27
|
+
cls = Class.new do
|
28
|
+
include Appydays::Configurable
|
29
|
+
configurable(:envtest) do
|
30
|
+
setting :knob, "zero"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
expect(cls).to have_attributes(knob: "one")
|
34
|
+
end
|
35
|
+
|
36
|
+
it "can use a custom environment key" do
|
37
|
+
ENV["OTHER_KNOB"] = "two"
|
38
|
+
cls = Class.new do
|
39
|
+
include Appydays::Configurable
|
40
|
+
configurable(:hello) do
|
41
|
+
setting :knob, "zero", key: "OTHER_KNOB"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
expect(cls).to have_attributes(knob: "two")
|
45
|
+
end
|
46
|
+
|
47
|
+
it "can convert the value given the converter" do
|
48
|
+
ENV["CONVTEST_KNOB"] = "0"
|
49
|
+
cls = Class.new do
|
50
|
+
include Appydays::Configurable
|
51
|
+
configurable(:convtest) do
|
52
|
+
setting :knob, "", convert: ->(v) { v + v }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
expect(cls).to have_attributes(knob: "00")
|
56
|
+
end
|
57
|
+
|
58
|
+
it "does not run the converter if the default is used" do
|
59
|
+
cls = Class.new do
|
60
|
+
include Appydays::Configurable
|
61
|
+
configurable(:hello) do
|
62
|
+
setting :knob, "0", convert: ->(v) { v + v }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
expect(cls).to have_attributes(knob: "0")
|
66
|
+
end
|
67
|
+
|
68
|
+
it "can use a nil default" do
|
69
|
+
cls = Class.new do
|
70
|
+
include Appydays::Configurable
|
71
|
+
configurable(:hello) do
|
72
|
+
setting :knob, nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
expect(cls).to have_attributes(knob: nil)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "converts strings to floats if the default is a float" do
|
79
|
+
ENV["FLOATTEST_KNOB"] = "3.2"
|
80
|
+
cls = Class.new do
|
81
|
+
include Appydays::Configurable
|
82
|
+
configurable(:floattest) do
|
83
|
+
setting :knob, 1.5
|
84
|
+
end
|
85
|
+
end
|
86
|
+
expect(cls).to have_attributes(knob: 3.2)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "converts strings to integers if the default is an integer" do
|
90
|
+
ENV["INTTEST_KNOB"] = "5"
|
91
|
+
cls = Class.new do
|
92
|
+
include Appydays::Configurable
|
93
|
+
configurable(:inttest) do
|
94
|
+
setting :knob, 2
|
95
|
+
end
|
96
|
+
end
|
97
|
+
expect(cls).to have_attributes(knob: 5)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "can coerce strings to booleans" do
|
101
|
+
ENV["BOOLTEST_KNOB"] = "TRue"
|
102
|
+
cls = Class.new do
|
103
|
+
include Appydays::Configurable
|
104
|
+
configurable(:booltest) do
|
105
|
+
setting :knob, false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
expect(cls).to have_attributes(knob: true)
|
109
|
+
end
|
110
|
+
|
111
|
+
it "does not run the converter when using the accessor" do
|
112
|
+
cls = Class.new do
|
113
|
+
include Appydays::Configurable
|
114
|
+
configurable(:booltest) do
|
115
|
+
setting :knob, 5
|
116
|
+
end
|
117
|
+
end
|
118
|
+
cls.knob = "5"
|
119
|
+
expect(cls).to have_attributes(knob: "5")
|
120
|
+
end
|
121
|
+
|
122
|
+
it "coalesces an empty string to nil" do
|
123
|
+
cls = Class.new do
|
124
|
+
include Appydays::Configurable
|
125
|
+
configurable(:hello) do
|
126
|
+
setting :knob, ""
|
127
|
+
end
|
128
|
+
end
|
129
|
+
expect(cls).to have_attributes(knob: nil)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "errors if the default value is not a supported type" do
|
133
|
+
expect do
|
134
|
+
Class.new do
|
135
|
+
include Appydays::Configurable
|
136
|
+
configurable(:hello) do
|
137
|
+
setting :knob, []
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end.to raise_error(TypeError)
|
141
|
+
end
|
142
|
+
|
143
|
+
it "runs a side effect" do
|
144
|
+
side_effect = []
|
145
|
+
Class.new do
|
146
|
+
include Appydays::Configurable
|
147
|
+
configurable(:hello) do
|
148
|
+
setting :knob, "zero", side_effect: ->(s) { side_effect << s }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
expect(side_effect).to contain_exactly("zero")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
it "can reset settings" do
|
156
|
+
cls = Class.new do
|
157
|
+
include Appydays::Configurable
|
158
|
+
configurable(:hello) do
|
159
|
+
setting :knob, 1
|
160
|
+
end
|
161
|
+
end
|
162
|
+
cls.knob = 5
|
163
|
+
expect(cls).to have_attributes(knob: 5)
|
164
|
+
cls.reset_configuration
|
165
|
+
expect(cls).to have_attributes(knob: 1)
|
166
|
+
end
|
167
|
+
|
168
|
+
it "runs after_configure hooks after configuration" do
|
169
|
+
side_effect = []
|
170
|
+
Class.new do
|
171
|
+
include Appydays::Configurable
|
172
|
+
configurable(:hello) do
|
173
|
+
setting :knob, 1
|
174
|
+
after_configured do
|
175
|
+
side_effect << self.knob
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
expect(side_effect).to contain_exactly(1)
|
180
|
+
end
|
181
|
+
|
182
|
+
it "can run after_configured hooks explicitly" do
|
183
|
+
side_effect = []
|
184
|
+
cls = Class.new do
|
185
|
+
include Appydays::Configurable
|
186
|
+
configurable(:hello) do
|
187
|
+
setting :knob, 1
|
188
|
+
after_configured do
|
189
|
+
side_effect << self.knob
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
cls.run_after_configured_hooks
|
194
|
+
expect(side_effect).to contain_exactly(1, 1)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Appydays::Dotenviable do
|
4
|
+
it "loads env files using RACK_ENV" do
|
5
|
+
expect(Dotenv).to receive(:load).with(".env.foo.local", ".env.foo", ".env")
|
6
|
+
described_class.load(env: {"RACK_ENV" => "foo"})
|
7
|
+
end
|
8
|
+
|
9
|
+
it "loads env files using the explicit env" do
|
10
|
+
expect(Dotenv).to receive(:load).with(".env.bar.local", ".env.bar", ".env")
|
11
|
+
described_class.load(rack_env: "bar")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "loads env files with the given default env if no RACK_ENV is defined" do
|
15
|
+
expect(Dotenv).to receive(:load).with(".env.bar.local", ".env.bar", ".env")
|
16
|
+
described_class.load(default_rack_env: "bar", env: {})
|
17
|
+
end
|
18
|
+
|
19
|
+
it "loads env files with RACK_ENV rather than the default, if RACK_ENV is defined" do
|
20
|
+
expect(Dotenv).to receive(:load).with(".env.bar.local", ".env.bar", ".env")
|
21
|
+
described_class.load(default_rack_env: "foo", env: {"RACK_ENV" => "bar"})
|
22
|
+
end
|
23
|
+
|
24
|
+
it "reapplies the original port if one was not loaded" do
|
25
|
+
env = {"PORT" => "123"}
|
26
|
+
expect(Dotenv).to receive(:load)
|
27
|
+
described_class.load(env: env)
|
28
|
+
expect(env).to include("PORT" => "123")
|
29
|
+
end
|
30
|
+
|
31
|
+
it "does not reapply the original port if one was loaded" do
|
32
|
+
env = {"PORT" => "123"}
|
33
|
+
expect(Dotenv).to receive(:load) { env["PORT"] = "456" }
|
34
|
+
described_class.load(env: env)
|
35
|
+
expect(env).to include("PORT" => "456")
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "appydays/loggable/request_logger"
|
4
|
+
|
5
|
+
RSpec.describe Appydays::Loggable do
|
6
|
+
it "can set the default log level" do
|
7
|
+
expect(SemanticLogger).to receive(:default_level=).with("trace")
|
8
|
+
described_class.default_level = "trace"
|
9
|
+
end
|
10
|
+
|
11
|
+
it "can look up the logger for an object for a non-Loggable" do
|
12
|
+
cls = Class.new
|
13
|
+
cls_logger = described_class[cls]
|
14
|
+
inst_logger = described_class[cls.new]
|
15
|
+
expect(cls_logger).to be_a(SemanticLogger::Logger)
|
16
|
+
expect(inst_logger).to be_a(SemanticLogger::Logger)
|
17
|
+
expect(cls_logger.name).to eq(inst_logger.name)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "can look up the logger for an object for a Loggable" do
|
21
|
+
cls = Class.new do
|
22
|
+
include Appydays::Loggable
|
23
|
+
end
|
24
|
+
cls_logger = described_class[cls]
|
25
|
+
inst = cls.new
|
26
|
+
inst_logger = described_class[inst]
|
27
|
+
expect(cls_logger).to be(inst_logger)
|
28
|
+
expect(cls.logger).to be(inst.logger)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "adds logger methods" do
|
32
|
+
cls = Class.new do
|
33
|
+
include Appydays::Loggable
|
34
|
+
end
|
35
|
+
inst = cls.new
|
36
|
+
expect(cls.logger).to be_a(SemanticLogger::Logger)
|
37
|
+
expect(inst.logger).to be_a(SemanticLogger::Logger)
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "custom formatting" do
|
41
|
+
it "combines :payload, :tags, and :named_tags into :context" do
|
42
|
+
logger1 = described_class["spec-helper-test"]
|
43
|
+
|
44
|
+
lines = capture_logs_from(logger1, formatter: :json) do
|
45
|
+
SemanticLogger.tagged("tag1", "tag2") do
|
46
|
+
SemanticLogger.named_tagged(nt1: 1, nt2: 2) do
|
47
|
+
logger1.error("hello", opt1: 1, opt2: 2)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
j = JSON.parse(lines[0])
|
52
|
+
expect(j).to include("context")
|
53
|
+
expect(j["context"]).to eq(
|
54
|
+
"_tags" => ["tag1", "tag2"],
|
55
|
+
"nt1" => 1,
|
56
|
+
"nt2" => 2,
|
57
|
+
"opt1" => 1,
|
58
|
+
"opt2" => 2,
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "spec helpers" do
|
64
|
+
logger1 = described_class["spec-helper-test"]
|
65
|
+
|
66
|
+
it "can capture log lines to a logger" do
|
67
|
+
lines = capture_logs_from(logger1) do
|
68
|
+
logger1.error("hello there")
|
69
|
+
end
|
70
|
+
expect(lines).to have_a_line_matching(/hello there/)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "can capture log lines to multiple loggers" do
|
74
|
+
lines = capture_logs_from(logger1) do
|
75
|
+
logger1.error("hello there")
|
76
|
+
end
|
77
|
+
expect(lines).to have_a_line_matching(/hello there/)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "can filter logs below a level" do
|
81
|
+
lines = capture_logs_from(logger1, level: :error) do
|
82
|
+
logger1.warn("hello there")
|
83
|
+
end
|
84
|
+
expect(lines).to be_empty
|
85
|
+
end
|
86
|
+
|
87
|
+
it "can specify the formatter" do
|
88
|
+
lines = capture_logs_from(logger1, formatter: :json) do
|
89
|
+
logger1.warn("hello there")
|
90
|
+
end
|
91
|
+
expect(lines).to have_a_line_matching(/"message":"hello there"/)
|
92
|
+
|
93
|
+
lines = capture_logs_from(logger1, formatter: :color) do
|
94
|
+
logger1.warn("hello there")
|
95
|
+
end
|
96
|
+
expect(lines).to have_a_line_matching(/-- hello there/)
|
97
|
+
end
|
98
|
+
|
99
|
+
it "sets and restores the level of all appenders" do
|
100
|
+
logger1.level = :info
|
101
|
+
other_appender = SemanticLogger.add_appender(io: StringIO.new, level: :trace)
|
102
|
+
capture_logs_from(logger1, level: :trace) do
|
103
|
+
expect(logger1.level).to eq(:trace)
|
104
|
+
expect(other_appender.level).to eq(:fatal)
|
105
|
+
end
|
106
|
+
expect(logger1.level).to eq(:info)
|
107
|
+
expect(other_appender.level).to eq(:trace)
|
108
|
+
SemanticLogger.remove_appender(other_appender)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe Appydays::Loggable::RequestLogger do
|
113
|
+
def run_app(app, opts: {}, loggers: [], env: {}, cls: Appydays::Loggable::RequestLogger)
|
114
|
+
rl = cls.new(app, **opts.merge(reraise: false))
|
115
|
+
return capture_logs_from(loggers << rl.logger, formatter: :json) do
|
116
|
+
_, _, body = rl.call(env)
|
117
|
+
body&.close
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
it "logs info about the request" do
|
122
|
+
lines = run_app(proc { [200, {}, ""] })
|
123
|
+
expect(lines).to have_a_line_matching(/"message":"request_finished".*"response_status":200/)
|
124
|
+
|
125
|
+
lines = run_app(proc { [400, {}, ""] })
|
126
|
+
expect(lines).to have_a_line_matching(/"message":"request_finished".*"response_status":400/)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "logs at 599 (or configured value) if something errors" do
|
130
|
+
lines = run_app(proc { raise "testing error" })
|
131
|
+
expect(lines).to have_a_line_matching(/"level":"error".*"response_status":599/)
|
132
|
+
expect(lines).to have_a_line_matching(/"message":"testing error"/)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "logs slow queries at warn" do
|
136
|
+
lines = run_app(proc { [200, {}, ""] }, opts: {slow_request_seconds: 0})
|
137
|
+
expect(lines).to have_a_line_matching(/"level":"warn".*"response_status":200/)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "logs errors at error" do
|
141
|
+
lines = run_app(proc { [504, {}, ""] }, opts: {slow_request_seconds: 0})
|
142
|
+
expect(lines).to have_a_line_matching(/"level":"error".*"response_status":504/)
|
143
|
+
end
|
144
|
+
|
145
|
+
it "adds tags around the execution of the request" do
|
146
|
+
logger = SemanticLogger["testlogger"]
|
147
|
+
lines = run_app(proc do
|
148
|
+
logger.info("check for tags")
|
149
|
+
[200, {}, ""]
|
150
|
+
end,
|
151
|
+
opts: {slow_request_seconds: 0}, loggers: [logger],)
|
152
|
+
expect(lines).to have_a_line_matching(/"message":"check for tags".*"request_method":/)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "adds subclass tags" do
|
156
|
+
ReqLogger = Class.new(Appydays::Loggable::RequestLogger) do
|
157
|
+
def request_tags(env)
|
158
|
+
return {my_header_tag: env["HTTP_MY_HEADER"]}
|
159
|
+
end
|
160
|
+
end
|
161
|
+
lines = run_app(proc { [200, {}, ""] }, env: {"HTTP_MY_HEADER" => "myval"}, cls: ReqLogger)
|
162
|
+
expect(lines).to have_a_line_matching(/"my_header_tag":"myval"/)
|
163
|
+
end
|
164
|
+
|
165
|
+
it "adds a request id" do
|
166
|
+
lines = run_app(proc { [200, {}, ""] })
|
167
|
+
expect(lines).to have_a_line_matching(/"request_id":"[0-9a-z]{8}-/)
|
168
|
+
end
|
169
|
+
|
170
|
+
it "reads a trace id from headers" do
|
171
|
+
lines = run_app(proc { [200, {}, ""] }, env: {"HTTP_TRACE_ID" => "123xyz"})
|
172
|
+
expect(lines).to have_a_line_matching(/"trace_id":"123xyz"/)
|
173
|
+
end
|
174
|
+
|
175
|
+
it "sets the trace ID header if not set" do
|
176
|
+
env = {}
|
177
|
+
lines = run_app(proc do
|
178
|
+
expect(env).to(include("HTTP_TRACE_ID"))
|
179
|
+
[200, {}, ""]
|
180
|
+
end, env: env,)
|
181
|
+
expect(lines).to have_a_line_matching(/"trace_id":"[0-9a-z]{8}-/)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# See https://github.com/eliotsykes/rspec-rails-examples/blob/master/spec/spec_helper.rb
|
4
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
5
|
+
#
|
6
|
+
require "appydays/dotenviable"
|
7
|
+
Appydays::Dotenviable.load(default_rack_env: "test")
|
8
|
+
|
9
|
+
require "rspec"
|
10
|
+
require "rspec/json_expectations"
|
11
|
+
require "appydays/loggable/spec_helpers"
|
12
|
+
require "appydays/spec_helpers"
|
13
|
+
require "appydays/configurable"
|
14
|
+
|
15
|
+
RSpec.configure do |config|
|
16
|
+
# config.full_backtrace = true
|
17
|
+
|
18
|
+
RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 600
|
19
|
+
|
20
|
+
config.expect_with :rspec do |expectations|
|
21
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
22
|
+
end
|
23
|
+
|
24
|
+
config.mock_with :rspec do |mocks|
|
25
|
+
mocks.verify_partial_doubles = true
|
26
|
+
end
|
27
|
+
|
28
|
+
config.order = :random
|
29
|
+
Kernel.srand config.seed
|
30
|
+
|
31
|
+
config.filter_run :focus
|
32
|
+
config.run_all_when_everything_filtered = true
|
33
|
+
config.disable_monkey_patching!
|
34
|
+
config.default_formatter = "doc" if config.files_to_run.one?
|
35
|
+
|
36
|
+
config.include(Appydays::Loggable::SpecHelpers)
|
37
|
+
config.include(Appydays::SpecHelpers)
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: appydays
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lithic Tech
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-03-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dotenv
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: semantic_logger
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rack
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec-core
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec-json_expectations
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-performance
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop-rake
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop-sequel
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: sequel
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
description: 'appydays provides support for logging and handling environment variables
|
168
|
+
|
169
|
+
'
|
170
|
+
email:
|
171
|
+
executables: []
|
172
|
+
extensions: []
|
173
|
+
extra_rdoc_files: []
|
174
|
+
files:
|
175
|
+
- README.md
|
176
|
+
- lib/appydays.rb
|
177
|
+
- lib/appydays/configurable.rb
|
178
|
+
- lib/appydays/dotenviable.rb
|
179
|
+
- lib/appydays/loggable.rb
|
180
|
+
- lib/appydays/loggable/request_logger.rb
|
181
|
+
- lib/appydays/loggable/sequel_logger.rb
|
182
|
+
- lib/appydays/loggable/spec_helpers.rb
|
183
|
+
- lib/appydays/spec_helpers.rb
|
184
|
+
- lib/appydays/version.rb
|
185
|
+
- spec/appydays/configurable_spec.rb
|
186
|
+
- spec/appydays/dotenviable_spec.rb
|
187
|
+
- spec/appydays/loggable_spec.rb
|
188
|
+
- spec/spec_helper.rb
|
189
|
+
homepage:
|
190
|
+
licenses:
|
191
|
+
- MIT
|
192
|
+
metadata: {}
|
193
|
+
post_install_message:
|
194
|
+
rdoc_options: []
|
195
|
+
require_paths:
|
196
|
+
- lib
|
197
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: 2.7.2
|
202
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
203
|
+
requirements:
|
204
|
+
- - ">="
|
205
|
+
- !ruby/object:Gem::Version
|
206
|
+
version: '0'
|
207
|
+
requirements: []
|
208
|
+
rubygems_version: 3.1.4
|
209
|
+
signing_key:
|
210
|
+
specification_version: 4
|
211
|
+
summary: Provides support for development best practices
|
212
|
+
test_files: []
|