api_hammer 0.10.2 → 0.11.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: e22063457c4fd23dcfacf1dd263bb86bed720da4
4
- data.tar.gz: 59d655dfeb7c3521bda18fa73cc58da1a0cee266
3
+ metadata.gz: 6d475db83c102fc0746ea2da97c992a9ab1e6a1f
4
+ data.tar.gz: f82cbddaf738a334f7779bed1ce64005c6e46f93
5
5
  SHA512:
6
- metadata.gz: e26ef7aec22dc8114044bb52947d3d1a49e00395ae45c80feb6903487385f3db6bf4f0871c32558e943cc7a77d85358d9d705695c9ae2a5acec9d25ad41eb76e
7
- data.tar.gz: 7b0d75819bfd4c637d24a0ee74cea82e589b3ac3d5f767129a7ac43b4370cb44fb0d4df01d5398f7324b5f4a1805a4cbc6727a19456f1c5cc2bb34373b1bec09
6
+ metadata.gz: 42161c34bc9260717cba35c2ce308636490e332722e43d1435fa2300b404f97156701d9bed860a727ed4dbf2fcab010968f6cb360d33dd10b63dfaf4f73b7d75
7
+ data.tar.gz: 59f6ccc971e7d4c925a7141c7fa91670bf36e83972598c0583015c0df5c48759195bf34fb6499887b3703fce63f9610a700f9d91b334720e9627bf93792ac0cf
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ # v0.11.0
2
+ - improved handling of text and binary bodies in logging middleware and hc
3
+
1
4
  # v0.10.2
2
5
  - rails request logging logs exception backtrace
3
6
 
data/bin/hc CHANGED
@@ -44,6 +44,9 @@ opt_parser = OptionParser.new do |opts|
44
44
  opts.on("-t", "--content-type CONTENT-TYPE", "Sets the Content-Type header of the request. This defaults to application/json if a body is included.") do |v|
45
45
  $options[:headers]['Content-Type'.downcase] = v
46
46
  end
47
+ opts.on("-o", "--output OUTPUT", "write response to file") do |v|
48
+ $options[:output] = v
49
+ end
47
50
  opts.on("--oauth-token TOKEN", "OAuth 1.0 token") do |token|
48
51
  $oauth[:token] = token
49
52
  end
@@ -116,3 +119,9 @@ end
116
119
  # OH LOOK IT'S FINALLY ACTUALLY CONNECTING TO SOMETHING
117
120
 
118
121
  response = connection.run_request(httpmethod.downcase.to_sym, url, body, headers)
