motion/http.rb in bubble-wrap-1.0.0 vs motion/http.rb in bubble-wrap-1.1.0

- old
+ new

@@ -1,9 +1,7 @@ module BubbleWrap - SETTINGS = {} - # The HTTP module provides a simple interface to make HTTP requests. # # TODO: preflight support, easier/better cookie support, better error handling module HTTP @@ -19,44 +17,44 @@ # BubbleWrap::HTTP.get("https://api.github.com/users/mattetti", {credentials: {username: 'matt', password: 'aimonetti'}}) do |response| # p response.body.to_str # prints the response's body # end # def self.get(url, options={}, &block) - create_query url, :get, options, block + options[:action] = block if block_given? + HTTP::Query.new(url, :get, options) end # Make a POST request def self.post(url, options={}, &block) - create_query url, :post, options, block + options[:action] = block if block_given? + HTTP::Query.new(url, :post, options) end # Make a PUT request def self.put(url, options={}, &block) - create_query url, :put, options, block + options[:action] = block if block_given? + HTTP::Query.new(url, :put, options) end # Make a DELETE request def self.delete(url, options={}, &block) - create_query url, :delete, options, block + options[:action] = block if block_given? + HTTP::Query.new(url, :delete, options) end # Make a HEAD request def self.head(url, options={}, &block) - create_query url, :head, options, block + options[:action] = block if block_given? + HTTP::Query.new(url, :head, options) end # Make a PATCH request def self.patch(url, options={}, &block) - create_query url, :patch, options, block + options[:action] = block if block_given? + HTTP::Query.new(url, :patch, options) end - private - def self.create_query(url, method, options, passed_block) - options[:action] = passed_block if passed_block - HTTP::Query.new( url, method, options ) - end - # Response class wrapping the results of a Query's response class Response attr_reader :body attr_reader :headers attr_accessor :status_code, :error_message @@ -74,10 +72,15 @@ def ok? status_code.to_s =~ /20\d/ ? true : false end + def to_s + "#<#{self.class}:#{self.object_id} - url: #{self.url}, body: #{self.body}, headers: #{self.headers}, status code: #{self.status_code}, error message: #{self.error_message} >" + end + alias description to_s + end # Class wrapping NSConnection and often used indirectly by the BubbleWrap::HTTP module methods. class Query attr_accessor :request @@ -104,157 +107,212 @@ # a proc will receive a Response object while the passed object # will receive the handle_query_response method # :headers<Hash> - headers send with the request # Anything else will be available via the options attribute reader. # - def initialize(url, http_method = :get, options={}) + def initialize(url_string, http_method = :get, options={}) @method = http_method.upcase.to_s @delegator = options.delete(:action) || self @payload = options.delete(:payload) + @files = options.delete(:files) + @boundary = options.delete(:boundary) || BW.create_uuid @credentials = options.delete(:credentials) || {} @credentials = {:username => '', :password => ''}.merge(@credentials) @timeout = options.delete(:timeout) || 30.0 @headers = escape_line_feeds(options.delete :headers) @cache_policy = options.delete(:cache_policy) || NSURLRequestUseProtocolCachePolicy @options = options @response = HTTP::Response.new - initiate_request(url) - connection.start + + @url = create_url(url_string) + @body = create_request_body + @request = create_request + @connection = create_connection(request, self) + @connection.start + UIApplication.sharedApplication.networkActivityIndicatorVisible = true - connection end - def generate_params(payload, prefix=nil) - list = [] - payload.each do |k,v| - if v.is_a?(Hash) - new_prefix = prefix ? "#{prefix}[#{k.to_s}]" : k.to_s - param = generate_params(v, new_prefix) - list << param - elsif v.is_a?(Array) - v.each do |val| - param = prefix ? "#{prefix}[#{k}][]=#{val}" : "#{k}[]=#{val}" - list << param - end - else - param = prefix ? "#{prefix}[#{k}]=#{v}" : "#{k}=#{v}" - list << param - end - end - return list.flatten + def to_s + "#<#{self.class}:#{self.object_id} - Method: #{@method}, url: #{@url.description}, body: #{@body.description}, Payload: #{@payload}, Headers: #{@headers} Credentials: #{@credentials}, Timeout: #{@timeout}, \ +Cache policy: #{@cache_policy}, response: #{@response.inspect} >" end + alias description to_s - def initiate_request(url_string) - # http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/nsrunloop_Class/Reference/Reference.html#//apple_ref/doc/constant_group/Run_Loop_Modes - # NSConnectionReplyMode - - unless @payload.nil? - if @payload.is_a?(Hash) - params = generate_params(@payload) - @payload = params.join("&") - end - url_string = "#{url_string}?#{@payload}" if @method == "GET" - end - #this method needs a refactor when the specs are done. (especially this utf8 escaping part) - log "BubbleWrap::HTTP building a NSRequest for #{url_string}" - @url = NSURL.URLWithString(url_string.stringByAddingPercentEscapesUsingEncoding NSUTF8StringEncoding) - @request = NSMutableURLRequest.requestWithURL(@url, - cachePolicy:@cache_policy, - timeoutInterval:@timeout) - @request.setHTTPMethod @method - @request.setAllHTTPHeaderFields(@headers) if @headers - - # @payload needs to be converted to data - unless @method == "GET" || @payload.nil? - @payload = @payload.to_s.dataUsingEncoding(NSUTF8StringEncoding) - @request.setHTTPBody @payload - end - - # NSHTTPCookieStorage.sharedHTTPCookieStorage - @connection = create_connection(request, self) - patch_nsurl_request - end - def connection(connection, didReceiveResponse:response) @status_code = response.statusCode @response_headers = response.allHeaderFields @response_size = response.expectedContentLength.to_f end # This delegate method get called every time a chunk of data is being received def connection(connection, didReceiveData:received_data) @received_data ||= NSMutableData.new @received_data.appendData(received_data) + + if download_progress = options[:download_progress] + download_progress.call(@received_data.length.to_f, response_size) + end end def connection(connection, willSendRequest:request, redirectResponse:redirect_response) - log "HTTP redirected #{request.description}" + log "HTTP redirected info: #{request} - #{self.description}" new_request = request.mutableCopy # new_request.setValue(@credentials.inspect, forHTTPHeaderField:'Authorization') # disabled while we figure this one out new_request.setAllHTTPHeaderFields(@headers) if @headers @connection.cancel @connection = create_connection(new_request, self) new_request end def connection(connection, didFailWithError: error) + log "HTTP Connection failed #{error.localizedDescription}" UIApplication.sharedApplication.networkActivityIndicatorVisible = false @request.done_loading! - log "HTTP Connection failed #{error.localizedDescription}" @response.error_message = error.localizedDescription call_delegator_with_response end + def connection(connection, didSendBodyData:sending, totalBytesWritten:written, totalBytesExpectedToWrite:expected) + if upload_progress = options[:upload_progress] + upload_progress.call(sending, written, expected) + end + end - # The transfer is done and everything went well def connectionDidFinishLoading(connection) UIApplication.sharedApplication.networkActivityIndicatorVisible = false @request.done_loading! - # copy the data in a local var that we will attach to the response object response_body = NSData.dataWithData(@received_data) if @received_data @response.update(status_code: status_code, body: response_body, headers: response_headers, url: @url) call_delegator_with_response end def connection(connection, didReceiveAuthenticationChallenge:challenge) - if (challenge.previousFailureCount == 0) - # by default we are keeping the credential for the entire session - # Eventually, it would be good to let the user pick one of the 3 possible credential persistence options: - # NSURLCredentialPersistenceNone, - # NSURLCredentialPersistenceForSession, - # NSURLCredentialPersistencePermanent - log "auth challenged, answered with credentials: #{credentials.inspect}" new_credential = NSURLCredential.credentialWithUser(credentials[:username], password:credentials[:password], persistence:NSURLCredentialPersistenceForSession) challenge.sender.useCredential(new_credential, forAuthenticationChallenge:challenge) + log "auth challenged, answered with credentials: #{credentials.inspect}" else challenge.sender.cancelAuthenticationChallenge(challenge) log 'Auth Failed :(' end end private + def create_request + log "BubbleWrap::HTTP building a NSRequest for #{@url.description}" + + request = NSMutableURLRequest.requestWithURL(@url, + cachePolicy:@cache_policy, + timeoutInterval:@timeout) + request.setHTTPMethod(@method) + request.setAllHTTPHeaderFields(@headers) + request.setHTTPBody(@body) + patch_nsurl_request(request) + + request + end + + def create_request_body + return nil if (@method == "GET" || @method == "HEAD") + return nil unless (@payload || @files) + @headers = {"Content-Type" => "multipart/form-data; boundary=#{@boundary}"} if @headers.nil? + + body = NSMutableData.data + + append_payload(body) if @payload + append_files(body) if @files + body.appendData("\r\n--#{@boundary}--\r\n".dataUsingEncoding NSUTF8StringEncoding) + + log "Built HTTP body: \n #{body.to_str}" + body + end + + def append_payload(body) + if @payload.is_a?(NSData) + body.appendData(@payload) + else + append_form_params(body) + end + end + + def append_form_params(body) + @payload.each do |key, value| + form_data = NSMutableData.new + s = "\r\n--#{@boundary}\r\n" + s += "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n" + s += value.to_s + form_data.appendData(s.dataUsingEncoding NSUTF8StringEncoding) + body.appendData(form_data) + end + end + + def append_files(body) + @files.each do |key, value| + file_data = NSMutableData.new + s = "\r\n--#{@boundary}\r\n" + s += "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{key}\"\r\n" + s += "Content-Type: application/octet-stream\r\n\r\n" + file_data.appendData(s.dataUsingEncoding NSUTF8StringEncoding) + file_data.appendData(value) + body.appendData(file_data) + end + end + + def create_url(url_string) + if (@method == "GET" || @method == "HEAD") && @payload + convert_payload_to_url if @payload.is_a?(Hash) + url_string += "?#{@payload}" + end + NSURL.URLWithString(url_string.stringByAddingPercentEscapesUsingEncoding NSUTF8StringEncoding) + end + + def convert_payload_to_url + params_array = generate_get_params(@payload) + @payload = params_array.join("&") + end + + def generate_get_params(payload, prefix=nil) + list = [] + payload.each do |k,v| + if v.is_a?(Hash) + new_prefix = prefix ? "#{prefix}[#{k.to_s}]" : k.to_s + param = generate_get_params(v, new_prefix) + list << param + elsif v.is_a?(Array) + v.each do |val| + param = prefix ? "#{prefix}[#{k}][]=#{val}" : "#{k}[]=#{val}" + list << param + end + else + param = prefix ? "#{prefix}[#{k}]=#{v}" : "#{k}=#{v}" + list << param + end + end + return list.flatten + end + def log(message) - NSLog message if SETTINGS[:debug] + NSLog message if BubbleWrap.debug? end def escape_line_feeds(hash) return nil if hash.nil? escaped_hash = {} hash.each{|k,v| escaped_hash[k] = v.gsub("\n", '\\n') } escaped_hash end - def patch_nsurl_request - @request.instance_variable_set("@done_loading", false) + def patch_nsurl_request(request) + request.instance_variable_set("@done_loading", false) - def @request.done_loading; @done_loading; end - def @request.done_loading!; @done_loading = true; end + def request.done_loading; @done_loading; end + def request.done_loading!; @done_loading = true; end end def call_delegator_with_response if @delegator.respond_to?(:call) @delegator.call( @response, self ) @@ -263,8 +321,9 @@ # This is a temporary method used for mocking. def create_connection(request, delegate) NSURLConnection.connectionWithRequest(request, delegate:delegate) end + end end end