Creating an SDK Pt. 2
Check out Part 1 here
In the last part we compiled a list of endpoints that we’d like our SDK to hit. I’ve included the raw data responses in the appendix of this article (since there is quite a bit) & distilled the information into the table below.
Endpoint | Params | Return Values |
/upcoming/us/<league>
|
<league>
|
[%Matchup{}]
|
/ticker/<league>
|
<league>
|
[%Ticker{}]
|
/scores/<league>/<date>
|
<league> , <date>
|
[%Score{}]
|
/gamecenter/<league>/<id>
|
<league> , <id>
|
%Gamecenter{}
|
/play_by_play/<league>
|
<league>
|
[%Play{}]
|
Some data is common between objects, but each resource returned is unique its data shape. We’ll need to build out functions to wrap each endpoints, as well as additional functions if there are multiple parameters. Because Elixir uses pattern matching when defining functions, we can create multiple functions for each amount of arguments passed that will return the same resource. The table below converts the endpoints to their respective functions and return value.
Function | Return Values |
matchups(league)
|
[%Matchup{}]
|
tickers(league)
|
[%Ticker{}]
|
scores(league)
|
[%Score{}]
|
scores(league, date)
|
[%Score{}]
|
gamecenter(league, id)
|
%Gamecenter{}
|
play_by_plays(league)
|
[%Play{}]
|
As you can see, we defined 2 functions for scores
. The first function takes just a league
without a date - this function will default the date
to the current date. The second function takes a specific date, which should be of DateTime
type. We also added s
to all the function names that return an array, because we’ll like need to have singular versions that return specific instances of each.
One thing all these functions have in common is that they’ll need to make a GET
request to the OddsShark API. Let’s build out a module that will handle our GET
requests.
OddsShark Module
Assuming Elixir is installed, we’ll generate our project with mix
.
mix new oddsshark --module OddsShark
This creates a new project for us with a lib/
directory that includes a oddsshark.ex
file created for us by mix
. We’ll use file as the layer between our individual calls and the OddsShark API.
We’ll need HTTPoison to as our request library, so add the following to your mix.exs
.
{:httpoison, "~> 0.11.1"},
{:poison, "~> 3.0"},
The actions we’ll need to do to send a GET
request are:
- Build the endpoint url
- Send the request
- Handle the response
Our OddsShark
module will take in an endpoint, apply the correct headers, send the request, handle the response, and if the request was successful, return the response body. For OddsShark, we need to add a Referer header (which, side note, is not a reliable security measure in any way). Thinking from an Elixir standpoint of piping return values into functions, our chain of actions looks something like this:
build_endpoint ->
add_headers ->
send_request ->
handle_response ->
process_response_body
Working through that chain, I ended up with the following. You can see how I included HTTPoison
with the use
statement. use HTTPoison
includes the HTTPosion
module in the current lexical scope, giving us access to call it’s functions. We could have also used alias
or import
, but I prefer using use
in this context to explicitly mark the different function calls between methods we’ve defined and those defined in HTTPoison
.
defmodule OddsShark do
use HTTPoison.Base
@moduledoc """
HTTP requests
"""
@api_root "http://io.oddsshark.com/"
def get_request(endpoint) do
url = create_request_url(endpoint)
HTTPoison.get(url, create_headers())
|> handle_response
end
defp handle_response({:ok, %{ body: body, status_code: 200}}) do
{:ok, process_response_body(body)}
end
defp create_request_url(endpoint) do
Path.join(@api_root, endpoint)
end
defp create_headers do
[{"Referer", "http://www.oddsshark.com"}]
end
defp process_response_body(body) do
body
|> Poison.decode!
end
end
On display is some of the cool pattern matching that is baked into the Elixir language, deconstructing input parameters to get the exact values we need and none of the fluff. We also use the defp
to create private functions rather than def
to minimize the API face. We also use a module attribute to set the @api_root
rather than burying a string constant deep in a function.
Next up is to implement a request endpoint using this OddsShark
module along with a corresponding model to store the response data.
Appendix
Raw Data Responses
Endpoint | Params | Data Shape |
/upcoming/us/<league>
|
<league>
|
[ { away_display_name: "Chicago", away_money_line: "-126", away_short_name: "CHC", away_spread: "-1.5", away_spread_price: "130", away_team: "Chi Cubs", away_votes: 54, game_date: "2017-04-02 20:35:00", game_id: "753017", home_display_name: "St. Louis", home_money_line: "116", home_short_name: "STL", home_spread: "1.5", home_spread_price: "-150", home_team: "St. Louis", home_votes: 46, over_price: "-135", over_votes: 53, "total" => "7", under_price: "115", under_votes: 47 }, { <...> } ] |
/ticker/<league>
|
<league>
|
[ { "current": true, "date": { "day": 2, "fullday": "Sunday", "month": "Apr", "type": "date" } }, { "away_name": "NY Yankees", "away_odds": "-114", "away_score": "3", "away_short_name": "NYY", "event_id": "752992", "home_name": "Tampa Bay", "home_odds": "104", "home_score": "7", "home_short_name": "TB", "matchup_link": "<url>", "status": "FINAL", "total": "7", "trending_total": 10, "type": "matchup" }, { <more like 1st index> } ] |
/scores/<league>/<date>
|
<league> , <date>
|
[ { away_pitcher_record: "10-11", roof: "Open", away_abbreviation: "MIA", away_primary_color: "#040204", home_score: nil, total: "7.5", away_pitcher_name: "Edinson Volquez (R)", home_nick_name: "Nationals", home_team_id: "27017", away_short_name: "MIA", segments: [ { away_points: nil, home_points: nil, segment: 1 }, <...> ], away_predicted_score: "", home_pitcher_name: "Stephen Strasburg (R)", stadium: "Nationals Park", home_secondary_color: "#BC0E2C", away_city: "Miami", home_pitcher_id: "40047", event_date: "2017-04-03 13:05", away_money_line: "195", home_image_id: "<url>", away_pitcher_era: "5.42", boxscore_available: "No", matchup_link: "<url>", home_votes: 58, home_predicted_score: "", home_record: "97-70", status: "1:05 pm", away_spread: "1.5", away_nick_name: "Marlins", away_name: "Miami Marlins", away_record: "79-82", away_spread_price: "-120", home_primary_color: "#041E44", home_short_name: "WSH", home_abbreviation: "WSH", home_spread: "-1.5", stadium_rotation: "30", home_spread_price: "100", away_pitcher_id: "39428", home_pitcher_record: "15-4", home_pitcher_era: "3.60", home_name: "Washington Nationals" }, { <more> } ] |
/gamecenter/<league>/<id>
|
<league> , <id>
|
{ "last_play": { "at_bat": "Gregory Bird", "description": "...", "events": [ { "count": "0-1", "description": "Strike Looking - 89.0 MPH Cutter", "pitch_count": "1" }, { <...> } ], "inning": "Top 9", "outs": "3 Outs", "pitcher": "Álexander Colomé" }, "leaders": { "away": { "hitting": [ { "AB": 4, "AVG": 0.75, "BB": 0, "H": 3, "last_name": "Castro", "preferred_name": "Starlin" }, { <...> } ], "pitching": [ { "ERA": 23.625, "H": 8, "IP": 2.2, "SO": 3, "last_name": "Tanaka", "preferred_name": "Masahiro" }, { <...> } ], "scoring": [ { "AB": 0, "HR": 0, "R": 0, "RBI": 1, "last_name": "Carter", "preferred_name": "Chris" }, { <...> } ] }, "home": { "hitting": [ { "AB": 4, "AVG": 0.75, "BB": 0, "H": 3, "last_name": "Morrison", "preferred_name": "Logan" }, { <...> } ], "pitching": [ { "ERA": 2.571, "H": 7, "IP": 7, "SO": 5, "last_name": "Archer", "preferred_name": "Chris" }, { <...> } ], "scoring": [ { "AB": 4, "HR": 1, "R": 1, "RBI": 3, "last_name": "Longoria", "preferred_name": "Evan" }, { <...> } ] } }, "scoreboard": { "away_moneyline": "-114", "away_odds": "-114", "away_score": 3, "away_spread": "-1.5", "event_id": "752992", "home_moneyline": "104", "home_odds": "104", "home_score": 7, "home_spread": "1.5", "segments": [ { "away_points": 0, "home_points": 3, "segment": 1 }, { <...> } ], "status": "FINAL", "total": "7", "trending_total": 10 }, "statistics": { "away": { "hitting": [ { "AB": 4, "AVG": 0.25, "BB": 0, "H": 1, "HR": 0, "LOB": 1, "R": 0, "RBI": 0, "SO": 0, "id": "32362986-6bba-443c-927d-8b87153caf0d", "last_name": "Gardner", "order": 1, "position": "LF", "preferred_name": "Brett" }, { <...> } ], "pitching": [ { "BB": 2, "ER": 7, "ERA": 23.625, "H": 8, "HR": 2, "IP": 2.2, "R": 7, "SO": 3, "id": "fdfda40f-e77b-4cc2-a72c-11951460beda", "last_name": "Tanaka", "preferred_name": "Masahiro" }, { <...> } ] }, "home": { "hitting": [ { "AB": 5, "AVG": 0.2, "BB": 0, "H": 1, "HR": 0, "LOB": 2, "R": 1, "RBI": 0, "SO": 2, "id": "21e3d551-a8b8-4f6b-9d06-6832e527ddc1", "last_name": "Dickerson", "order": 1, "position": "DH", "preferred_name": "Corey" }, { <...> } ], "pitching": [ { "BB": 1, "ER": 2, "ERA": 2.571, "H": 7, "HR": 0, "IP": 7, "R": 2, "SO": 5, "id": "79b77d44-2221-4a47-b52a-784d9d894761", "last_name": "Archer", "preferred_name": "Chris" }, { <...> } ] } }, "team_statistics": { "away": { "hitting": { "AB": 36, "AVG": 0.25, "BB": 1, "H": 9, "HR": 0, "LOB": 18, "R": 3, "RBI": 3, "SO": 8 }, "pitching": { "BB": 3, "ER": 7, "ERA": 7.875, "H": 13, "HR": 2, "IP": 8, "R": 7, "SO": 10 } }, "home": { "hitting": { "AB": 36, "AVG": 0.361, "BB": 3, "H": 13, "HR": 2, "LOB": 16, "R": 7, "RBI": 6, "SO": 10 }, "pitching": { "BB": 1, "ER": 2, "ERA": 2, "H": 9, "HR": 0, "IP": 9, "R": 3, "SO": 8 } } }, "umpires": [ { "assignment": "1B", "experience": "15", "first_name": "Marvin", "full_name": "Marvin Hudson", "id": "af0d8c89-dd6d-443e-8dac-a3f9106d0a9f", "last_name": "Hudson" }, { <...> } ] } |
/play_by_play/<league>
|
<league>
|
[ { id: 1, half: "T", segment: 1, summary: "Brett Gardner flies out to left field to Mallex Smith." }, { <more> } ] |