require 'logger' require 'rest-client' require 'json' class Mudbug def self.version vpath = File.join(File.dirname(__FILE__), '..', 'VERSION') File.read(vpath).chomp end class StatusCodeError < 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 }, }, } def self.log_to dest=nil case dest when nil, 'stderr', 'STDERR' dest = $stderr when 'stdout', 'STDOUT' dest = $stdout when IO # do nothing when String # assume file path, do nothing else raise "unable to log_to #{dest} (#{dest.class})" end if defined?(@@log) l = Logger.new dest l.formatter = @@log.formatter l.level = @@log.level @@log = l else @@log = Logger.new dest @@log.formatter = proc { |sev, time, progname, msg| line = "[#{time.strftime('%Y-%m-%d %H:%M:%S')}] #{sev.to_s.upcase}: " line << "(#{progname}) " if progname line << msg << "\n" } @@log.level = Logger::WARN end @@log end def self.log_level=(sym) log_to unless defined?(@@log) level = Logger.const_get(sym.to_s.upcase) raise "unknown log level #{sym}" unless level @@log.level = level end # 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 # def self.process(resp, accept = nil) log_to unless defined?(@@log) @@log.debug { "response code: #{resp.code}" } @@log.debug { "response headers:\n" << resp.raw_headers.inspect } unless (200..299).include?(resp.code) @@log.warn { "processing with HTTP Status Code #{resp.code}" } end # do you even Content-type, bro? ct = resp.headers[:content_type] unless ct @@log.warn { "abort processing -- no response Content-type" } return resp.body end # warn if we got Content-type we didn't ask for ct, charset = ct.split(';').map { |s| s.strip } if accept and !accept.include?(ct) @@log.warn { "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] } @@log.warn { "abort processing -- unrecognized Content-type: #{ct}" } return resp.body end attr_reader :options attr_accessor :host def initialize(host, options = {}) self.class.log_to options.delete :log_to @host = host @options = options accept :json, :html, :text 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 # def resource(path, options = {}) path = "/#{path}" unless path[0,1] == '/' RestClient::Resource.new "http://#{@host}#{path}", @options.merge(options) end # no payload # [:get, :delete].each { |meth| define_method(meth) { |path, options = {}| res = resource(path, options) self.class.process(res.send(meth), 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, options={}| payload = payload.to_json unless payload.is_a?(String) res = resource(path, options) self.class.process(res.send(meth, payload), res.heads[:accept]) } } end