Danilo Cândido
Danilo Cândido

Reputation: 408

Design pattern to use results of different APIs

I have two APIs with different resources:

I would like to use these two results in one controller. I want to treat them like if there were just one.

Do I have to create a wrapper for each api like:

module ApiAWrapper
  #code here
end

module ApiBWrapper
  #code here
end

and call the following inside my controller?

MyController
  def index
    @clients << ApiAWrapper.most_recent
    @clients << ApiBWrapper.most_recent
    @clients
  end
end

Doing this, @clients will be:

['mike', 'Anna', 'Danilo', 'Jack', 'Bruce', 'Mary']

Is this the right way to use these different APIs with similar responses? Is there a design pattern that I can use or I should read about to guide me?

Upvotes: 2

Views: 1668

Answers (2)

justapilgrim
justapilgrim

Reputation: 6862

When I need external services to respond in a common way, I implement a parser. In other languages, you could use interfaces to enforce a method signature contract, but Ruby doesn't have this feature because of the duck typing.

This parser could be a function or a module. For example:

module GitHub
  class Service
    BASE_URI = 'https://api.github.com'

    def self.fetch
      response = HTTP.get("#{BASE_URI}/clients")
      raise GitHub::ApiError unless response.ok?
      Parser.new(response).to_common
    end
  end

  class Parser
    def initialize(response)
      @response = response
    end

    def to_common
      json_response = JSON.parse(@response)
      json_response[:customers] = json_response.delete :clients
      # more rules
      # ...
      json_response
    end
  end
end

Ok, there you go. Now you've got a Service, for fetching and handling the HTTP part, and the Parser, that handles the response body from the HTTP request. Now, let's suppose that you want to use another API, the BitBucket API, for instance:

module BitBucket
  class Service
    BASE_URI = 'https://bitbucket.com/api'

    def self.fetch
      response = HTTP.get("#{BASE_URI}/customers")
      raise BitBucket::ApiError unless response.ok?
      Parser.new(response).to_common
    end
  end

  class Parser
    def initialize(response)
      @response = response
    end

    def to_common
      json_response = JSON.parse(@response)
      json_response[:clients] = (json_response.delete(:data).delete(:clients))
      # more rules
      # ...
      json_response
    end
  end
end

This way, you'll have both services returning using the same interface. To join the results, you could do:

data = [GitHub::Service.fetch, BitBucket::Service.fetch, ...]
names = data.map { |customer_list| customer_list[:name] }
names.uniq

Upvotes: 3

Leticia Esperon
Leticia Esperon

Reputation: 2841

You should have wrappers for your API calls anyway because the controller should have as little logic as possible. Regardless, I would create a class Client with a method to deserialize an array of client jsons into an array of clients. That way, in both wrappers you would call this method and return the array of clients ready to concat in the controller.

Something like:

class Client
  attr_accessor :name

  def initialize(client_json)
    @name = client_json['name']
  end

  def self.deserialize_clients(clients_json)
    clients_json.map{ |c| Client.new(c) }
  end
end

Then for the wrappers:

module ApiAWrapper
   def self.most_recent
     response = #api call here
     Client.deserialize_clients(JSON.parse(response.body))
   end
end

What do you think?

Upvotes: 3

Related Questions