122
+
123
+ if $options[:output]
124
+ File.open($options[:output], 'wb') do |f|
125
+ f.write(response.body)
126
+ end
127
+ end
@@ -0,0 +1,103 @@
1
+ require 'rack'
2
+
3
+ module ApiHammer
4
+ class Body
5
+ attr_reader :body, :content_type
6
+
7
+ def initialize(body, content_type)
8
+ @body = body
9
+ @content_type = content_type
10
+ end
11
+
12
+ # parses the body to an object
13
+ def object
14
+ instance_variable_defined?(:@object) ? @object : @object = begin
15
+ if media_type == 'application/json'
16
+ JSON.parse(body) rescue nil
17
+ elsif media_type == 'application/x-www-form-urlencoded'
18
+ CGI.parse(body).map { |k, vs| {k => vs.last} }.inject({}, &:update)
19
+ end
20
+ end
21
+ end
22
+
23
+ def filtered(options)
24
+ @filtered ||= Body.new(begin
25
+ if media_type == 'application/json'
26
+ begin
27
+ ApiHammer::Filtration::Json.new(body, options).filter
28
+ rescue JSON::ParserError
29
+ body
30
+ end
31
+ elsif media_type == 'application/x-www-form-urlencoded'
32
+ ApiHammer::Filtration::FormEncoded.new(body, options).filter
33
+ else
34
+ body
35
+ end
36
+ end, content_type)
37
+ end
38
+
39
+ def content_type_attrs
40
+ @content_type_attrs ||= ContentTypeAttrs.new(content_type)
41
+ end
42
+
43
+ def media_type
44
+ content_type_attrs.media_type
45
+ end
46
+
47
+ # deal with the vagaries of getting the response body in a form which JSON
48
+ # gem will not cry about generating
49
+ def jsonifiable
50
+ @jsonifiable ||= Body.new(catch(:jsonifiable) do
51
+ original_body = self.body
52
+ unless original_body.is_a?(String)
53
+ begin
54
+ # if the response body is not a string, but JSON doesn't complain
55
+ # about dumping whatever it is, go ahead and use it
56
+ JSON.generate([original_body])
57
+ throw :jsonifiable, original_body
58
+ rescue
59
+ # otherwise return nil - don't know what to do with whatever this object is
60
+ throw :jsonifiable, nil
61
+ end
62
+ end
63
+
64
+ # first try to change the string's encoding per the Content-Type header
65
+ body = original_body.dup
66
+ unless body.valid_encoding?
67
+ # I think this always comes in as ASCII-8BIT anyway so may never get here. hopefully.
68
+ body.force_encoding('ASCII-8BIT')
69
+ end
70
+
71
+ content_type_attrs = ContentTypeAttrs.new(content_type)
72
+ if content_type_attrs.parsed?
73
+ charset = content_type_attrs['charset'].first
74
+ if charset && Encoding.list.any? { |enc| enc.to_s.downcase == charset.downcase }
75
+ if body.dup.force_encoding(charset).valid_encoding?
76
+ body.force_encoding(charset)
77
+ else
78
+ # I guess just ignore the specified encoding if the result is not valid. fall back to
79
+ # something else below.
80
+ end
81
+ end
82
+ end
83
+ begin
84
+ JSON.generate([body])
85
+ rescue Encoding::UndefinedConversionError
86
+ # if updating by content-type didn't do it, try UTF8 since JSON wants that - but only
87
+ # if it seems to be valid utf8.
88
+ # don't try utf8 if the response content-type indicated something else.
89
+ try_utf8 = !(content_type_attrs && content_type_attrs.parsed? && content_type_attrs['charset'].any?)
90
+ if try_utf8 && body.dup.force_encoding('UTF-8').valid_encoding?
91
+ body.force_encoding('UTF-8')
92
+ else
93
+ # I'm not sure if there is a way in this situation to get JSON gem to generate the
94
+ # string correctly. fall back to an array of codepoints I guess? this is a weird
95
+ # solution but the best I've got for now.
96
+ body = body.codepoints.to_a
97
+ end
98
+ end
99
+ body
100
+ end, content_type)
101
+ end
102
+ end
103
+ end
@@ -27,7 +27,7 @@ module ApiHammer
27
27
  end
28
28
  end
29
29
 
30
- attr_reader :media_type
30
+ attr_reader :content_type, :media_type
31
31
 
32
32
  def parsed?
33
33
  @parsed
@@ -47,12 +47,19 @@ module ApiHammer
47
47
  %r(model/.*) => :binary,
48
48
  %r(text/.*) => :text,
49
49
  %r(message/.*) => :text,
50
+ %r(application/(.+\+)?json) => :text,
51
+ %r(application/(.+\+)?xml) => :text,
52
+ %r(model/(.+\+)?xml) => :text,
53
+ 'application/x-www-form-urlencoded' => :text,
54
+ 'application/javascript' => :text,
55
+ 'application/ecmascript' => :text,
50
56
  'application/octet-stream' => :binary,
51
57
  'application/ogg' => :binary,
52
58
  'application/pdf' => :binary,
53
59
  'application/postscript' => :binary,
54
60
  'application/zip' => :binary,
55
61
  'application/gzip' => :binary,
62
+ 'application/vnd.apple.pkpass' => :binary,
56
63
  }
57
64
  types.each do |match, type|
