api_hammer 0.13.3 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|