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 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