1
\$\begingroup\$

I am trying to create an API wrapper for recharge (A Shopify subscription service), I am using the HTTParty gem

module RechargeAPI
  require 'httparty'
  BASE_URI = 'https://api.rechargeapps.com'
  API_TOKEN = 'my_token'

  class Client
    include HTTParty
    base_uri BASE_URI
    headers 'X-Recharge-Access-Token': API_TOKEN
  end

  class Customer < Client
    def self.search(params)
      response = get('/customers', query: params)
      self.from_json(response.body)
    end

    def self.find(params)
      self.search(params).first
    end

    def self.all
      response = get('/customers')
      self.from_json(response.body)
    end

    def self.from_json(customers_json)
      customers = JSON.parse(customers_json).dig('customers')
      customers.map do|customer| OpenStruct.new(customer)
      end
    end

  end

end

RechargeAPI::Customer.find(shopify_customer_id: 5363543224286) # returns <OpenStruct accepts_marketing=nil, analytics_data={"utm_params"=>[]}, billing_address1=....

It works fine, However i feel i am not using the best practices for writing an api wrapper. Ideally i would set my api token with something like RechargeAPI.api_token = 'token' rather than it being hardcoded or in an ENV file. But i dont know how then i would use headers 'X-Recharge-Access-Token': API_TOKEN

Also ideally RechargeAPI::Customer.find(shopify_customer_id: 5363543224863) would return a RechargeAPI::Customer object rather than an OpenStruct. I would love to be able to inherit from Struct but obviously i cannot as I am inheriting from my RechargeAPI::Client class.

Could anybody advise on how i could go about doing this, or any way to improve this code. Thankyou!

\$\endgroup\$
1
  • \$\begingroup\$ Welcome to Code Review! The current question title is too general to be useful here. Please edit to the site standard, which is for the title to simply state the task accomplished by the code. Please see How to get the best value out of Code Review: Asking Questions for guidance on writing good question titles. \$\endgroup\$ Commented Sep 23, 2021 at 14:43

2 Answers 2

0
\$\begingroup\$

I mostly agree with Sidney. Depending how big the API is I would not split 1 (A class responsible for your HTTP requests to Shopify in general) and 2 (A class that calls your HTTP class to make the requests; it calls the right URLs and creates the domain objects).

Here is an example

module RechargeAPI
  require 'httparty'
  BASE_URI = 'https://api.rechargeapps.com'
  API_TOKEN = 'my_token'

  class Client
    include HTTParty
    base_uri BASE_URI
    headers 'X-Recharge-Access-Token': API_TOKEN

    def self.search_customers(params)
      JSON.parse(get('/customers', query: params))
    end

    def self.customers
      JSON.parse(get('/customers'))
    end
  end
end

A good example how to implement an API wrapper is e.g. https://github.com/octokit/octokit.rb or https://github.com/Shopify/buildkit.

Then you can use your API wrapper to build your PORO / model classes like this

class Customer < Struct.new(:id, :name)
  cattr_accessor :client
    RechargeAPI::Client.new
  end

  def self.all
    client.customers.map do |params|
      new(params[:id], params[:name])
    end
  end

  def self.search(params)
    client.search(params).map do |params|
      new(params[:id], params[:name])
    end
  end

  def self.find(params)
    search(params).first
  end
end

Please note that I use a class attribute accessor (cattr_accessor) to instantiate the client on the Customer class which is implement in ActiveSupport / Rails. This has the advantage that you don't need to inherit from your API class anymore and you can now use dependency injection to replace the client. For instance, in testing you can now use a dummy client instead of doing real HTTP calls.

class DummyClient
  self.customers
    { id: 1, name: 'name' }
  end
end

def test_customer_all_returns_customers
  Customer.client = DummyClient.new

  assert_equal 1, Customer.all.count
  customer = Customer.first
  assert_equal 1, customer.id
  assert_equal 'name', customer.name
end
\$\endgroup\$
1
\$\begingroup\$

I think you should have three classes:

  1. A class responsible for your HTTP requests to Shopify in general.
  2. A class that calls your HTTP class to make the requests; it calls the right URLs and creates the domain objects.
  3. The Customer class, which should be more independent, should not depend on the above classes; in DDD terms, it's a domain object.

In this approach, you will be using composition over inheritance.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ I like that. A Customer is not really an HTTP Client but rather a resource and should just encapsulate the business/domain logic (and maybe persistence if needed). \$\endgroup\$ Commented Oct 2, 2021 at 20:29
  • \$\begingroup\$ What would i do then if i needed to access a nested resource, e.g. customer_instance.products, Lets say in this example i get products by going to the endpoint /products?customer_id=foo \$\endgroup\$
    – Artyrcheek
    Commented Oct 12, 2021 at 17:50
  • \$\begingroup\$ I would do it all with pure Ruby. The product would be a new model called Product, then Customer#products points to an Array of Products. You can have a factory that, from the JSON, creates the Customer and its list of products. \$\endgroup\$
    – m26a
    Commented Oct 12, 2021 at 21:18
  • \$\begingroup\$ There maybe be some gem that helps you achieve or simplify what I've said. I didn't mention any gem because I don't know any, and they may not be necessary depending on your use case. It depends on how much you are used to using the gem and the complexity. \$\endgroup\$
    – m26a
    Commented Oct 12, 2021 at 21:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.