cloudspokes/cs-website-cms

View on GitHub
app/models/api_model.rb

Summary

Maintainability
A
3 hrs
Test Coverage
class ApiModel
  include ActiveModel::Model

  cattr_accessor :access_token  

  # Implements the has_many relationship
  # Passing :parent as an option allows modification of the calling class
  # This is used mostly for has and belongs to many relationships, where
  # a model collection will have a different endpoint
  # Case in point: Members and Challenges
  def self.has_many(entity, options={})
    # add in this relationship to the column_names table
    @column_names << entity.to_sym
    rel_column_names << entity.to_sym
    parent = options[:parent]

    # dynamically create a method on this instance that will reference the collection
    define_method("#{entity.to_sym}=") do |accessor_value|
      instance_variable_set("@#{entity.to_sym}", accessor_value)
    end

    define_method(entity.to_sym) do
      klass = entity.to_s.classify.constantize
      (parent || klass).get_has_many([to_param, entity.to_s]).map do |e|
        next if e.respond_to?(:last) # we got an array instead of a Hashie::Mash
        klass.new e
      end
    end
  end

  # Overrides the attr_accesssor class method so we are able to capture and
  # then save the defined fields as column_names
  def self.attr_accessor(*vars)
    @column_names ||= []
    @column_names.concat( vars )
    super
  end

  # Returns the previously defined attr_accessor fields
  def self.column_names
    @column_names
  end

  def self.rel_column_names
    @rel_column_names ||= []
  end

  # Returns the api_endpoint. Note that you need to implement this method
  # in the child object
  def self.api_endpoint
    raise 'Please implement ::api_endpoint in the child object'
  end

  # Returns all the records from the CloudSpokes API
  def self.all
    http_get(api_endpoint).map {|item| new item}
  end

  # Returns the first record
  def self.first
    all.first
  end

  # Wrap initialize with a sanitation clause
  def initialize(params={})
    @raw_data = params.dup
    params.delete_if {|k, v| !self.class.column_names.include? k.to_sym}
    super(params)
  end

  # Returns the raw data that created this object
  def raw_data
    @raw_data
  end

  # Returns if this record has the id attribute set (used by url_for for routing)
  def persisted?
    !!id
  end

  def save
    new_record? ? create : update
  end

  def update_attributes(attrs={})
    attrs.each do |attr, value|
      self.public_send("#{attr}=", value)
    end
    save
  end

  def new_record?
    id.blank?
  end

  def self.create(attrs)
    obj = new(attrs)
    obj.save
    obj
  end

  def self.api_request_headers
    headers =  {
      'Authorization' => 'Token token="'+ENV['CS_API_KEY']+'"',
      'Content-Type' => 'application/json'
    }
    headers.merge!('oauth_token' => access_token) if access_token
    headers
  end

  # Finds an entity (i.e., /members/jeffdonthemic) and any supported params {fields: 'id,name'}
  def self.find(entity, params = nil)
    Kernel.const_get(self.name).new(http_get "#{self.api_endpoint}/#{entity}", params)
  end  

  # temp - for use with node api. passed entire endpoint
  def self.http_get_v2(endpoint, params = nil)
    options = { headers: api_request_headers }
    options.merge!(query = {query: params}) if params.present?
    response = HTTParty::get(endpoint, options)
    Rails.logger.fatal "[INFO][v2] #{response['serverInformation']['requestDuration']}ms response time for #{response.request.last_uri}"
    process_response(response)      
  rescue Exception => e
    Rails.logger.fatal "[FATAL][v2] Processing error: #{e.to_yaml}"
  end  

  def self.http_get(endpoint, params = nil)
    options = { headers: api_request_headers }
    options.merge!(query = {query: params}) if params.present?
    if access_token
      process_response(HTTParty::get("#{ENV['CS_API_URL']}/#{endpoint}", options))      
    else
      Rails.logger.info "[PUBLIC-GET] #{ENV['CS_API_URL']}/#{endpoint}?#{options[:query].to_param}"
      Rails.cache.fetch("#{ENV['CS_API_URL']}/#{endpoint}?#{options[:query].to_param}", :expires_in => ENV['MEMCACHE_EXPIRY'].to_i.minute) do
        Rails.logger.info "[PUBLIC-GET] #{ENV['CS_API_URL']}/#{endpoint}?#{options[:query].to_param} -- Call to API"
        process_response(HTTParty::get("#{ENV['CS_API_URL']}/#{URI.escape(endpoint)}", options))
      end
    end
  end

  def self.http_post(endpoint, params)
    options = { 
      headers: api_request_headers, 
      body: params.to_json
    }
    process_response(HTTParty::post("#{ENV['CS_API_URL']}/#{endpoint}", options))
  end

  def self.http_put(endpoint, params)
    options = { 
      headers: api_request_headers, 
      query: params
    }
    process_response(HTTParty::put("#{ENV['CS_API_URL']}/#{endpoint}", options))
  end   

  def self.get_has_many(entities = [], params)
    endpoint = has_many_endpoint_from_entities(entities)
    endpoint << "/#{params.to_param}" unless params.empty?  
    http_get endpoint, params
  end 

  def self.has_many_endpoint_from_entities(entities = [])
    entities = entities.respond_to?(:join) ? entities.join("/") : entities.to_s
    entities.present? ? "#{has_many_api_endpoint}/#{entities}" : has_many_api_endpoint
  end   

  def self.process_response(response)
    case response.code
      when 200
        resp = Hashie::Mash.new(response).response
        raise ApiExceptions::SFDCError.new(resp.first.errorcode, resp.first.message, response.request.last_uri) if resp.is_a?(Array) && resp.first.is_a?(Hash) && resp.first.has_key?('errorcode')
        resp
      when 404
        raise ApiExceptions::EntityNotFoundError.new 
      when 401
        raise ApiExceptions::AccessDenied.new         
      when 500...600
        Rails.logger.fatal "[FATAL] WTF Error processing response (#{response.code}): #{response}. URL: #{response.request.last_uri}. Options: #{response.request.options.to_s}"
        raise ApiExceptions::WTFError.new 
    end    
  end   

  private

    def save_data
      columns = self.class.column_names - self.class.rel_column_names - [:id]
      columns.inject({}) do |ret, column|
        val = self.public_send(column)
        ret[column] = val if val.present?
        ret
      end
    end

    # define update_endpoint to subclass if you want to use another url for update
    def update_endpoint
      id
    end

    # define create_endpoint to subclass if you want to use another url for create
    def create_endpoint
      ""
    end

    def update
      self.class.put update_endpoint, save_data
    end

    def create
      self.class.post create_endpoint, save_data
    end
end