module Wordnik class Request require 'uri' require 'addressable/uri' require 'typhoeus' require 'active_model' require "wordnik/version" include ActiveModel::Validations include ActiveModel::Conversion extend ActiveModel::Naming attr_accessor :host, :path, :format, :params, :body, :http_method, :headers validates_presence_of :format, :http_method # All requests must have an HTTP method and a path # Optionals parameters are :params, :headers, :body, :format, :host # def initialize(http_method, path, attributes={}) attributes[:format] ||= Wordnik.configuration.response_format attributes[:params] ||= {} # Set default headers default_headers = { 'Content-Type' => "application/#{attributes[:format].downcase}", :api_key => Wordnik.configuration.api_key, :user_agent => Wordnik.configuration.user_agent } # api_key from headers hash trumps the default, even if its value is blank if attributes[:headers].present? && attributes[:headers].has_key?(:api_key) default_headers.delete(:api_key) end # api_key from params hash trumps all others (headers and default_headers) if attributes[:params].present? && attributes[:params].has_key?(:api_key) default_headers.delete(:api_key) attributes[:headers].delete(:api_key) if attributes[:headers].present? end # Merge argument headers into defaults attributes[:headers] = default_headers.merge(attributes[:headers] || {}) # Stick in the auth token if there is one if Wordnik.authenticated? attributes[:headers].merge!({:auth_token => Wordnik.configuration.auth_token}) end self.http_method = http_method.to_sym self.path = path attributes.each do |name, value| send("#{name.to_s.underscore.to_sym}=", value) end end # Construct a base URL # def url(options = {}) u = Addressable::URI.new( :scheme => Wordnik.configuration.scheme, :host => Wordnik.configuration.host, :path => self.interpreted_path, :query => self.query_string.sub(/\?/, '') ).to_s # Drop trailing question mark, if present u.sub! /\?$/, '' # Obfuscate API key? u.sub! /api\_key=\w+/, 'api_key=YOUR_API_KEY' if options[:obfuscated] u end # Iterate over the params hash, injecting any path values into the path string # e.g. /word.{format}/{word}/entries => /word.json/cat/entries def interpreted_path p = self.path.dup # Fill in the path params self.params.each_pair do |key, value| p = p.gsub("{#{key}}", value.to_s) end # Stick a .{format} placeholder into the path if there isn't # one already or an actual format like json or xml # e.g. /words/blah => /words.{format}/blah unless ['.json', '.xml', '{format}'].any? {|s| p.downcase.include? s } p = p.sub(/^(\/?\w+)/, "\\1.#{format}") end p = p.sub("{format}", self.format.to_s) URI.encode [Wordnik.configuration.base_path, p].join("/").gsub(/\/+/, '/') end # Massage the request body into a state of readiness # If body is a hash, camelize all keys then convert to a json string # def body=(value) if value.is_a?(Hash) value = value.inject({}) do |memo, (k,v)| memo[k.to_s.camelize(:lower).to_sym] = v memo end end @body = value end # If body is an object, JSONify it before making the actual request. # def outgoing_body body.is_a?(String) ? body : body.to_json end # Construct a query string from the query-string-type params def query_string # Iterate over all params, # .. removing the ones that are part of the path itself. # .. stringifying values so Addressable doesn't blow up. query_values = {} self.params.each_pair do |key, value| next if self.path.include? "{#{key}}" # skip path params next if value.blank? && value.class != FalseClass # skip empties key = key.to_s.camelize(:lower).to_sym unless key.to_sym == :api_key # api_key is not a camelCased param query_values[key] = value.to_s end # We don't want to end up with '?' as our query string # if there aren't really any params return "" if query_values.blank? # Addressable requires query_values to be set after initialization.. qs = Addressable::URI.new qs.query_values = query_values qs.to_s end def make(attempt = 0) # url is calculated, so we need to compute it once. u = self.url #Wordnik.logger.debug "Making attempt #{attempt}; now fetching #{u}" if attempt > 0 request = Typhoeus::Request.new(u, :headers => self.headers.stringify_keys, :method => self.http_method.to_sym) # Make request proxy-aware if Wordnik.configuration.proxy.present? request.proxy = Wordnik.configuration.proxy request.proxy_username = Wordnik.configuration.proxy_username if Wordnik.configuration.proxy_username.present? request.proxy_password = Wordnik.configuration.proxy_password if Wordnik.configuration.proxy_password.present? end Wordnik.logger.debug "\n #{self.http_method.to_s.upcase} #{u}\n body: #{self.outgoing_body}\n headers: #{request.headers}\n\n" request.body = self.outgoing_body unless self.http_method.to_sym == :get # Execute the request — blocking call here. Typhoeus::Hydra.hydra.queue request Typhoeus::Hydra.hydra.run # if we are using local load balancing, check for timeouts and connection errors resp = request.response if Wordnik.configuration.load_balancer if (resp.timed_out? || resp.code == 0) # Wordnik.logger.debug "informing load balancer about failure" Wordnik.configuration.load_balancer.inform_failure if (attempt <= 3) # Wordnik.logger.debug "Trying again after failing #{attempt} times..." return make(attempt + 1) if attempt <= 3 # try three times to get a result... end else # Wordnik.logger.debug "informing load balancer about success" Wordnik.configuration.load_balancer.inform_success end end Response.new(resp) end def response self.make end def response_code_pretty return unless @response.present? @response.code.to_s end def response_headers_pretty return unless @response.present? # JSON.pretty_generate(@response.headers).gsub(/\n/, '
') # <- This was for RestClient @response.headers.gsub(/\n/, '
') # <- This is for Typhoeus end # It's an ActiveModel thing.. def persisted? false end end end