api_hammer 0.13.3 → 0.14.0
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 +4 -4
- data/.travis.yml +3 -6
- data/CHANGELOG.md +12 -2
- data/api_hammer.gemspec +1 -1
- data/lib/api_hammer/content_type_attrs.rb +2 -2
- data/lib/api_hammer/faraday/request_logger.rb +24 -27
- data/lib/api_hammer/filtration/form_encoded.rb +1 -1
- data/lib/api_hammer/rails/check_required_params.rb +3 -3
- data/lib/api_hammer/rails_request_logging.rb +17 -9
- data/lib/api_hammer/request_logger.rb +54 -36
- data/lib/api_hammer/trailing_newline.rb +1 -1
- data/lib/api_hammer/version.rb +1 -1
- data/lib/api_hammer.rb +6 -0
- data/test/check_required_params_test.rb +6 -0
- data/test/faraday_request_logger_test.rb +14 -0
- data/test/halt_test.rb +0 -1
- metadata +11 -17
- data/Gemfile_ar_3 +0 -8
- data/Gemfile_ar_40 +0 -8
- data/Gemfile_ar_41 +0 -8
- data/lib/api_hammer/active_record_cache_find_by.rb +0 -134
- data/test/active_record_cache_find_by_test.rb +0 -226
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 909efcbfb3f192ecb429eaf23a7dce4a1d07dd68
|
4
|
+
data.tar.gz: 9526647e571cdce3a3f39911dbaa116897d8277b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d965bcb0458ef5c542e1ab0ebc2b0cfc4955ca98d1231def62582cc458aad1450bc897e31d1bd1803696f438bd5b2e81ac0769cfa10b18d998c6150912132be8
|
7
|
+
data.tar.gz: 95ba356edbba139809480c576e632e0cf464dbc9b854d11fcd37dce1b82375d338020a320bf801c0f31fdb2149cce6ed42e900b04c1dc4c8a74b781543e60b61
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,18 @@
|
|
1
|
+
# v0.14.0
|
2
|
+
- some rails 5 support
|
3
|
+
- check_required_params to support ActionController::Parameters #44
|
4
|
+
- handle not loading deprecated log_tailer when not found #33
|
5
|
+
- use ruby String#bytesize instead of Rack::Util #34
|
6
|
+
- fix same bug as v0.13.3 with logging non-ascii bodies on faraday logger #35
|
7
|
+
- remove ActiveRecord cache_find_by
|
8
|
+
- option :log_bodies for request loggers
|
9
|
+
- fix warnings when run with -w
|
10
|
+
|
1
11
|
# v0.13.3
|
2
|
-
- fix bug when logging non-ascii bodies with filtration enabled
|
12
|
+
- fix bug when logging non-ascii bodies with filtration enabled #31
|
3
13
|
|
4
14
|
# v0.13.2
|
5
|
-
- fix with_indifferent_access usage when we don't depend on activesupport
|
15
|
+
- fix with_indifferent_access usage when we don't depend on activesupport #29
|
6
16
|
|
7
17
|
# v0.13.1
|
8
18
|
- ApiHammer::Sinatra class method use_with_lint
|
data/api_hammer.gemspec
CHANGED
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_dependency 'json'
|
28
28
|
spec.add_dependency 'addressable'
|
29
29
|
spec.add_dependency 'coderay'
|
30
|
+
spec.add_dependency 'i18n'
|
30
31
|
spec.add_development_dependency 'rake'
|
31
32
|
spec.add_development_dependency 'rack-test'
|
32
33
|
spec.add_development_dependency 'minitest'
|
@@ -34,6 +35,5 @@ Gem::Specification.new do |spec|
|
|
34
35
|
spec.add_development_dependency 'yard'
|
35
36
|
spec.add_development_dependency 'simplecov'
|
36
37
|
spec.add_development_dependency 'activesupport'
|
37
|
-
spec.add_development_dependency 'activerecord'
|
38
38
|
spec.add_development_dependency 'sqlite3'
|
39
39
|
end
|
@@ -2,7 +2,7 @@ module ApiHammer
|
|
2
2
|
# parses attributes out of content type header
|
3
3
|
class ContentTypeAttrs
|
4
4
|
def initialize(content_type)
|
5
|
-
@media_type = content_type.split(/\s*[;]\s*/, 2).first if content_type
|
5
|
+
@media_type = (content_type.split(/\s*[;]\s*/, 2).first if content_type)
|
6
6
|
@media_type.strip! if @media_type
|
7
7
|
@content_type = content_type
|
8
8
|
@parsed = false
|
@@ -12,7 +12,7 @@ module ApiHammer
|
|
12
12
|
uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
13
13
|
scanner = StringScanner.new(content_type)
|
14
14
|
scanner.scan(/.*;\s*/) || throw(:unparseable)
|
15
|
-
while
|
15
|
+
while scanner.scan(/(\w+)=("?)([^"]*)("?)\s*(,?)\s*/)
|
16
16
|
key = scanner[1]
|
17
17
|
quote1 = scanner[2]
|
18
18
|
value = scanner[3]
|
@@ -2,6 +2,7 @@ require 'faraday'
|
|
2
2
|
require 'term/ansicolor'
|
3
3
|
require 'json'
|
4
4
|
require 'strscan'
|
5
|
+
require 'api_hammer/request_logger'
|
5
6
|
|
6
7
|
module ApiHammer
|
7
8
|
module Faraday
|
@@ -15,8 +16,10 @@ module ApiHammer
|
|
15
16
|
#
|
16
17
|
# options:
|
17
18
|
# - :filter_keys defines keys whose values will be filtered out of the logging
|
19
|
+
# - :log_bodies - true, false, :on_error
|
18
20
|
class RequestLogger < ::Faraday::Middleware
|
19
21
|
include Term::ANSIColor
|
22
|
+
include ApiHammer::RequestLoggerHelper
|
20
23
|
|
21
24
|
def initialize(app, logger, options={})
|
22
25
|
@app = app
|
@@ -34,30 +37,24 @@ module ApiHammer
|
|
34
37
|
|
35
38
|
@app.call(request_env).on_complete do |response_env|
|
36
39
|
now = Time.now
|
40
|
+
status = response_env.status
|
37
41
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
['request', request_body, request_env.request_headers],
|
52
|
-
['response', response_env.body, response_env.response_headers]
|
53
|
-
].map do |(role, body, headers)|
|
54
|
-
{role => Body.new(body, headers['Content-Type']).jsonifiable}
|
55
|
-
end.inject({}, &:update)
|
56
|
-
|
57
|
-
if @options[:filter_keys]
|
58
|
-
bodies = bodies.map do |(role, body)|
|
59
|
-
{role => body.filtered(@options.slice(:filter_keys))}
|
42
|
+
if log_bodies(status)
|
43
|
+
bodies = [
|
44
|
+
['request', request_body, request_env.request_headers],
|
45
|
+
['response', response_env.body, response_env.response_headers]
|
46
|
+
].map do |(role, body_s, headers)|
|
47
|
+
body = Body.new(body_s, headers['Content-Type'])
|
48
|
+
if body.content_type_attrs.text?
|
49
|
+
if @options[:filter_keys]
|
50
|
+
body = body.filtered(:filter_keys => @options[:filter_keys])
|
51
|
+
end
|
52
|
+
log_body = body.jsonifiable.body
|
53
|
+
end
|
54
|
+
{role => log_body}
|
60
55
|
end.inject({}, &:update)
|
56
|
+
else
|
57
|
+
bodies = {}
|
61
58
|
end
|
62
59
|
|
63
60
|
data = {
|
@@ -66,24 +63,24 @@ module ApiHammer
|
|
66
63
|
'method' => request_env[:method],
|
67
64
|
'uri' => request_env[:url].normalize.to_s,
|
68
65
|
'headers' => request_env.request_headers,
|
69
|
-
'body' =>
|
66
|
+
'body' => bodies['request'],
|
70
67
|
}.reject{|k,v| v.nil? },
|
71
68
|
'response' => {
|
72
|
-
'status' =>
|
69
|
+
'status' => status.to_s,
|
73
70
|
'headers' => response_env.response_headers,
|
74
|
-
'body' =>
|
71
|
+
'body' => bodies['response'],
|
75
72
|
}.reject{|k,v| v.nil? },
|
76
73
|
'processing' => {
|
77
74
|
'began_at' => began_at.utc.to_f,
|
78
75
|
'duration' => now - began_at,
|
79
|
-
'activesupport_tagged_logging_tags' =>
|
76
|
+
'activesupport_tagged_logging_tags' => log_tags,
|
80
77
|
}.reject{|k,v| v.nil? },
|
81
78
|
}
|
82
79
|
|
83
80
|
json_data = JSON.generate(data)
|
84
81
|
dolog = proc do
|
85
82
|
now_s = now.strftime('%Y-%m-%d %H:%M:%S %Z')
|
86
|
-
@logger.info "#{bold(intense_magenta('>'))} #{status_s} : #{bold(intense_magenta(request_env[:method].to_s.upcase))} #{intense_magenta(request_env[:url].normalize.to_s)} @ #{intense_magenta(now_s)}"
|
83
|
+
@logger.info "#{bold(intense_magenta('>'))} #{status_s(status)} : #{bold(intense_magenta(request_env[:method].to_s.upcase))} #{intense_magenta(request_env[:url].normalize.to_s)} @ #{intense_magenta(now_s)}"
|
87
84
|
@logger.info json_data
|
88
85
|
end
|
89
86
|
|
@@ -17,7 +17,7 @@ module ApiHammer
|
|
17
17
|
end
|
18
18
|
if ss.scan(/[^&;]+/)
|
19
19
|
kv = ss[0]
|
20
|
-
key,
|
20
|
+
key, _ = kv.split('=', 2)
|
21
21
|
parsed_key = CGI::unescape(key)
|
22
22
|
if [*@options[:filter_keys]].any? { |fk| parsed_key =~ /(\A|[\[\]])#{Regexp.escape(fk)}(\z|[\[\]])/ }
|
23
23
|
filtered << [key, '[FILTERED]'].join('=')
|
@@ -35,9 +35,9 @@ module ApiHammer::Rails
|
|
35
35
|
when Array
|
36
36
|
check.each { |subcheck| check_required_params_helper(subcheck, subparams, errors, parents) }
|
37
37
|
when Hash
|
38
|
-
if subparams.is_a?(Hash)
|
39
|
-
check.each do |
|
40
|
-
check_required_params_helper(subcheck, subparams[
|
38
|
+
if subparams.is_a?(Hash) || (Object.const_defined?('ActionController') && subparams.is_a?(::ActionController::Parameters))
|
39
|
+
check.each do |key_, subcheck|
|
40
|
+
check_required_params_helper(subcheck, subparams[key_], errors, parents + [key_])
|
41
41
|
end
|
42
42
|
else
|
43
43
|
add_error.call(I18n.t(:"errors.required_params.must_be_hash", :default => "%{key} must be a Hash", :key => key))
|
@@ -1,15 +1,23 @@
|
|
1
1
|
require 'api_hammer'
|
2
2
|
require 'active_support/log_subscriber'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
4
|
+
begin
|
5
|
+
require 'rails/rack/log_tailer'
|
6
|
+
log_tailer_exists = true
|
7
|
+
rescue LoadError
|
8
|
+
log_tailer_exists = false
|
9
|
+
end
|
10
|
+
|
11
|
+
if log_tailer_exists
|
12
|
+
# fix up this class to tail the log when the body is closed rather than when its own #call is done.
|
13
|
+
module Rails
|
14
|
+
module Rack
|
15
|
+
class LogTailer
|
16
|
+
def call(env)
|
17
|
+
status, headers, body = @app.call(env)
|
18
|
+
body_proxy = ::Rack::BodyProxy.new(body) { tail! }
|
19
|
+
[status, headers, body_proxy]
|
20
|
+
end
|
13
21
|
end
|
14
22
|
end
|
15
23
|
end
|
@@ -4,6 +4,32 @@ require 'json'
|
|
4
4
|
require 'addressable/uri'
|
5
5
|
|
6
6
|
module ApiHammer
|
7
|
+
module RequestLoggerHelper
|
8
|
+
def log_bodies(status)
|
9
|
+
if @options[:log_bodies] == :on_error
|
10
|
+
(400..599).include?(status.to_i)
|
11
|
+
elsif @options.key?(:log_bodies)
|
12
|
+
@options[:log_bodies]
|
13
|
+
else
|
14
|
+
true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def status_s(status)
|
19
|
+
status_color = case status.to_i
|
20
|
+
when 200..299
|
21
|
+
:intense_green
|
22
|
+
when 400..499
|
23
|
+
:intense_yellow
|
24
|
+
when 500..599
|
25
|
+
:intense_red
|
26
|
+
else
|
27
|
+
:white
|
28
|
+
end
|
29
|
+
bold(send(status_color, status.to_s))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
7
33
|
# Rack middleware for logging. much like Rack::CommonLogger but with a log message that isn't an unreadable
|
8
34
|
# mess of dashes and unlabeled numbers.
|
9
35
|
#
|
@@ -14,12 +40,14 @@ module ApiHammer
|
|
14
40
|
# pretty, but informative.
|
15
41
|
class RequestLogger < Rack::CommonLogger
|
16
42
|
include Term::ANSIColor
|
43
|
+
include RequestLoggerHelper
|
17
44
|
|
18
45
|
LARGE_BODY_SIZE = 4096
|
19
46
|
|
20
47
|
# options
|
21
48
|
# - :logger
|
22
|
-
# - :filter_keys
|
49
|
+
# - :filter_keys - array of keys to filter from logged bodies
|
50
|
+
# - :log_bodies - true, false, :on_error
|
23
51
|
def initialize(app, logger, options={})
|
24
52
|
@options = options
|
25
53
|
super(app, logger)
|
@@ -59,25 +87,13 @@ module ApiHammer
|
|
59
87
|
request = Rack::Request.new(env)
|
60
88
|
response = Rack::Response.new('', status, response_headers)
|
61
89
|
|
62
|
-
status_color = case status.to_i
|
63
|
-
when 200..299
|
64
|
-
:intense_green
|
65
|
-
when 400..499
|
66
|
-
:intense_yellow
|
67
|
-
when 500..599
|
68
|
-
:intense_red
|
69
|
-
else
|
70
|
-
:white
|
71
|
-
end
|
72
|
-
status_s = bold(send(status_color, status.to_s))
|
73
|
-
|
74
90
|
request_headers = env.map do |(key, value)|
|
75
91
|
http_match = key.match(/\AHTTP_/)
|
76
92
|
if http_match
|
77
93
|
name = http_match.post_match.downcase
|
78
94
|
{name => value}
|
79
95
|
else
|
80
|
-
name = %w(content_type content_length).detect { |
|
96
|
+
name = %w(content_type content_length).detect { |sname| sname.downcase == key.downcase }
|
81
97
|
if name
|
82
98
|
{name => value}
|
83
99
|
end
|
@@ -101,7 +117,7 @@ module ApiHammer
|
|
101
117
|
'response' => {
|
102
118
|
'status' => status.to_s,
|
103
119
|
'headers' => response_headers,
|
104
|
-
'length' => response_headers['Content-Length'] || response_body.to_enum.map(
|
120
|
+
'length' => response_headers['Content-Length'] || response_body.to_enum.map(&:bytesize).inject(0, &:+),
|
105
121
|
}.reject { |k,v| v.nil? },
|
106
122
|
'processing' => {
|
107
123
|
'began_at' => began_at.utc.to_f,
|
@@ -111,27 +127,29 @@ module ApiHammer
|
|
111
127
|
}
|
112
128
|
response_body_string = response_body.to_enum.to_a.join('')
|
113
129
|
body_info = [['request', request_body, request.content_type], ['response', response_body_string, response.content_type]]
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
if
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
130
|
+
if log_bodies(status)
|
131
|
+
body_info.map do |(role, body, content_type)|
|
132
|
+
parsed_body = ApiHammer::Body.new(body, content_type)
|
133
|
+
content_type_attrs = ApiHammer::ContentTypeAttrs.new(content_type)
|
134
|
+
if content_type_attrs.text?
|
135
|
+
if (400..599).include?(status.to_i) || body.size < LARGE_BODY_SIZE
|
136
|
+
# log bodies if they are not large, or if there was an error (either client or server)
|
137
|
+
data[role]['body'] = parsed_body.filtered(@options.reject { |k,v| ![:filter_keys].include?(k) }).jsonifiable.body
|
138
|
+
else
|
139
|
+
# otherwise, log id and uuid fields
|
140
|
+
body_object = parsed_body.object
|
141
|
+
sep = /(?:\b|\W|_)/
|
142
|
+
hash_ids = proc do |hash|
|
143
|
+
hash.reject { |key, value| !(key =~ /#{sep}([ug]u)?id#{sep}/ && value.is_a?(String)) }
|
144
|
+
end
|
145
|
+
if body_object.is_a?(Hash)
|
146
|
+
body_ids = hash_ids.call(body_object)
|
147
|
+
elsif body_object.is_a?(Array) && body_object.all? { |e| e.is_a?(Hash) }
|
148
|
+
body_ids = body_object.map(&hash_ids)
|
149
|
+
end
|
133
150
|
|
134
|
-
|
151
|
+
data[role]['body_ids'] = body_ids if body_ids && body_ids.any?
|
152
|
+
end
|
135
153
|
end
|
136
154
|
end
|
137
155
|
end
|
@@ -139,7 +157,7 @@ module ApiHammer
|
|
139
157
|
json_data = JSON.dump(data)
|
140
158
|
dolog = proc do
|
141
159
|
now_s = now.strftime('%Y-%m-%d %H:%M:%S %Z')
|
142
|
-
@logger.info "#{bold(intense_cyan('<'))} #{status_s} : #{bold(intense_cyan(request.request_method))} #{intense_cyan(request_uri.normalize)} @ #{intense_cyan(now_s)}"
|
160
|
+
@logger.info "#{bold(intense_cyan('<'))} #{status_s(status)} : #{bold(intense_cyan(request.request_method))} #{intense_cyan(request_uri.normalize)} @ #{intense_cyan(now_s)}"
|
143
161
|
@logger.info json_data
|
144
162
|
end
|
145
163
|
# do the logging with tags that applied to the request if appropriate
|
@@ -30,7 +30,7 @@ module ApiHammer
|
|
30
30
|
if env['REQUEST_METHOD'].downcase != 'head' && ApiHammer::ContentTypeAttrs.new(content_type).text?
|
31
31
|
body = TNLBodyProxy.new(body){}
|
32
32
|
if headers["Content-Length"]
|
33
|
-
headers["Content-Length"] = body.map(
|
33
|
+
headers["Content-Length"] = body.map(&:bytesize).inject(0, &:+).to_s
|
34
34
|
end
|
35
35
|
end
|
36
36
|
[status, headers, body]
|
data/lib/api_hammer/version.rb
CHANGED
data/lib/api_hammer.rb
CHANGED
@@ -2,6 +2,12 @@ proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.cal
|
|
2
2
|
|
3
3
|
require 'api_hammer/version'
|
4
4
|
|
5
|
+
require 'i18n'
|
6
|
+
# this weirdness is because enforce_available_locales complains when you try to use the default locale
|
7
|
+
# with no translations stored. usually the application will have stored translations in the locale, but,
|
8
|
+
# not always, so we put a dummy key in.
|
9
|
+
I18n.backend.store_translations(I18n.locale, {__api_hammer__: ''})
|
10
|
+
|
5
11
|
module ApiHammer
|
6
12
|
autoload :Rails, 'api_hammer/rails'
|
7
13
|
autoload :Sinatra, 'api_hammer/sinatra'
|
@@ -40,6 +40,12 @@ describe 'ApiHammer::Rails#check_required_params' do
|
|
40
40
|
assert_equal({'error_message' => 'person must be a Hash', 'errors' => {'person' => ['person must be a Hash']}}, err.body)
|
41
41
|
end
|
42
42
|
|
43
|
+
it 'is has the wrong type for person with hash check' do
|
44
|
+
c = controller_with_params(:person => [])
|
45
|
+
err = assert_raises(ApiHammer::Rails::Halt) { c.check_required_params(:person => {:id => Fixnum}) }
|
46
|
+
assert_equal({'error_message' => 'person must be a Hash', 'errors' => {'person' => ['person must be a Hash']}}, err.body)
|
47
|
+
end
|
48
|
+
|
43
49
|
it 'is missing person#name' do
|
44
50
|
c = controller_with_params(:id => '99', :person => {:height => '3'}, :lucky_numbers => ['2'])
|
45
51
|
err = assert_raises(ApiHammer::Rails::Halt) { c.check_required_params(checks) }
|
@@ -126,6 +126,20 @@ describe ApiHammer::RequestLogger do
|
|
126
126
|
end
|
127
127
|
end
|
128
128
|
|
129
|
+
describe 'log_bodies' do
|
130
|
+
it 'does not log bodies when log_bodies is false' do
|
131
|
+
app = proc do |env|
|
132
|
+
[200, {'Content-Type' => 'text/plain'}, ['data go here']]
|
133
|
+
end
|
134
|
+
conn = Faraday.new do |f|
|
135
|
+
f.request :api_hammer_request_logger, logger, :log_bodies => false
|
136
|
+
f.use Faraday::Adapter::Rack, app
|
137
|
+
end
|
138
|
+
conn.get '/'
|
139
|
+
assert(!logio.string.include?('data go here'))
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
129
143
|
describe 'filtering' do
|
130
144
|
describe 'json response' do
|
131
145
|
it 'filters' do
|
data/test/halt_test.rb
CHANGED
@@ -42,7 +42,6 @@ describe 'ApiHammer::Rails#halt' do
|
|
42
42
|
assert_equal record, FakeController.new.find_or_halt(model, {:id => 'anid'})
|
43
43
|
end
|
44
44
|
it 'it halts with 404 if not' do
|
45
|
-
record = Object.new
|
46
45
|
model = Class.new do
|
47
46
|
(class << self; self; end).class_eval do
|
48
47
|
define_method(:where) { |attrs| [] }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api_hammer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.14.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ethan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-05-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -95,13 +95,13 @@ dependencies:
|
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: i18n
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - '>='
|
102
102
|
- !ruby/object:Gem::Version
|
103
103
|
version: '0'
|
104
|
-
type: :
|
104
|
+
type: :runtime
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
@@ -109,7 +109,7 @@ dependencies:
|
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
112
|
+
name: rake
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - '>='
|
@@ -123,7 +123,7 @@ dependencies:
|
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
126
|
+
name: rack-test
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
129
|
- - '>='
|
@@ -137,7 +137,7 @@ dependencies:
|
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
|
-
name: minitest
|
140
|
+
name: minitest
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
142
142
|
requirements:
|
143
143
|
- - '>='
|
@@ -151,7 +151,7 @@ dependencies:
|
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
|
-
name:
|
154
|
+
name: minitest-reporters
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
156
156
|
requirements:
|
157
157
|
- - '>='
|
@@ -165,7 +165,7 @@ dependencies:
|
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '0'
|
167
167
|
- !ruby/object:Gem::Dependency
|
168
|
-
name:
|
168
|
+
name: yard
|
169
169
|
requirement: !ruby/object:Gem::Requirement
|
170
170
|
requirements:
|
171
171
|
- - '>='
|
@@ -179,7 +179,7 @@ dependencies:
|
|
179
179
|
- !ruby/object:Gem::Version
|
180
180
|
version: '0'
|
181
181
|
- !ruby/object:Gem::Dependency
|
182
|
-
name:
|
182
|
+
name: simplecov
|
183
183
|
requirement: !ruby/object:Gem::Requirement
|
184
184
|
requirements:
|
185
185
|
- - '>='
|
@@ -193,7 +193,7 @@ dependencies:
|
|
193
193
|
- !ruby/object:Gem::Version
|
194
194
|
version: '0'
|
195
195
|
- !ruby/object:Gem::Dependency
|
196
|
-
name:
|
196
|
+
name: activesupport
|
197
197
|
requirement: !ruby/object:Gem::Requirement
|
198
198
|
requirements:
|
199
199
|
- - '>='
|
@@ -234,16 +234,12 @@ files:
|
|
234
234
|
- .yardopts
|
235
235
|
- CHANGELOG.md
|
236
236
|
- Gemfile
|
237
|
-
- Gemfile_ar_3
|
238
|
-
- Gemfile_ar_40
|
239
|
-
- Gemfile_ar_41
|
240
237
|
- LICENSE.txt
|
241
238
|
- README.md
|
242
239
|
- Rakefile.rb
|
243
240
|
- api_hammer.gemspec
|
244
241
|
- bin/hc
|
245
242
|
- lib/api_hammer.rb
|
246
|
-
- lib/api_hammer/active_record_cache_find_by.rb
|
247
243
|
- lib/api_hammer/body.rb
|
248
244
|
- lib/api_hammer/content_type_attrs.rb
|
249
245
|
- lib/api_hammer/faraday/outputter.rb
|
@@ -275,7 +271,6 @@ files:
|
|
275
271
|
- lib/logstash/filters/request_bodies_parsed.rb
|
276
272
|
- lib/logstash/filters/ruby_logger.rb
|
277
273
|
- lib/logstash/filters/sidekiq.rb
|
278
|
-
- test/active_record_cache_find_by_test.rb
|
279
274
|
- test/check_required_params_test.rb
|
280
275
|
- test/faraday_request_logger_test.rb
|
281
276
|
- test/halt_test.rb
|
@@ -310,7 +305,6 @@ signing_key:
|
|
310
305
|
specification_version: 4
|
311
306
|
summary: an API tool
|
312
307
|
test_files:
|
313
|
-
- test/active_record_cache_find_by_test.rb
|
314
308
|
- test/check_required_params_test.rb
|
315
309
|
- test/faraday_request_logger_test.rb
|
316
310
|
- test/halt_test.rb
|
data/Gemfile_ar_3
DELETED
data/Gemfile_ar_40
DELETED
data/Gemfile_ar_41
DELETED
@@ -1,134 +0,0 @@
|
|
1
|
-
require 'active_record'
|
2
|
-
|
3
|
-
module ActiveRecord
|
4
|
-
class Relation
|
5
|
-
if !method_defined?(:first_without_caching)
|
6
|
-
alias_method :first_without_caching, :first
|
7
|
-
def first(*args)
|
8
|
-
one_record_with_caching(args.empty?) { first_without_caching(*args) }
|
9
|
-
end
|
10
|
-
end
|
11
|
-
if !method_defined?(:take_without_caching) && method_defined?(:take)
|
12
|
-
alias_method :take_without_caching, :take
|
13
|
-
def take(*args)
|
14
|
-
one_record_with_caching(args.empty?) { take_without_caching(*args) }
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
# retrieves one record, hitting the cache if appropriate. the argument may bypass caching
|
19
|
-
# (the caller could elect to just not call this method if caching is to be avoided, but since this
|
20
|
-
# method already builds in opting whether or not to hit cache, the code is simpler just passing that in).
|
21
|
-
#
|
22
|
-
# requires a block which returns the record
|
23
|
-
def one_record_with_caching(can_cache = true)
|
24
|
-
actual_right = proc do |where_value|
|
25
|
-
if where_value.right.is_a?(Arel::Nodes::BindParam)
|
26
|
-
column, value = bind_values.detect { |(column, value)| column.name.to_s == where_value.left.name.to_s }
|
27
|
-
value
|
28
|
-
else
|
29
|
-
where_value.right
|
30
|
-
end
|
31
|
-
end
|
32
|
-
cache_find_bys = klass.send(:cache_find_bys)
|
33
|
-
can_cache &&= cache_find_bys &&
|
34
|
-
!loaded? && # if it's loaded no need to hit cache
|
35
|
-
where_values.all? { |wv| wv.is_a?(Arel::Nodes::Equality) } && # no inequality or that sort of thing
|
36
|
-
cache_find_bys.include?(where_values.map { |wv| wv.left.name.to_s }.sort) && # any of the set of where-values to cache match this relation
|
37
|
-
where_values.map(&actual_right).all? { |r| r.is_a?(String) || r.is_a?(Numeric) } && # check all right side values are simple types, number or string
|
38
|
-
offset_value.nil? &&
|
39
|
-
joins_values.blank? &&
|
40
|
-
order_values.blank? &&
|
41
|
-
!reverse_order_value &&
|
42
|
-
includes_values.blank? &&
|
43
|
-
preload_values.blank? &&
|
44
|
-
select_values.blank? &&
|
45
|
-
group_values.blank? &&
|
46
|
-
from_value.nil? &&
|
47
|
-
lock_value.nil?
|
48
|
-
|
49
|
-
if can_cache
|
50
|
-
cache_key = klass.send(:cache_key_for, where_values.map { |wv| [wv.left.name, actual_right.call(wv)] })
|
51
|
-
klass.finder_cache.fetch(cache_key) do
|
52
|
-
yield
|
53
|
-
end
|
54
|
-
else
|
55
|
-
yield
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
class Base
|
61
|
-
class << self
|
62
|
-
def finder_cache=(val)
|
63
|
-
define_singleton_method(:finder_cache) { val }
|
64
|
-
end
|
65
|
-
|
66
|
-
# the cache. should be an instance of some sort of ActiveSupport::Cache::Store.
|
67
|
-
# by default uses Rails.cache if that exists, or creates a ActiveSupport::Cache::MemoryStore to use.
|
68
|
-
# set this per-model or on ActiveRecord::Base, as needed; it is inherited.
|
69
|
-
def finder_cache
|
70
|
-
# if this looks weird, it kind of is. on the first invocation of #finder_cache, we call finder_cache=
|
71
|
-
# which overwrites the finder_cache method, and this then calls the newly defined method.
|
72
|
-
self.finder_cache = (Object.const_defined?(:Rails) && ::Rails.cache) || ::ActiveSupport::Cache::MemoryStore.new
|
73
|
-
self.finder_cache
|
74
|
-
end
|
75
|
-
|
76
|
-
# causes requests to retrieve a record by the given attributes (all of them) to be cached.
|
77
|
-
# this is for single records only. it is unsafe to use with a set of attributes whose values
|
78
|
-
# (in conjunction) may be associated with multiple records.
|
79
|
-
#
|
80
|
-
# see .finder_cache and .find_cache= for where it is cached.
|
81
|
-
#
|
82
|
-
# #flush_find_cache is defined on the instance. it is called on save to clear an updated record from
|
83
|
-
# the cache. it may also be called explicitly to clear a record from the cache.
|
84
|
-
#
|
85
|
-
# beware of multiple application servers with different caches - a record cached in multiple will not
|
86
|
-
# be invalidated in all when it is saved in one.
|
87
|
-
def cache_find_by(*attribute_names)
|
88
|
-
unless cache_find_bys
|
89
|
-
# initial setup
|
90
|
-
self.cache_find_bys = Set.new
|
91
|
-
after_update :flush_find_cache
|
92
|
-
before_destroy :flush_find_cache
|
93
|
-
end
|
94
|
-
|
95
|
-
find_by = attribute_names.map do |name|
|
96
|
-
raise(ArgumentError) unless name.is_a?(Symbol) || name.is_a?(String)
|
97
|
-
name.to_s.dup.freeze
|
98
|
-
end.sort.freeze
|
99
|
-
|
100
|
-
self.cache_find_bys = (cache_find_bys | [find_by]).freeze
|
101
|
-
end
|
102
|
-
|
103
|
-
private
|
104
|
-
def cache_find_bys=(val)
|
105
|
-
define_singleton_method(:cache_find_bys) { val }
|
106
|
-
singleton_class.send(:private, :cache_find_bys)
|
107
|
-
end
|
108
|
-
|
109
|
-
def cache_find_bys
|
110
|
-
nil
|
111
|
-
end
|
112
|
-
|
113
|
-
def cache_key_for(find_attributes)
|
114
|
-
attrs = find_attributes.map { |k,v| [k.to_s, v.to_s] }.sort_by(&:first).inject([], &:+)
|
115
|
-
cache_key_prefix = ['cache_find_by', table_name]
|
116
|
-
@parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
117
|
-
cache_key = (cache_key_prefix + attrs).map do |part|
|
118
|
-
@parser.escape(part, /[^a-z0-9\-\.\_\~]/i)
|
119
|
-
end.join('/')
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
# clears this record from the cache used by cache_find_by
|
124
|
-
def flush_find_cache
|
125
|
-
self.class.send(:cache_find_bys).each do |attribute_names|
|
126
|
-
find_attributes = attribute_names.map { |attr_name| [attr_name, attribute_was(attr_name)] }
|
127
|
-
self.class.instance_exec(find_attributes) do |find_attributes|
|
128
|
-
finder_cache.delete(cache_key_for(find_attributes))
|
129
|
-
end
|
130
|
-
end
|
131
|
-
nil
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
@@ -1,226 +0,0 @@
|
|
1
|
-
proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('.', File.dirname(__FILE__)))
|
2
|
-
require 'helper'
|
3
|
-
|
4
|
-
require 'active_support/cache'
|
5
|
-
require 'active_record'
|
6
|
-
|
7
|
-
ActiveRecord::Base.establish_connection(
|
8
|
-
:adapter => "sqlite3",
|
9
|
-
:database => ":memory:"
|
10
|
-
)
|
11
|
-
|
12
|
-
module Rails
|
13
|
-
class << self
|
14
|
-
def cache
|
15
|
-
@cache ||= ActiveSupport::Cache::MemoryStore.new
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
require 'api_hammer/active_record_cache_find_by'
|
21
|
-
|
22
|
-
ActiveRecord::Schema.define do
|
23
|
-
create_table :albums do |table|
|
24
|
-
table.column :title, :string
|
25
|
-
table.column :performer, :string
|
26
|
-
table.column :tracks, :integer
|
27
|
-
table.column :catalog_xid, :integer
|
28
|
-
end
|
29
|
-
create_table :catalogs do |table|
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
class Album < ActiveRecord::Base
|
34
|
-
belongs_to :catalog, :foreign_key => :catalog_xid
|
35
|
-
cache_find_by(:id)
|
36
|
-
cache_find_by(:performer)
|
37
|
-
cache_find_by(:title, :performer)
|
38
|
-
cache_find_by(:tracks)
|
39
|
-
cache_find_by(:catalog_xid, :title)
|
40
|
-
end
|
41
|
-
|
42
|
-
class Catalog < ActiveRecord::Base
|
43
|
-
has_many :albums, :foreign_key => :catalog_xid
|
44
|
-
end
|
45
|
-
|
46
|
-
class VinylAlbum < Album
|
47
|
-
self.finder_cache = ActiveSupport::Cache::MemoryStore.new
|
48
|
-
end
|
49
|
-
|
50
|
-
describe 'ActiveRecord::Base.cache_find_by' do
|
51
|
-
def assert_caches(key, cache = Rails.cache)
|
52
|
-
assert !cache.read(key), "cache already contains a key #{key}: #{cache.read(key)}"
|
53
|
-
yield
|
54
|
-
ensure
|
55
|
-
assert cache.read(key), "key #{key} was not cached"
|
56
|
-
end
|
57
|
-
|
58
|
-
def assert_not_caches(key, cache = Rails.cache)
|
59
|
-
assert !cache.read(key), "cache already contains a key #{key}: #{cache.read(key)}"
|
60
|
-
yield
|
61
|
-
ensure
|
62
|
-
assert !cache.read(key), "key was incorrectly cached - #{key}: #{cache.read(key)}"
|
63
|
-
end
|
64
|
-
|
65
|
-
after do
|
66
|
-
Album.all.each(&:destroy)
|
67
|
-
Catalog.all.each(&:destroy)
|
68
|
-
end
|
69
|
-
|
70
|
-
it('caches #find by primary key') do
|
71
|
-
id = Album.create!.id
|
72
|
-
assert_caches("cache_find_by/albums/id/#{id}") { assert Album.find(id) }
|
73
|
-
end
|
74
|
-
|
75
|
-
it('caches #find_by_id') do
|
76
|
-
id = Album.create!.id
|
77
|
-
assert_caches("cache_find_by/albums/id/#{id}") { assert Album.find_by_id(id) }
|
78
|
-
end
|
79
|
-
|
80
|
-
it('caches #where.first with primary key') do
|
81
|
-
id = Album.create!.id
|
82
|
-
assert_caches("cache_find_by/albums/id/#{id}") { assert Album.where(:id => id).first }
|
83
|
-
end
|
84
|
-
|
85
|
-
it('caches find_by_x with one attribute') do
|
86
|
-
Album.create!(:performer => 'x')
|
87
|
-
assert_caches("cache_find_by/albums/performer/x") { assert Album.find_by_performer('x') }
|
88
|
-
end
|
89
|
-
|
90
|
-
it('caches find_by_x! with one attribute') do
|
91
|
-
Album.create!(:performer => 'x')
|
92
|
-
assert_caches("cache_find_by/albums/performer/x") { assert Album.find_by_performer!('x') }
|
93
|
-
end
|
94
|
-
|
95
|
-
it('caches where.first with one attribute') do
|
96
|
-
Album.create!(:performer => 'x')
|
97
|
-
assert_caches("cache_find_by/albums/performer/x") { assert Album.where(:performer => 'x').first }
|
98
|
-
end
|
99
|
-
|
100
|
-
it('caches where.first! with one attribute') do
|
101
|
-
Album.create!(:performer => 'x')
|
102
|
-
assert_caches("cache_find_by/albums/performer/x") { assert Album.where(:performer => 'x').first! }
|
103
|
-
end
|
104
|
-
|
105
|
-
it('caches #where.first with integer attribute') do
|
106
|
-
id = Album.create!(:tracks => 3).id
|
107
|
-
assert_caches("cache_find_by/albums/tracks/3") { assert Album.where(:tracks => 3).first }
|
108
|
-
end
|
109
|
-
|
110
|
-
it('does not cache #where.first with inequality of integer attribute') do
|
111
|
-
id = Album.create!(:tracks => 3).id
|
112
|
-
assert_not_caches("cache_find_by/albums/tracks/3") { assert Album.where(Album.arel_table['tracks'].gteq(3)).first }
|
113
|
-
end
|
114
|
-
|
115
|
-
if ActiveRecord::Relation.method_defined?(:take)
|
116
|
-
it('caches where.take with one attribute') do
|
117
|
-
Album.create!(:performer => 'x')
|
118
|
-
assert_caches("cache_find_by/albums/performer/x") { assert Album.where(:performer => 'x').take }
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
it('does not cache where.last with one attribute') do
|
123
|
-
Album.create!(:performer => 'x')
|
124
|
-
assert_not_caches("cache_find_by/albums/performer/x") { assert Album.where(:performer => 'x').last }
|
125
|
-
end
|
126
|
-
|
127
|
-
it('does not cache find with array') do
|
128
|
-
ids = [Album.create!.id, Album.create!.id]
|
129
|
-
assert_not_caches("cache_find_by/albums/id/#{ids.first}") { assert Album.find(ids) }
|
130
|
-
end
|
131
|
-
|
132
|
-
it('does not cache find_by_x with array') do
|
133
|
-
ids = [Album.create!.id, Album.create!.id]
|
134
|
-
assert_not_caches("cache_find_by/albums/id/#{ids.first}") { assert Album.find_by_id(ids) }
|
135
|
-
end
|
136
|
-
|
137
|
-
it('does not cache where.first with array') do
|
138
|
-
ids = [Album.create!.id, Album.create!.id]
|
139
|
-
assert_not_caches("cache_find_by/albums/id/#{ids.first}") { assert Album.where(:id => ids).first }
|
140
|
-
end
|
141
|
-
|
142
|
-
it('does not cache find_by_x with one attribute') do
|
143
|
-
Album.create!(:title => 'x')
|
144
|
-
assert_not_caches("cache_find_by/albums/title/x") { assert Album.find_by_title('x') }
|
145
|
-
end
|
146
|
-
|
147
|
-
it('does not cache where.first with one attribute') do
|
148
|
-
Album.create!(:title => 'x')
|
149
|
-
assert_not_caches("cache_find_by/albums/title/x") { assert Album.where(:title => 'x').first }
|
150
|
-
end
|
151
|
-
|
152
|
-
it('caches find_by_x with two attributes') do
|
153
|
-
Album.create!(:title => 'x', :performer => 'y')
|
154
|
-
assert_caches("cache_find_by/albums/performer/y/title/x") { assert Album.find_by_title_and_performer('x', 'y') }
|
155
|
-
end
|
156
|
-
|
157
|
-
it('caches where.first with two attributes') do
|
158
|
-
Album.create!(:title => 'x', :performer => 'y')
|
159
|
-
assert_caches("cache_find_by/albums/performer/y/title/x") { assert Album.where(:title => 'x', :performer => 'y').first }
|
160
|
-
end
|
161
|
-
|
162
|
-
it('caches with two attributes on an association with a where') do
|
163
|
-
c = Catalog.create!
|
164
|
-
Album.create!(:title => 'x', :performer => 'y', :catalog_xid => c.id)
|
165
|
-
c = Catalog.first
|
166
|
-
assert_caches("cache_find_by/albums/catalog_xid/#{c.id}/title/x") { assert c.albums.where(:title => 'x').first }
|
167
|
-
end
|
168
|
-
|
169
|
-
it('flushes cache on save') do
|
170
|
-
album = Album.create!(:title => 'x', :performer => 'y')
|
171
|
-
assert_caches(key1 = "cache_find_by/albums/performer/y/title/x") { assert Album.find_by_title_and_performer('x', 'y') }
|
172
|
-
assert_caches(key2 = "cache_find_by/albums/performer/y") { assert Album.find_by_performer('y') }
|
173
|
-
album.update_attributes!(:performer => 'z')
|
174
|
-
assert !Rails.cache.read(key1), Rails.cache.instance_eval { @data }.inspect
|
175
|
-
assert !Rails.cache.read(key2), Rails.cache.instance_eval { @data }.inspect
|
176
|
-
end
|
177
|
-
|
178
|
-
it('flushes cache on destroy') do
|
179
|
-
album = Album.create!(:title => 'x', :performer => 'y')
|
180
|
-
assert_caches(key1 = "cache_find_by/albums/performer/y/title/x") { assert Album.find_by_title_and_performer('x', 'y') }
|
181
|
-
assert_caches(key2 = "cache_find_by/albums/performer/y") { assert Album.find_by_performer('y') }
|
182
|
-
album.destroy
|
183
|
-
assert !Rails.cache.read(key1), Rails.cache.instance_eval { @data }.inspect
|
184
|
-
assert !Rails.cache.read(key2), Rails.cache.instance_eval { @data }.inspect
|
185
|
-
end
|
186
|
-
|
187
|
-
it 'inherits cache_find_bys' do
|
188
|
-
assert VinylAlbum.send(:cache_find_bys).any? { |f| f == ['id'] }
|
189
|
-
end
|
190
|
-
|
191
|
-
it 'uses a different cache when specified' do
|
192
|
-
assert Album.finder_cache != VinylAlbum.finder_cache
|
193
|
-
|
194
|
-
id = Album.create!.id
|
195
|
-
key = "cache_find_by/albums/id/#{id}"
|
196
|
-
assert_caches(key) do
|
197
|
-
assert_not_caches(key, VinylAlbum.finder_cache) do
|
198
|
-
assert Album.find(id)
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
id = VinylAlbum.create!.id
|
203
|
-
key = "cache_find_by/albums/id/#{id}"
|
204
|
-
assert_caches(key, VinylAlbum.finder_cache) do
|
205
|
-
assert_not_caches(key) do
|
206
|
-
assert VinylAlbum.find(id)
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
it 'does not get confused by values with slashes' do
|
212
|
-
Album.create!(:title => 'z', :performer => 'y/title/x')
|
213
|
-
Album.create!(:title => 'x', :performer => 'y')
|
214
|
-
|
215
|
-
Album.where(:performer => 'y', :title => 'x').first
|
216
|
-
assert_equal 'z', Album.where(:performer => 'y/title/x').first.title
|
217
|
-
end
|
218
|
-
|
219
|
-
it 'works with a symbol on the left' do
|
220
|
-
# this makes an association with :catalog_xid as the left side of a where_value. these are usually
|
221
|
-
# strings. this just makes sure it doesn't error out.
|
222
|
-
c = Catalog.create!
|
223
|
-
c = Catalog.first
|
224
|
-
c.albums.where(:title => 'y').first
|
225
|
-
end
|
226
|
-
end
|