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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0ec8f15a064560aa1f4f50123bbcf2a334130488
4
- data.tar.gz: c29ed9ef11362ef5a872d617d656b3a56c879198
3
+ metadata.gz: 909efcbfb3f192ecb429eaf23a7dce4a1d07dd68
4
+ data.tar.gz: 9526647e571cdce3a3f39911dbaa116897d8277b
5
5
  SHA512:
6
- metadata.gz: c681350085a2cbcfbfc208f988ce91700e98b5350a1bd13d26ca06008a256e5a159c40b88b2f236944097549c334908b05af1ae30ce907d4421187f76d84bf2e
7
- data.tar.gz: 92ae18c0f12d10abb11829e129fcd648142910b0ccbcbcaeeaf06ec072785c4bcfea46841002e3bef16149859652a32b40273d7d52ef87b14f3fa1391abce6da
6
+ metadata.gz: d965bcb0458ef5c542e1ab0ebc2b0cfc4955ca98d1231def62582cc458aad1450bc897e31d1bd1803696f438bd5b2e81ac0769cfa10b18d998c6150912132be8
7
+ data.tar.gz: 95ba356edbba139809480c576e632e0cf464dbc9b854d11fcd37dce1b82375d338020a320bf801c0f31fdb2149cce6ed42e900b04c1dc4c8a74b781543e60b61
data/.travis.yml CHANGED
@@ -1,7 +1,4 @@
1
- # we don't actually use travis, just wwtd
2
-
3
- gemfile:
4
- - Gemfile_ar_3
5
- - Gemfile_ar_40
6
- - Gemfile_ar_41
1
+ rvm:
2
+ - 2.0.0-p353
3
+ - 2.2.4
7
4
  script: rake test
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 match = scanner.scan(/(\w+)=("?)([^"]*)("?)\s*(,?)\s*/)
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
- status_color = case response_env.status.to_i
39
- when 200..299
40
- :intense_green
41
- when 400..499
42
- :intense_yellow
43
- when 500..599
44
- :intense_red
45
- else
46
- :white
47
- end
48
- status_s = bold(send(status_color, response_env.status.to_s))
49
-
50
- bodies = [
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' => (bodies['request'].body if bodies['request'].content_type_attrs.text?),
66
+ 'body' => bodies['request'],
70
67
  }.reject{|k,v| v.nil? },
71
68
  'response' => {
72
- 'status' => response_env.status.to_s,
69
+ 'status' => status.to_s,
73
70
  'headers' => response_env.response_headers,
74
- 'body' => (bodies['response'].body if bodies['response'].content_type_attrs.text?),
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' => @log_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, value = kv.split('=', 2)
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 |key, subcheck|
40
- check_required_params_helper(subcheck, subparams[key], errors, parents + [key])
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
- require 'rails/rack/log_tailer'
5
- # fix up this class to tail the log when the body is closed rather than when its own #call is done.
6
- module Rails
7
- module Rack
8
- class LogTailer
9
- def call(env)
10
- status, headers, body = @app.call(env)
11
- body_proxy = ::Rack::BodyProxy.new(body) { tail! }
12
- [status, headers, body_proxy]
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 { |name| name.downcase == key.downcase }
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(&::Rack::Utils.method(:bytesize)).inject(0, &:+),
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
- body_info.map do |(role, body, content_type)|
115
- parsed_body = ApiHammer::Body.new(body, content_type)
116
- content_type_attrs = ApiHammer::ContentTypeAttrs.new(content_type)
117
- if content_type_attrs.text?
118
- if (400..599).include?(status.to_i) || body.size < LARGE_BODY_SIZE
119
- # log bodies if they are not large, or if there was an error (either client or server)
120
- data[role]['body'] = parsed_body.filtered(@options.reject { |k,v| ![:filter_keys].include?(k) }).jsonifiable.body
121
- else
122
- # otherwise, log id and uuid fields
123
- body_object = parsed_body.object
124
- sep = /(?:\b|\W|_)/
125
- hash_ids = proc do |hash|
126
- hash.reject { |key, value| !(key =~ /#{sep}([ug]u)?id#{sep}/ && value.is_a?(String)) }
127
- end
128
- if body_object.is_a?(Hash)
129
- body_ids = hash_ids.call(body_object)
130
- elsif body_object.is_a?(Array) && body_object.all? { |e| e.is_a?(Hash) }
131
- body_ids = body_object.map(&hash_ids)
132
- end
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
- data[role]['body_ids'] = body_ids if body_ids && body_ids.any?
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(&Rack::Utils.method(:bytesize)).inject(0, &:+).to_s
33
+ headers["Content-Length"] = body.map(&:bytesize).inject(0, &:+).to_s
34
34
  end
35
35
  end
36
36
  [status, headers, body]
@@ -1,3 +1,3 @@
1
1
  module ApiHammer
2
- VERSION = "0.13.3"
2
+ VERSION = "0.14.0"
3
3
  end
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.13.3
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-04-05 00:00:00.000000000 Z
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: rake
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: :development
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: rack-test
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: minitest
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-reporters
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: yard
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: simplecov
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: activesupport
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: activerecord
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
@@ -1,8 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gemspec
4
-
5
- gem 'byebug'
6
- gem 'wwtd'
7
-
8
- gem 'activerecord', '~> 3.0'
data/Gemfile_ar_40 DELETED
@@ -1,8 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gemspec
4
-
5
- gem 'byebug'
6
- gem 'wwtd'
7
-
8
- gem 'activerecord', '~> 4.0.0'
data/Gemfile_ar_41 DELETED
@@ -1,8 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gemspec
4
-
5
- gem 'byebug'
6
- gem 'wwtd'
7
-
8
- gem 'activerecord', '~> 4.1.0'
@@ -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