58
65
  matched = match.is_a?(Regexp) ? media_type =~ %r(\A#{match.source}\z) : media_type == match
@@ -60,8 +67,13 @@ module ApiHammer
60
67
  return type == :text
61
68
  end
62
69
  end
63
- # fallback (unknown or not given) assume text
64
- return true
70
+ # fallback (unknown or not given) assume that unknown content types are binary but omitted
71
+ # content-type means text
72
+ if content_type && content_type =~ /\S/
73
+ return false
74
+ else
75
+ return true
76
+ end
65
77
  end
66
78
  end
67
79
  end
@@ -1,5 +1,6 @@
1
1
  require 'faraday'
2
2
  require 'rack'
3
+ require 'api_hammer'
3
4
 
4
5
  module ApiHammer
5
6
  # outputs the response body to the given logger or output device (defaulting to STDOUT)
@@ -55,6 +56,8 @@ module ApiHammer
55
56
  color :response_header
56
57
  color :response_blankline, :intense_green, :bold
57
58
 
59
+ color :omitted_body, :intense_yellow
60
+
58
61
  def call(request_env)
59
62
  puts "#{info('*')} #{info_body("connect to #{request_env[:url].host} on port #{request_env[:url].port}")}"
60
63
  puts "#{info('*')} #{info_body("getting our SSL on")}" if request_env[:url].scheme=='https'
@@ -125,23 +128,27 @@ module ApiHammer
125
128
  # - formatted prettily if #pretty? is true
126
129
  def alter_body_by_content_type(body, content_type)
127
130
  return body unless body.is_a?(String)
128
- media_type = ::Rack::Request.new({'CONTENT_TYPE' => content_type}).media_type
129
- if pretty?
130
- case media_type
131
- when 'application/json'
132
- require 'json'
133
- begin
134
- body = JSON.pretty_generate(JSON.parse(body))
135
- rescue JSON::ParserError
131
+ content_type_attrs = ApiHammer::ContentTypeAttrs.new(content_type)
132
+ if content_type_attrs.text?
133
+ if pretty?
134
+ case content_type_attrs.media_type
135
+ when 'application/json'
136
+ require 'json'
137
+ begin
138
+ body = JSON.pretty_generate(JSON.parse(body))
139
+ rescue JSON::ParserError
140
+ end
136
141
  end
137
142
  end
138
- end
139
- if color?
140
- coderay_scanner = CodeRayForMediaTypes.reject{|k,v| !v.any?{|type| type === media_type} }.keys.first
141
- if coderay_scanner
142
- require 'coderay'
143
- body = CodeRay.scan(body, coderay_scanner).encode(:terminal)
143
+ if color?
144
+ coderay_scanner = CodeRayForMediaTypes.reject{|k,v| !v.any?{|type| type === content_type_attrs.media_type} }.keys.first
145
+ if coderay_scanner
146
+ require 'coderay'
147
+ body = CodeRay.scan(body, coderay_scanner).encode(:terminal)
148
+ end
144
149
  end
150
+ else
151
+ body = omitted_body("[[omitted binary body (size = #{body.size})]]")
145
152
  end
146
153
  body
147
154
  end
@@ -5,72 +5,6 @@ require 'strscan'
5
5
 
6
6
  module ApiHammer
7
7
  module Faraday
8
- class Request
9
- def initialize(request_env, response_env)
10
- @request_env = request_env
11
- @response_env = response_env
12
- end
13
-
14
- attr_reader :request_env
15
- attr_reader :response_env
16
-
17
- # deal with the vagaries of getting the response body in a form which JSON
18
- # gem will not cry about generating
19
- def response_body
20
- instance_variable_defined?(:@response_body) ? @response_body : @response_body = catch(:response_body) do
21
- unless response_env.body.is_a?(String)
22
- begin
23
- # if the response body is not a string, but JSON doesn't complain
24
- # about dumping whatever it is, go ahead and use it
25
- JSON.generate([response_env.body])
26
- throw :response_body, response_env.body
27
- rescue
28
- # otherwise return nil - don't know what to do with whatever this object is
29
- throw :response_body, nil
30
- end
31
- end
32
-
33
- # first try to change the string's encoding per the Content-Type header
34
- content_type = response_env.response_headers['Content-Type']
35
- response_body = response_env.body.dup
36
- unless response_body.valid_encoding?
37
- # I think this always comes in as ASCII-8BIT anyway so may never get here. hopefully.
38
- response_body.force_encoding('ASCII-8BIT')
39
- end
40
-
41
- content_type_attrs = ContentTypeAttrs.new(content_type)
42
- if content_type_attrs.parsed?
43
- charset = content_type_attrs['charset'].first
44
- if charset && Encoding.list.any? { |enc| enc.to_s.downcase == charset.downcase }
45
- if response_body.dup.force_encoding(charset).valid_encoding?
46
- response_body.force_encoding(charset)
47
- else
48
- # I guess just ignore the specified encoding if the result is not valid. fall back to
49
- # something else below.
50
- end
51
- end
52
- end
53
- begin
54
- JSON.generate([response_body])
55
- rescue Encoding::UndefinedConversionError
56
- # if updating by content-type didn't do it, try UTF8 since JSON wants that - but only
57
- # if it seems to be valid utf8.
58
- # don't try utf8 if the response content-type indicated something else.
59
- try_utf8 = !(content_type_attrs && content_type_attrs.parsed? && content_type_attrs['charset'].any?)
60
- if try_utf8 && response_body.dup.force_encoding('UTF-8').valid_encoding?
61
- response_body.force_encoding('UTF-8')
62
- else
63
- # I'm not sure if there is a way in this situation to get JSON gem to generate the
64
- # string correctly. fall back to an array of codepoints I guess? this is a weird
65
- # solution but the best I've got for now.
66
- response_body = response_body.codepoints.to_a
67
- end
68
- end
69
- response_body
70
- end
71
- end
72
- end
73
-
74
8
  # Faraday middleware for logging.
75
9
  #
76
10
  # logs two lines:
@@ -101,8 +35,6 @@ module ApiHammer
101
35
  @app.call(request_env).on_complete do |response_env|
102
36
  now = Time.now
103
37
 
104
- request = ApiHammer::Faraday::Request.new(request_env, response_env)
105
-
106
38
  status_color = case response_env.status.to_i
107
39
  when 200..299
108
40
  :intense_green
@@ -115,17 +47,17 @@ module ApiHammer
115
47
  end
116
48
  status_s = bold(send(status_color, response_env.status.to_s))
117
49
 
118
- filtered_request_body = request_body.dup if request_body
119
- filtered_response_body = request.response_body.dup if request.response_body
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)
120
56
 
