./lib/animoto/client.rb in animoto-0.0.0.alpha3 vs ./lib/animoto/client.rb in animoto-0.0.0.alpha4

- old
+ new

@@ -1,10 +1,7 @@ -require 'uri' -require 'net/http' -require 'net/https' -require 'json' require 'yaml' +require 'uri' require 'animoto/errors' require 'animoto/content_type' require 'animoto/standard_envelope' require 'animoto/resource' @@ -23,23 +20,22 @@ require 'animoto/video' require 'animoto/job' require 'animoto/directing_and_rendering_job' require 'animoto/directing_job' require 'animoto/rendering_job' +require 'animoto/dynamic_class_loader' +require 'animoto/http_engine' +require 'animoto/response_parser' module Animoto class Client API_ENDPOINT = "https://api2-staging.animoto.com/" API_VERSION = 1 BASE_CONTENT_TYPE = "application/vnd.animoto" - HTTP_METHOD_MAP = { - :get => Net::HTTP::Get, - :post => Net::HTTP::Post - } attr_accessor :key, :secret, :endpoint - attr_reader :format + attr_reader :http_engine, :response_parser # Creates a new Client object which handles credentials, versioning, making requests, and # parsing responses. # # If you have your key and secret in ~/.animotorc or /etc/.animotorc, those credentials will @@ -50,31 +46,65 @@ # @param [String] key the API key for your account # @param [String] secret the secret key for your account # @return [Client] # @raise [ArgumentError] if no credentials are supplied def initialize *args - @debug = ENV['DEBUG'] options = args.last.is_a?(Hash) ? args.pop : {} @key = args[0] @secret = args[1] @endpoint = options[:endpoint] - - home_path = File.expand_path '~/.animotorc' - config = if File.exist?(home_path) - YAML.load(File.read(home_path)) - elsif File.exist?('/etc/.animotorc') - YAML.load(File.read('/etc/.animotorc')) + configure_from_rc_file + @endpoint ||= API_ENDPOINT + __send__ :http_engine=, options[:http_engine] || :net_http + __send__ :response_parser=, options[:response_parser] || :json + end + + # Set the HTTP engine this client will use. + # + # @param [HTTPEngine, Symbol, Class] engine you may pass a + # HTTPEngine instance to use, or the symbolic name of a adapter to use, + # or a Class whose instances respond to #request and return a String of + # the response body + # @see Animoto::HTTPEngine + # @return [HTTPEngine] the engine instance + # @raise [ArgumentError] if given a class without the correct interface + def http_engine= engine + @http_engine = case engine + when Animoto::HTTPEngine + engine + when Class + if engine.instance_methods.include?('request') + engine.new + else + raise ArgumentError + end + else + Animoto::HTTPEngine[engine].new end - @key ||= config['key'] - @secret ||= config['secret'] - @endpoint ||= config['endpoint'] - unless @key && @secret - raise ArgumentError, "You must supply your key and secret" + end + + # Set the response parser this client will use. + # + # @param [ResponseParser, Symbol, Class] parser you may pass a + # ResponseParser instance to use, or the symbolic name of a adapter to use, + # or a Class whose instances respond to #parse, #unparse, and #format. + # @see Animoto::ResponseParser + # @return [ResponseParser] the parser instance + # @raise [ArgumentError] if given a class without the correct interface + def response_parser= parser + @response_parser = case parser + when Animoto::ResponseParser + parser + when Class + if %{format parse unparse}.all? { |m| parser.instance_methods.include? m } + parser.new + else + raise ArgumentError + end + else + Animoto::ResponseParser[parser].new end - - @endpoint ||= API_ENDPOINT - @format = 'json' end # Finds a resource by its URL. # # @param [Class] klass the Resource class you're finding @@ -122,133 +152,97 @@ resource.load(find_request(resource.class, resource.url, options)) end private + # Sets the API credentials from an .animotorc file. First looks for one in the current + # directory, then checks ~/.animotorc, then finally /etc/.animotorc. + # + # @raise [ArgumentError] if none of the files are found + def configure_from_rc_file + catch(:done) do + current_path = Dir.pwd + '/.animotorc' + home_path = File.expand_path('~/.animotorc') + config = if File.exist?(current_path) + YAML.load(File.read(current_path)) + elsif File.exist?(home_path) + home_path = File.expand_path '~/.animotorc' + YAML.load(File.read(home_path)) + elsif File.exist?('/etc/.animotorc') + YAML.load(File.read('/etc/.animotorc')) + end + if config + @key ||= config['key'] + @secret ||= config['secret'] + @endpoint ||= config['endpoint'] + throw :done if @key && @secret + end + raise ArgumentError, "You must supply your key and secret" + end + end + # Builds a request to find a resource. # # @param [Class] klass the Resource class you're looking for # @param [String] url the URL of the resource # @param [Hash] options - # @return [Hash] deserialized JSON response body + # @return [Hash] deserialized response body def find_request klass, url, options = {} - # request(:get, URI.parse(url).path, nil, { "Accept" => content_type_of(klass) }, options) - request(:get, URI.parse(url), nil, { "Accept" => content_type_of(klass) }, options) + request(:get, url, nil, { "Accept" => content_type_of(klass) }, options) end # Builds a request requiring a manifest. # # @param [Manifest] manifest the manifest being acted on # @param [String] endpoint the endpoint to send the request to # @param [Hash] options - # @return [Hash] deserialized JSON response body + # @return [Hash] deserialized response body def send_manifest manifest, endpoint, options = {} - # request(:post, endpoint, manifest.to_json, { "Accept" => "application/#{format}", "Content-Type" => content_type_of(manifest) }, options) u = URI.parse(endpoint) u.path = endpoint - request(:post, u, manifest.to_json, { "Accept" => "application/#{format}", "Content-Type" => content_type_of(manifest) }, options) + request( + :post, + u.to_s, + response_parser.unparse(manifest.to_hash), + { "Accept" => "application/#{response_parser.format}", "Content-Type" => content_type_of(manifest) }, + options + ) end # Makes a request and parses the response. # # @param [Symbol] method which HTTP method to use (should be lowercase, i.e. :get instead of :GET) - # @param [URI] uri a URI object of the request URI + # @param [String] url the URL of the request # @param [String, nil] body the request body # @param [Hash<String,String>] headers the request headers (will be sent as-is, which means you should # specify "Content-Type" => "..." instead of, say, :content_type => "...") # @param [Hash] options - # @return [Hash] deserialized JSON response body - def request method, uri, body, headers = {}, options = {} - http = Net::HTTP.new uri.host, uri.port - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - req = build_request method, uri, body, headers, options - if @debug - puts "********************* REQUEST *******************" - puts "#{req.method} #{uri.to_s} HTTP/#{http.instance_variable_get(:@curr_http_version)}\r\n" - req.each_capitalized { |header, value| puts "#{header}: #{value}\r\n" } - puts "\r\n" - puts req.body unless req.method == 'GET' + # @return [Hash] deserialized response body + # @raise [Error] + def request method, url, body, headers = {}, options = {} + error = catch(:fail) do + options = { :username => @key, :password => @secret }.merge(options) + response = http_engine.request(method, url, body, headers, options) + return response_parser.parse(response) end - response = http.request(req) - if @debug - puts "********************* RESPONSE *******************" - puts "#{response.code} #{response.message}\r\n" - response.each_capitalized { |header, value| puts "#{header}: #{value}\r\n" } - puts "\r\n" - body = response.body - if body.nil? || body.empty? - puts "(No content)" - else - puts body - end + if error + errors = response_parser.parse(error)['response']['status']['errors'] + raise Animoto::Error.new(errors.collect { |e| e['message'] }.join(', ')) + else + raise Animoto::Error end - read_response response + rescue NoMethodError => e + raise Animoto::Error.new("Invalid response (#{error.inspect})") end - # Builds the request object. - # - # @param [Symbol] method which HTTP method to use (should be lowercase, i.e. :get instead of :GET) - # @param [String] uri the request path - # @param [String, nil] body the request body - # @param [Hash<String,String>] headers the request headers (will be sent as-is, which means you should - # specify "Content-Type" => "..." instead of, say, :content_type => "...") - # @param [Hash] options - # @return [Net::HTTPRequest] the request object - def build_request method, uri, body, headers, options - req = HTTP_METHOD_MAP[method].new uri.path - req.body = body - req.initialize_http_header headers - req.basic_auth key, secret - req - end - - # Verifies and parses the response. - # - # @param [Net::HTTPResponse] response the response object - # @return [Hash] deserialized JSON response body - def read_response response - check_status response - parse_response response - end - - # Checks the status of the response to make sure it's successful. - # - # @param [Net::HTTPResponse] response the response object - # @return [nil] - # @raise [Error,RuntimeError] if the response code isn't in the 200 range - def check_status response - unless (200..299).include?(response.code.to_i) - if response.body - begin - json = JSON.parse(response.body) - errors = json['response']['status']['errors'] - rescue => e - raise response.message - else - raise Animoto::Error.new(errors.collect { |e| e['message'] }.join(', ')) - end - else - raise response.message - end - end - end - - # Parses a JSON response body into a Hash. - # @param [Net::HTTPResponse] response the response object - # @return [Hash] deserialized JSON response body - def parse_response response - JSON.parse(response.body) - end - # Creates the full content type string given a Resource class or instance # @param [Class,ContentType] klass_or_instance the class or instance to build the # content type for # @return [String] the full content type with the version and format included (i.e. # "application/vnd.animoto.storyboard-v1+json") def content_type_of klass_or_instance klass = klass_or_instance.is_a?(Class) ? klass_or_instance : klass_or_instance.class - "#{BASE_CONTENT_TYPE}.#{klass.content_type}-v#{API_VERSION}+#{format}" + "#{BASE_CONTENT_TYPE}.#{klass.content_type}-v#{API_VERSION}+#{response_parser.format}" end end end \ No newline at end of file