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:

  1. Build the endpoint url
  2. Send the request
  3. 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>
  }  
]