In this chapter we are going to integrate two external APIs into our Walks application. We are going to see how integrating external services is somewhat similar to integrating an internal API. There are some issues regarding the way we consume and offer external content that need to be considered, however.
Imagine that our Walks application could be enriched with a weather forecast, or that our Citywalks API could also suggest that users pack an umbrella or sunscreen before leaving the house. If you are thinking what I am thinking right now, you would agree that this would be a nice feature to have.
The problem with this is that you actually need a simple weather forecasting service to implement it—yet you may want to code a way to get weather forecasts in your app yourself. The ideal solution would be a third-party service that you could talk to to get a weather forecast for a particular city or location.
Luckily for us, there are a number of APIs that offer this service on the Web. Most of these also allow your app to make a certain number of calls per day for free, which is our ideal situation since we are just starting with a test project.
I have chosen to use the OpenWeatherMap project, since it adopts an approach to its business and service that is inspired by OpenStreetMap and Wikipedia and involves making its information available to everybody to use. OpenWeatherMap collects data from weather stations and forecasts from meteorological services and research laboratories, combining long-term and short-term forecasts with real-time data from weather stations. It processes all of this data and immediately updates the current weather and forecasts in its API.
Of course, you are free to integrate another service if you find something else that you like more. Please note that in this case the JSON returned will probably require different parsing, but in the end you should be able to easily adapt the code in the repository. As usual, you are encouraged to fork and modify what you see fit.
To be able to provide weather recommendations through the Citywalks API we will need to create a new endpoint. We are going to call this suggestions. When a user calls the suggestions endpoint, this will consult the OpenWeatherMap API; if the percentage of cloud cover is predicted to be over 40% it will suggest some videos from YouTube, and otherwise it will return places of interest in the area. Figure 12-1 illustrates this configuration.
To search YouTube videos, the user can pass a category
parameter. If the user doesn’t pass the category
param, the API will search for Radiohead videos.
To begin, we are going to create a new controller in api/v1/:
class
Api
::
V1
:
:SuggestionController
<
ApplicationController
respond_to
:json
include
MakeSuggestion
def
index
if
@suggestions
render
:json
=>
@suggestions
.
to_json
else
@error
=
Error
.
new
(
:text
=>
"404 Not found"
,
:status
=>
404
,
:url
=>
request
.
url
,
:method
=>
request
.
method
)
render
:json
=>
@error
.
serializer
end
end
end
MakeSuggestion
is the concern that is going to handle part of the logic behind the suggestion controller. Therefore, we need to create a file in controllers/concerns called make_suggestion.rb. First we need to define an entry action that takes place as soon as we hit the endpoint:
def
entry_action
@suggestions
=
[]
if
params
[
:location
]
location
=
params
[
:location
].
split
(
','
)
@suggestions
<<
make_forecast
(
location
[
0
]
,
location
[
1
]
)
else
@suggestions
<<
suggestions_by_ip
end
end
Then we define the make_forecast
function:
def
make_forecast
(
latitude
,
longitude
)
forecast
=
get_weather
(
latitude
,
longitude
)
if
forecast
[
:forecast
].
doc
[
"clouds"
][
"all"
]
>
40
request_youtube_videos
(
params
[
:category
]
||
"Radiohead"
)
else
request_pins
(
"
#{
longitude
}
,
#{
latitude
}
"
)
end
end
Both the get_weather
and request_youtube_videos
methods will be defined in their respective modules.
The last method that we will need to define is suggestions_by_ip
. This method will simply find out the request IP and corresponding location before calling the make_forecast
method:
def
suggestions_by_ip
if
ip
==
"127.0.0.1"
get_weather
(
"41.23"
,
"2.09"
)
else
block
=
Walk
.
request_block
(
ip
)
.
doc
[
"ip_block"
]
if
block
longitude
=
block
[
"point"
].
scan
(
/(([^)]+))/
)
.
last
.
first
.
split
(
" "
)
[
0
]
latitude
=
block
[
"point"
].
scan
(
/(([^)]+))/
)
.
last
.
first
.
split
(
" "
)
[
1
]
make_forecast
(
latitude
,
longitude
)
end
end
end
Now we can create the modules that we need in the lib/ folder. We start with the Weather
module by creating a file called weather.rb:
module
Weather
WEATHER_URL
=
"http://api.openweathermap.org/data/2.5/weather?"
def
get_weather
(
lat
,
lon
)
forecast
=
{
forecast
:
send_request
(
"
#{
WEATHER_URL
}
lat=
#{
lat
}
&lon=
#{
lon
}
"
)
}
end
end
WEATHER_URL
defines our endpoint on the OpenWeatherMap API; then we just define a method to call the API and pass latitude and longitude information.
The Youtube
module is going to be a little bit more complicated to define. This is because to be able to use the YouTube Data API, our application must have authorization credentials. The Google Developers Console supports different types of authentication methods that need to be configured for your project.
Start by visiting the Google Developers Console and selecting or creating a new project. In the sidebar on the left, select “APIs & auth.” In the list of APIs, make sure the status is “ON” for the YouTube Data API v3.
Again in the sidebar on the left, select “Credentials.” The API supports two types of credentials. Create whichever credentials are appropriate for your project.
If you choose OAuth 2.0, your application must send an OAuth 2.0 token with any request that accesses private user data. Your application sends a client ID and, possibly, a client secret to obtain a token. You can generate OAuth 2.0 credentials for web applications, service accounts, or installed applications.
If you choose to authenticate with API keys, you will use a developer key to authenticate your request. We will use this method. The key identifies your project and provides API access, quotas, and reports.
If the key type you need does not already exist, create an API key by selecting “Create New Key” and then selecting the appropriate key type, which in our case is a server key. Then enter the additional data required for that key type: in our case, the list of IPs that are allowed to use that key. If you are running your application in your local machine, this list will probably need to include only:
Otherwise, you will need the IP of the server where your app is running.
Once you have finished configuring your app in the Developers Console, you can start writing the module:
require
"
net/http
"
require
"
json
"
require
"
google/api_client
"
require
"
trollop
"
module
Youtube
DEVELOPER_KEY
=
<
Your
Developer
Key
>
YOUTUBE_API_SERVICE_NAME
=
"
youtube
"
YOUTUBE_API_VERSION
=
"
v3
"
def
get_service
client
=
:
:APIClient
.
new
(
:key
=
>
DEVELOPER_KEY
,
:authorization
=
>
nil
,
:application_name
=
>
<
Your
Application
Name
>
,
:application_version
=
>
'1.0.0'
)
youtube
=
client
.
discovered_api
(
YOUTUBE_API_SERVICE_NAME
,
YOUTUBE_API_VERSION
)
return
client
,
youtube
end
def
wrap_search_results
(
videos
,
channels
,
playlists
)
search_results
=
{
videos
:
videos
,
channels
:
channels
,
playlists
:
playlists
}
end
def
wrap_json
(
search_result
)
result
=
{
title
:
search_result
.
snippet
.
title
,
description
:
search_result
.
snippet
.
description
,
url
:
make_video_url
(
search_result
)
}
end
def
make_video_url
(
search_result
)
case
search_result
.
id
.
kind
when
'youtube#video'
"
https://www.youtube.com/watch? v=
#{
search_result
.
id
.
videoId
}
"
when
'youtube#channel'
"
https://www.youtube.com/channel/
#{
search_result
.
id
.
channelId
}
"
when
'youtube#playlist'
"
https://www.youtube.com/playlist? list=
#{
search_result
.
id
.
playlistId
}
"
end
end
def
request_youtube_videos
(
query
)
opts
=
Trollop
:
:options
do
opt
:q
,
'Search term'
,
:type
=
>
String
,
:default
=
>
query
opt
:max_results
,
'Max results'
,
:type
=
>
:int
,
:default
=
>
25
end
client
,
youtube
=
get_service
begin
# Call the search.list method to retrieve results matching the specified
# query term.
search_response
=
client
.
execute!
(
:api_method
=
>
youtube
.
search
.
list
,
:parameters
=
>
{
:part
=
>
'snippet'
,
:q
=
>
opts
[
:q
]
,
:maxResults
=
>
opts
[
:max_results
]
}
)
videos
=
[
]
channels
=
[
]
playlists
=
[
]
# Add each result to the appropriate list, and then display the lists of
# matching videos, channels, and playlists.
search_response
.
data
.
items
.
each
do
|
search_result
|
case
search_result
.
id
.
kind
when
'youtube#video'
videos
<<
wrap_json
(
search_result
)
when
'youtube#channel'
channels
<<
wrap_json
(
search_result
)
when
'youtube#playlist'
playlists
<<
wrap_json
(
search_result
)
end
end
wrap_search_results
(
videos
,
channels
,
playlists
)
rescue
:
:APIClient
::
TransmissionError
=
>
e
puts
e
.
result
.
body
end
end
end
Now we can call our new endpoint:
$
curl
http
:
/
/
0
.
0
.
0
.
0
:
3001
/
api
/
v1
/
suggestion?
location
=
41
.
23
,
2
.
09
You can try different locations to see how the results change with the cloud coverage.
Every API defines some terms of service stating what users can do and what they cannot do with the service offered. You will have to consider the terms of the services you decide to integrate to better understand if the APIs you have decided to use are a good fit for your purposes.
When you review different services you might find that some APIs are a good fit technically, but will not scale as your app starts growing and serving more users. Some other APIs might instead provide very few limits in terms of traffic or the number of requests that you can send them, but restrict how you use the information provided.
While you’re thinking about the terms of service of the APIs integrated into your app, you should also be thinking about how to develop your own terms of service for when you publish your API. The terms of service are a way to tell your users that you are offering your API under specific conditions. If those conditions are not met, you can decide to stop users from using your service.
This is particularly important if you are offering subscriptions to use your service. When the users subscribe, they agree to a contract, and the terms of service can also be part of this contract.
Thinking about your terms of service might also help you define your business model. For example, you might have published your API under some conditions for a certain amount of time. If you now recognize that you might offer a different kind of service, with the same infrastructure, you might as well offer it as an additional plan for your users to subscribe to. This is a typical scenario when you start developing an application that users can subscribe to for free, and then you have to figure out a way to scale this into a business and make money out of it.
In cases like this it sometimes makes sense to offer a pay-per-use plan or a paid subscription offering some additional features to the free plan.
If you are thinking about setting up a startup and considering ways to get funded, you might find there are periods in which you have to live off your savings because your business isn’t profitable yet.
Paul Graham wrote some years ago about the notion of being “ramen profitable,” or just profitable enough to survive and for founders to have time to develop the business.
Up to this moment we have relied on the concept that every time we make a request to the server we are able to receive an answer almost immediately (allowing for small communication delays between the client and the server). This unfortunately isn’t always the case.
Consider a situation where your users need to create resources, and these operations might take a considerable amount of time to complete. This could be the case when there is some image processing, for example, that happens in your backend. If you let the client wait for the request to finish, this might time out and return an error instead.
In cases like this you could have your API return a 202 (Accepted) response. This will tell the client that the request has been accepted for processing, but the processing has not been completed yet. When the server returns a 202 response, the client doesn’t know exactly what will happen to its request, since there is no facility for status returns from asynchronous operations such as this. The request is not guaranteed to eventually be acted upon, as it may be disallowed when processing actually takes place. Therefore, you can also return a Location: /resources/xxxxxx
field in your response telling the client where to ask for the actual status of the request.
For more on HTTP responses, see “HTTP Semantics”.
When the resource has finally been created and the request has completed, this temporary resource can return a 303 (See Other) response and the URI to the definitive resource.
To create a temporary resource, you might consider writing a model in your application that is not persisted to your database. Although you do not need to persist data to your database, the client might have to query the server, possibly more than once, to know the status of the requested resource and to ensure that the action being processed in the backend has actually completed.
In situations like this you might consider writing your object to a Redis instance. Redis is a key/value cache and store, released under the open source BSD license. It is also referred to as a data structure server since keys can contain strings, hashes, lists, sets, sorted sets, bitmaps, and hyperloglogs. In our case Redis will function as a normal database, storing our temporary resources for the time we need them.
To install Redis you will have to download and compile the source. A complete installation guide can be found at http://redis.io/topics/quickstart.
Under OS X with Homebrew you can just run:
$
brew
install
redis
We will use an object-hash mapping library for Redis called Ohm. Ohm comes packed in a gem for Ruby on Rails. You can install it by adding the following to your Gemfile:
gem
'ohm'
gem
'ohm-contrib'
We are now going to define our temporary resource model under app/models:
require
'ohm'
require
'ohm/contrib'
class
Process
<
Ohm
:
:Model
include
Ohm
:
:Versioned
attribute
:process_id
attribute
:message
attribute
:location
attribute
:status
attribute
:code
index
:temp_id
Ohm
.
redis
=
Redis
.
new
(
ENV
[
'REDIS_URL'
]
)
def
serialize
{
process
:
{
id
:
process_id
,
location
:
location
},
response
:
{
message
:
message
,
code
:
code
.
to_i
},
status
:
status
.
to_i
}
end
end
and define a processes controller in api/v1:
class
API
::
V1
:
:ProcessesController
<
ApplicationController
include
ProcessOperations
def
show
@process
=
Process
.
find
(
process_id
:
params
[
:id
]
)
.
first
if
@process
render
json
:
@process
.
serialize
,
status
:
@process
.
serialize
[
:status
]
else
render
json
:
process_not_found
,
status
:
404
end
end
def
create
if
long_action_to_be_performed
@process
=
create_process
(
params
)
render
json
:
@process
,
status
:
@process
[
:status
]
else
render
json
:
process_error
,
status
:
500
end
end
end
The show action fetches the process and displays its status. The create action triggers the long_action_to_be_performed
method and creates our temporary process to track it.
We are not going to define the method long_action_to_be_performed
since it can be a worker triggered or a service.
We are instead going to define the ProcessOperations
module:
module
ProcessOperations
extend
ActiveSupport
:
:Concern
def
create_process
(
params
)
process
=
Process
.
create
(
process_id
:
params
[
:id
]
,
message
:
"Accepted"
,
location
:
"processes/
#{
params
[
:id
]
}
"
,
code
:
202
,
status
:
202
)
if
process
process
.
serialize
else
process_error
end
end
def
process_error
error
=
{
response
:
{
message
:
"Internal Server Error"
,
code
:
500
},
status
:
500
}
end
def
process_not_found
error
=
{
response
:
{
message
:
"Not Found"
,
code
:
404
},
status
:
404
}
end
end
The ProcessOperations
module creates the process and returns structured errors when called.
Finally, we are going to define the routes for the new model and controller we’ve introduced:
namespace
:api
do
namespace
:v1
,
format
:
:json
do
post
'processes/create'
,
to
:
'processes#create'
get
'processes/:id'
,
to
:
'processes#show'
end
end
The asynchronous REST implementation shown here implies that the client will use polling to query the temporary resource until a 303 response is returned. The client itself can also implement some logic to perform the polling after a certain amount of time. Another option would be to implement web hooks on the server side and trigger an HTTP callback notifying the client that the resource is ready.
In this chapter we discussed how to integrate external data streams into our application. In the next chapter we will see how this leads us to design and develop products independently from the devices where they will be run. This approach to software development can be considered valid for web, mobile, or even IoT applications.
18.117.188.138