2

I have two APIs with different resources:

  • www.api-A.com**/consumers,

    which returns: {consumers: ['mike', 'Anna', 'Danilo']}

  • www.api-B.com**/clients,

    which returns: {clients: ['Jack', 'Bruce', 'Mary']}

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?

sawa
  • 165,429
  • 45
  • 277
  • 381
Danilo Cândido
  • 408
  • 1
  • 5
  • 18
  • What about duplicates? – spickermann Nov 04 '18 at 13:31
  • Sometimes the response of each api can be different. But I woud like to avoid duplication too. Because I can have not two Wrapper APi's, but 3 our 5 or more. – Danilo Cândido Nov 04 '18 at 14:23
  • because of your purpose this might not a proper comment. if that I apologize. :-) how about defining class? to support those resource similar to model. or have a look this gem. https://github.com/remiprev/her#multiple-apis I think if you have to call `3 our 5 or more` it'll be sequential and may effect on response. – unlimitedfocus Nov 04 '18 at 16:22

2 Answers2

3

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?

Leticia Esperon
  • 2,499
  • 1
  • 18
  • 40
  • It's good, that way I can you Client in both ApiAWrapper and ApiBWrapper. But how would be my controller action if call these too wrappers? – Danilo Cândido Nov 04 '18 at 14:09
  • 1
    The same as in your question. I would still wrap those queries in another service so the controller doesn't know you are querying two different APIs. Maybe a `ClientService` with a method that returns the array of clients from both APIs and then in your controller you just do `@clients = ClientService.all_clients` – Leticia Esperon Nov 04 '18 at 20:23
3

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
vinibrsl
  • 6,563
  • 4
  • 31
  • 44