121
57
  if @options[:filter_keys]
122
- body_info = [['request', filtered_request_body, request_env.request_headers], ['response', filtered_response_body, response_env.response_headers]]
123
- body_info.map do |(role, body, headers)|
124
- if body
125
- parsed_body = ApiHammer::ParsedBody.new(body, headers['Content-Type'])
126
- body.replace(parsed_body.filtered_body(@options.slice(:filter_keys)))
127
- end
128
- end
58
+ bodies = bodies.map do |(role, body)|
59
+ {role => body.filtered(@options.slice(:filter_keys))}
60
+ end.inject({}, &:update)
129
61
  end
130
62
 
131
63
  data = {
@@ -134,12 +66,12 @@ module ApiHammer
134
66
  'method' => request_env[:method],
135
67
  'uri' => request_env[:url].normalize.to_s,
136
68
  'headers' => request_env.request_headers,
137
- 'body' => (filtered_request_body if ContentTypeAttrs.new(request_env.request_headers['Content-Type']).text?),
69
+ 'body' => (bodies['request'].body if bodies['request'].content_type_attrs.text?),
138
70
  }.reject{|k,v| v.nil? },
139
71
  'response' => {
140
72
  'status' => response_env.status.to_s,
141
73
  'headers' => response_env.response_headers,
142
- 'body' => (filtered_response_body if ContentTypeAttrs.new(response_env.response_headers['Content-Type']).text?),
74
+ 'body' => (bodies['response'].body if bodies['response'].content_type_attrs.text?),
143
75
  }.reject{|k,v| v.nil? },
