/
mudbug.rb
152 lines (134 loc) · 4.31 KB
/
mudbug.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
require 'rest-client'
require 'json'
require 'lager'
class Mudbug
def self.version
file = File.expand_path('../../VERSION', __FILE__)
File.read(file).chomp
end
extend Lager
log_to $stderr, :warn
class StatusCodeError < RuntimeError; end
class ContentTypeError < RuntimeError; end
# this structure declares what we support in the request Accept: header
# and defines automatic processing of the response based on the
# response Content-type: header
#
CONTENT = {
json: {
type: 'application/json',
proc: proc { |text| JSON.parse(text, symobolize_names: true) },
},
html: {
type: 'text/html',
proc: proc { |text| text },
},
text: {
type: 'text/plain',
proc: proc { |text| text },
},
}
# map our internal symbols to HTTP content types
# assign q scores based on the parameter order
# construct the right side of the Accept: header
#
def self.accept_header(*types)
types.map.with_index { |t, i|
type = CONTENT[t] ? CONTENT[t][:type] : "application/#{t.to_s.downcase}"
quality = "q=" << sprintf("%0.1f", 1.0 - i*0.1)
i == 0 ? type : [type, quality].join(';')
}.join(', ')
end
# do stuff based on response's Content-type
# accept is e.g. [:json, :html]
#
def self.process(resp, accept = nil)
@lager.debug { "accept: #{accept}" }
@lager.debug { "response code: #{resp.code}" }
@lager.debug { "response headers:\n" << resp.raw_headers.inspect }
unless (200..299).include?(resp.code)
@lager.warn { "processing with HTTP Status Code #{resp.code}" }
end
# do you even Content-type, bro?
ct = resp.headers[:content_type]
unless ct
@lager.warn { "abandon processing -- no response Content-type" }
return resp.body
end
# get the content-type
ct, charset = ct.split(';').map { |s| s.strip }
@lager.info { "got charset: #{charset}; ignoring" } if charset
# raise if we got Content-type we didn't ask for
if accept and !accept.include?(ct)
raise ContentTypeError, "Asked for #{accept} but got #{ct}"
end
# process the response for known content types
CONTENT.each { |sym, hsh|
return hsh[:proc].call(resp.body) if ct == hsh[:type]
}
@lager.warn { "abandon processing -- unrecognized Content-type: #{ct}" }
return resp.body
end
attr_reader :options
attr_accessor :host, :protocol
def initialize(host = 'localhost', options = {})
@host = host
https = options.delete(:https)
@protocol = https ? 'https' : 'http'
@options = options
accept :json, :html, :text
yield self if block_given?
end
# Writes the Accept: header for you
# e.g.
# accept :json, :html # Accept: application/json, text/html
# accept nil # remove Accept: header
# Now adds q-scores automatically based on order
# Note: the hard work is done by the class method
#
def accept(*types)
types = types.first if types.first.is_a?(Array)
@options[:headers] ||= {}
return @options[:headers].delete(:accept) if types.first.nil?
@options[:headers][:accept] = self.class.accept_header(*types)
end
# use this method directly if you want finer-grained request and response
# handling
# supports /path/to/res, path/to/res, http://host.com/path/to/res
#
def resource(path)
uri = URI.parse(path)
if uri.host # a full URL was passed in
@host = uri.host
@protocol = uri.scheme
url = uri.to_s
else
path = "/#{path}" unless path[0,1] == '/'
url = "#{@protocol}://#{@host}#{path}"
end
RestClient::Resource.new(url, @options)
end
# no payload
#
[:get, :delete].each { |meth|
define_method(meth) { |path, params = {}|
res = resource(path)
resp = res.send(meth, params: params)
self.class.process(resp, res.headers[:accept])
}
}
# (JSON) payload required
# if payload is a String, then assume it's already JSON
# otherwise apply #to_json to payload automatically. Quack.
#
[:post, :put].each { |meth|
define_method(meth) { |path, payload, params = {}|
payload = payload.to_json unless payload.is_a?(String)
res = resource(path)
resp = res.send(meth, payload,
content_type: CONTENT[:json][:type],
params: params)
self.class.process(resp, res.headers[:accept])
}
}
end