lib/tent-client/cycle_http.rb in tent-client-0.0.1 vs lib/tent-client/cycle_http.rb in tent-client-0.2.1

- old
+ new

@@ -1,49 +1,159 @@ +require 'yajl' + class TentClient # Proxies to Faraday and cycles through server urls # until either non left or response status in the 200s or 400s class CycleHTTP - attr_reader :client, :server_urls + attr_reader :client, :servers def initialize(client, &faraday_block) @faraday_block = faraday_block @client = client - @server_urls = client.server_urls.dup + + if client.entity_uri + unless (Hash === client.server_meta) && (Array === client.server_meta['servers']) + raise MalformedServerMeta.new("Server meta post for Entity(#{client.entity_uri.inspect}) is malformed: #{client.server_meta.inspect}") + end + + @servers = client.server_meta['servers'].sort_by { |s| s['preference'] } + else + @servers = [] + end end + def current_server + @current_server || servers.first + end + def new_http - @http = Faraday.new(:url => server_urls.shift) do |f| + @current_server = servers.shift + @http = Faraday.new do |f| @faraday_block.call(f) end end def http - @http || new_http + @http ||= new_http end - %w{ head get put post patch delete options }.each do |verb| - define_method verb do |*args, &block| - res = http.send(verb, *args, &block) - return res unless server_urls.any? - case res.status - when 200...300, 400...500 - res - else - new_http - send(verb, *args, &block) + def named_url(name, params = {}) + unless (Hash === current_server) && (Hash === current_server['urls']) && (template = current_server['urls'][name.to_s]) + raise ServerNotFound.new("Failed to match #{name.to_s.inspect} to a url for server: #{Yajl::Encoder.encode(current_server)}") + end + + template.to_s.gsub(/\{([^\}]+)\}/) { + param = (params.delete($1) || params.delete($1.to_sym)).to_s + URI.encode_www_form_component(param) + } + end + + %w( options get head delete ).map(&:to_sym).each do |verb| + class_eval(<<-RUBY + def #{verb}(url, params = {}, headers = {}, &block) + run_request(#{verb.inspect}, url, params, nil, headers, &block) end +RUBY + ) + end + + %w( post put patch ).map(&:to_sym).each do |verb| + class_eval(<<-RUBY + def #{verb}(url, params = {}, body = nil, headers = {}, &block) + run_request(#{verb.inspect}, url, params, body, headers, &block) + end +RUBY + ) + end + + def multipart_request(verb, url, params, parts, headers = {}, &block) + body = multipart_body(parts) + run_request(verb.to_sym, url, params, body, headers) do |request| + request.headers['Content-Type'] = "#{MULTIPART_CONTENT_TYPE}; boundary=#{MULTIPART_BOUNDARY}" + request.headers['Content-Length'] = body.length.to_s + yield(request) if block_given? end end + def run_request(verb, url, params, body, headers, &block) + args = [verb, url, params, body, headers] + if Symbol === url + name = url + url = named_url(url, params || {}) + else + name = nil + end + + res = http.run_request(verb, url, body, headers) do |request| + request.params.update(params) if params + yield request if block_given? + end + + if name + res.env[:tent_server] = current_server + end + + return res if servers.empty? || !name + + case res.status + when 200...300, 400...500 + res + else + new_http + run_request(*args, &block) + end + rescue Faraday::Error::TimeoutError, Faraday::Error::ConnectionFailed + raise if servers.empty? + new_http + run_request(*args, &block) + end + def respond_to_missing?(method_name, include_private = false) http.respond_to?(method_name, include_private) end def method_missing(method_name, *args, &block) if http.respond_to?(method_name) http.send(method_name, *args, &block) else super end + end + + private + + def multipart_body(parts) + # group by category + parts = parts.inject(Hash.new) do |memo, part| + category = part[:category] || part['category'] + memo[category] ||= [] + memo[category] << part + memo + end + + # expend into request parts + parts = parts.inject(Array.new) do |memo, (category, category_parts)| + if category_parts.size > 1 + memo.concat category_parts.each_with_index.map { |part, index| + headers = part[:headers] || part['headers'] + Faraday::Parts::FilePart.new(MULTIPART_BOUNDARY, "#{category}[#{index}]", upload_io(part), headers) + } + else + part = category_parts.first + headers = part[:headers] || part['headers'] + memo << Faraday::Parts::FilePart.new(MULTIPART_BOUNDARY, category, upload_io(part), :headers => headers) + end + end + + parts << Faraday::Parts::EpiloguePart.new(MULTIPART_BOUNDARY) + Faraday::CompositeReadIO.new(parts) + end + + def upload_io(part) + Faraday::UploadIO.new( + (part[:file] || part['file']) || StringIO.new(part[:data] || part['data']), + part[:content_type] || part['content-type'], + part[:filename] || part['filename'] + ) end end end