144
76
  'processing' => {
145
77
  'began_at' => began_at.utc.to_f,
@@ -112,24 +112,27 @@ module ApiHammer
112
112
  response_body_string = response_body.to_enum.to_a.join('')
113
113
  body_info = [['request', request_body, request.content_type], ['response', response_body_string, response.content_type]]
114
114
  body_info.map do |(role, body, content_type)|
115
- parsed_body = ApiHammer::ParsedBody.new(body, content_type)
116
- if (400..599).include?(status.to_i) || body.size < LARGE_BODY_SIZE
117
- # log bodies if they are not large, or if there was an error (either client or server)
118
- data[role]['body'] = parsed_body.filtered_body(@options.reject { |k,v| ![:filter_keys].include?(k) })
119
- else
120
- # otherwise, log id and uuid fields
121
- body_object = parsed_body.object
122
- sep = /(?:\b|\W|_)/
123
- hash_ids = proc do |hash|
124
- hash.reject { |key, value| !(key =~ /#{sep}([ug]u)?id#{sep}/ && value.is_a?(String)) }
125
- end
126
- if body_object.is_a?(Hash)
127
- body_ids = hash_ids.call(body_object)
128
- elsif body_object.is_a?(Array) && body_object.all? { |e| e.is_a?(Hash) }
129
- body_ids = body_object.map(&hash_ids)
130
- end
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.jsonifiable.filtered(@options.reject { |k,v| ![:filter_keys].include?(k) }).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
131
133
 
132
- data[role]['body_ids'] = body_ids if body_ids && body_ids.any?
134
+ data[role]['body_ids'] = body_ids if body_ids && body_ids.any?
135
+ end
133
136
  end
134
137
  end
135
138
  Thread.current['request_logger.info'] = nil
@@ -26,7 +26,7 @@ module ApiHammer
26
26
  end
27
27
  def call(env)
28
28
  status, headers, body = *@app.call(env)
29
- if env['REQUEST_METHOD'].downcase != 'head'
29
+ if env['REQUEST_METHOD'].downcase != 'head' && ApiHammer::ContentTypeAttrs.new(env['CONTENT_TYPE']).text?
30
30
  body = TNLBodyProxy.new(body){}
31
31
  if headers["Content-Length"]
32
32
  headers["Content-Length"] = body.map(&Rack::Utils.method(:bytesize)).inject(0, &:+).to_s
@@ -1,3 +1,3 @@
1
1
  module ApiHammer
2
- VERSION = "0.10.2"
2
+ VERSION = "0.11.0"
3
3
  end
data/lib/api_hammer.rb CHANGED
@@ -12,7 +12,7 @@ module ApiHammer
12
12
  autoload :RailsOrSidekiqLogger, 'api_hammer/rails_or_sidekiq_logger'
13
13
  autoload :FaradayOutputter, 'api_hammer/faraday/outputter'
14
14
  autoload :FaradayCurlVOutputter, 'api_hammer/faraday/outputter'
15
- autoload :ParsedBody, 'api_hammer/parsed_body'
15
+ autoload :Body, 'api_hammer/body'
16
16
  autoload :ContentTypeAttrs, 'api_hammer/content_type_attrs'
17
17
  autoload :JsonScriptEscapeHelper, 'api_hammer/json_script_escape_helper'
18
18
  module Faraday
@@ -21,7 +21,7 @@ class LogStash::Filters::RequestBodiesParsed < LogStash::Filters::Base
21
21
  if event[re]['headers'].is_a?(Hash) && !content_type
22
22
  _, content_type = event[re]['headers'].detect { |(k,_)| k =~ /\Acontent.type\z/i }
23
23
  end
24
- parsed_body = ApiHammer::ParsedBody.new(event[re]['body'], content_type)
24
+ parsed_body = ApiHammer::Body.new(event[re]['body'], content_type)
25
25
  event[re]['body_parsed'] = parsed_body.object if parsed_body.object
26
26
  end
27
27
  end
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.10.2
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ethan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-19 00:00:00.000000000 Z
11
+ date: 2015-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -244,6 +244,7 @@ files:
244
244
  - bin/hc
245
245
  - lib/api_hammer.rb
246
246
  - lib/api_hammer/active_record_cache_find_by.rb
247
+ - lib/api_hammer/body.rb
247
248
  - lib/api_hammer/check_required_params.rb
248
249
  - lib/api_hammer/content_type_attrs.rb
249
250
  - lib/api_hammer/faraday/outputter.rb
@@ -252,7 +253,6 @@ files:
252
253
  - lib/api_hammer/filtration/json.rb
253
254
  - lib/api_hammer/halt.rb
254
255
  - lib/api_hammer/json_script_escape_helper.rb
255
- - lib/api_hammer/parsed_body.rb
256
256
  - lib/api_hammer/public_instance_exec.rb
257
257
  - lib/api_hammer/rails.rb
258
258
  - lib/api_hammer/rails_or_sidekiq_logger.rb
@@ -1,39 +0,0 @@
1
- require 'rack'
2
-
3
- module ApiHammer
4
- class ParsedBody
5
- attr_reader :body, :content_type, :media_type
6
-
7
- def initialize(body, content_type)
8
- @body = body
9
- @content_type = content_type
10
- @media_type = ::Rack::Request.new({'CONTENT_TYPE' => content_type}).media_type
11
- end
12
-
13
- def object
14
- instance_variable_defined?(:@object) ? @object : @object = begin
15
- if media_type == 'application/json'
16
- JSON.parse(body) rescue nil
17
- elsif media_type == 'application/x-www-form-urlencoded'
18
- CGI.parse(body).map { |k, vs| {k => vs.last} }.inject({}, &:update)
19
- end
20
- end
21
- end
22
-
23
- def filtered_body(options)
24
- @filtered_body ||= begin
25
- if media_type == 'application/json'
26
- begin
27
- ApiHammer::Filtration::Json.new(body, options).filter
28
- rescue JSON::ParserError
29
- body
30
- end
31
- elsif media_type == 'application/x-www-form-urlencoded'
32
- ApiHammer::Filtration::FormEncoded.new(body, options).filter
33
- else
34
- body
35
- end
36
- end
37
- end
38
- end
39
- end