Structure - mirkode/asana-code-challenge GitHub Wiki

The app consists of multiple files.

├- app.rb - (the main entry point)
├- config.ru 
├- Gemfile
├- Gemfile.lock
├- README.md - (the file you're just reading)
└- adapters
│  ├- adapter.rb - (connects to the external API and retrieves the 'raw' response data)
│  └- plugs - (a folder of provider specific logic)
│     ├- bing.rb - (Bing-specific endpoint and stub for response parsing)
│     ├- google.rb - (Google Maps-specific endpoint and response parsing)
│     └- osm.rb - (OpenStreetMaps-specific endpoint and response parsing)
├- public
|  └- index.html (the demo page)
└- spec
   └- ... (test-suite)

General

The app.rb includes two routes. One is the simple API GET endpoint, the other one just shows the static demo page (index.html).
The app itself is structure in such a way so multiple external API providers can be used.
The Adapter object (adapter.rb) handles all requests regardless of the provider. It gets initialized with a query (e.g. the address) and an optional provider (e.g. Google). If no provider is passed in, the default provider, as specified in the .env file, will be used.
The Adapter class includes HTTParty and inherits its methods. That allows us to call get(url) in the Adapter class.
After initialization we call .get_coordinates on the Adapter object. This causes the object to instantiate a new provider-based Plug object (with the given provider name) and retrieves the provider specific endpoint.
Please note: The provider name in the .env file must match case sensitively the name of the respective 'Plug' class (more on that below).
Having retrieved the endpoint, the Adapter object now invokes the internal get method, which again invokes the internal api_call method.
The get method returns two values: the response and wether the request was a success or not.
If it was successful, the response is checked for any results. If there aren't any, a GeoCodingError is raised with the error message no results. This handles invalid and/or non-existant addresses.
If there are results, those get parsed in a provider-specific and returned in a standardized manner, because obviously each provider responses look differently.
This results in a consistent API output, regardless of which provider is being used in the end.
The GET request itself is quite straight forward. As mentioned before the get method calls the internal api_call method. This has the advantage that other requests, such as PUT or POST can also be used without the need to copy the HTTParty request functionality.
The api_call uses the respective HTTParty method to connect to the external API endpoint (as passed through the provider-specific Plug).
Unless the returned HTTP status code from HTTParty does not match 200 a GeoCodingError is raised.
If it matches the success status code, the status code, as well as the raw response, gets returned as the response variable by the get method.

Plugs

I mentioned the Plugs twice in the section above.
A Plug is serving the provider-specific logic and handles the authorization, endpoint construction, and response parsing for every needed API endpoint.
Those are located in the plugs folder within the adapters folder.
If you would need a new external API endpoint three methods are mandatory:

  1. endpoint: here you would need to create the endpoint URL as given by the provider (the return value would be something like "https://maps.googleapis.com/maps/api/geocode/json?address=SEARCH_QUERY&key=GOOGLE_API_KEY").
  2. has_no_results?(response): here the response gets checked if it has any results. This needs to be adjusted to the API response of the external provider and return either true|false.
  3. parse_response(response): in this method the response needs to be parsed and a standardized JSON needs to be returned. Please see the format of the JSON below:
{
  'latitude': 52.0000000,
  'longitude': 13.000000,
  'formatted': '<the formatted address>',
  'type': '<the location type (e.g. recreational)>'
}

In order to use a plug all that needs to be done is to require it in the adapter.rb file (e.g. require plugs/google) and then be either passed as a GET param (e.g. /search?address=xyz&provider=OSM, or set as a default in the .env file (resp. in the specific ENV variable called DEFAULT_PROVIDER).
Even if there is a use case to try and find really every address/location by using multiple providers, a loop over all those providers (e.g. using something like Threads and Mutex or so) would be also possible in the get_coordinates method. Instead of firing one GET request to one provider, one could loop over all providers, fire a GET request, parse the response, add it to the locations and then, when all providers have been accessed, sort the locations array and remove possible duplicates.