Part 1 - Fetching Data. Creating a Ruby App to get notified about Epic Free Games.
Tags: ruby, telegram, games, sequel
Published: 2023-06-23 10:12:34

---
title: Part 1 - Fetching Data. Creating a Ruby App to get notified about Epic Free Games.
published: true
description: Let's build a Ruby application that gets the selection of free games in the Epic Game Store and notifies us via Telegram
tags: 
  - ruby
  - telegram
  - games
  - sequel
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-06-23 10:09 +0000
---

**Part 1** | [Part 2](https://dev.to/epergo/creating-a-ruby-app-to-get-notified-about-epic-free-games-saving-data-5e4k)

In the Epic Store there is a selection of free games that changes each week. If you want to get them you have to manually access the web page of the store each week, could we build something to get notified?

Turns out there is an endpoint to get the current free games so we can build a Ruby app to fetch it and notify us if there is anything new. Let's build it!

## Creating the project

```bash
mkdir free-game-watcher
cd free-game-watcher
bundle init
```

This will create a simple `Gemfile` pointing to RubyGems and nothing more. I like to always have installed a static analysis library, so I encourage you to add [standard](https://github.com/standardrb/standard) to your development dependencies.

```bash
bundle add standard --groups development
```

And configure your editor to run it on save and/or tell you about issues.

## Making HTTP Requests

The main selling point of the application is to get notified of free games, for that we need to query the Epic Games Store to fetch the current selection.

The endpoint that the Store uses is public so we will use it as well. It is a GET request without query params and doesn't require any authentication. Let's have a look.

```bash
$ curl https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions | jq '.data.Catalog.searchStore.elements[1] | {title: .title, promotions: .promotio
ns.promotionalOffers, upcoming: .promotions.upcomingPromotionalOffers, images: .keyImages[3]}'

{
  "title": "Breathedge",
  "promotions": [
    {
      "promotionalOffers": [
        {
          "startDate": "2023-04-27T15:00:00.000Z",
          "endDate": "2023-05-04T15:00:00.000Z",
          "discountSetting": {
            "discountType": "PERCENTAGE",
            "discountPercentage": 0
          }
        }
      ]
    }
  ],
  "upcoming": [],
  "images": {
    "type": "Thumbnail",
    "url": "https://cdn1.epicgames.com/08ae29e4f70a4b62aa055e383381aa82/offer/EGS_Breathedge_RedRuinsSoftworks_S2-1200x1600-c0559585221ea11c9d48273c3a79b1ba.jpg"
  }
}
```

_I've used [jq](https://stedolan.github.io/jq/) to show you the information that we will care about (only for one game), feel free to inspect the full JSON response._

We have to implement this query in Ruby, for that we will use [Faraday](https://github.com/lostisland/faraday). There are many different http libraries but I've chosen Faraday because, besides being a great tool, the gem that we will use later to connect to telegram has Faraday as a dependency so installing the same library saves us from having different libraries for the same purpose.

```bash
bundle add faraday
```

Now we are going to create an _EpicStoreAdapter_ class that will contain the method to query the endpoint. Where should we place it?

The application logic should go into its own `app` folder. If later we want to add a simple web page we can create a standalone `web` directory.

So, create the new `app` folder and place a new file called `epic_store_adapter.rb` inside it with our new class that needs to receive the base URL of the endpoint to configure the Faraday client.

```diff
# app/epic_store_adapter.rb

+ class EpicStoreAdapter
+  def initialize(base_url)
+    @connection = Faraday.new(
+      url: base_url,
+      headers: {"Content-Type": "application/json"},
+      request: {timeout: 15}
+    )
+  end
+ end
```

And a method to query free games:

```diff
# app/epic_store_adapter.rb

class EpicStoreAdapter
  def initialize(base_url)
    @connection = Faraday.new(
      url: base_url,
      headers: {"Content-Type": "application/json"},
      request: {timeout: 15}
    )
  end
+
+  def get_free_games
+    response = @connection.get("/freeGamesPromotions")
+    JSON.parse(response.body)
+  end
end
```

Let's check that this class does what we want. To create basic for it we'll use [rspec](https://github.com/rspec/rspec-metagem).

```bash
# Install RSpec
$ bundle add rspec --group test
Fetching gem metadata from https://rubygems.org/...
...

# Init RSpec configuration files
$ bundle exec rspec --init
  create   .rspec
  create   spec/spec_helper.rb

# Check that it works
$ bundle exec rspec
No examples found.


Finished in 0.00051 seconds (files took 0.06503 seconds to load)
0 examples, 0 failures
```

Before creating our spec we are going to add [VCR](https://github.com/vcr/vcr) to store API responses so everytime we run tests we avoid making new real requests.

```bash
# Install VCR
$ bundle add vcr --group test
```

VCR needs to be configured first so in our `spec_helper.rb` file (which was created previously with `rspec --init`) set the cassette library directory and the HTTP library:

```diff
# spec/spec_helper.rb

+ require "vcr"
+
+ VCR.configure do |config|
+   config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
+   config.hook_into(:faraday)
+ end

RSpec.configure do |config|
  # ...
end
```

And finally we can write a simple test to check that we have correctly fetched games.

```ruby
# spec/app/epic_store_adapter_spec.rb

require_relative "../../app/epic_store_adapter"

RSpec.describe EpicStoreAdapter do
  let(:adapter) { EpicStoreAdapter.new("https://store-site-backend-static.ak.epicgames.com") }

  it "fetches current selection of free games" do
    VCR.use_cassette("free_games") do
      response = adapter.get_free_games
      games = response["data"]["Catalog"]["searchStore"]["elements"]
      expect(games.first["title"]).to eq("Borderlands 3 Season Pass")
    end
  end
end
```

_We will clean up that `require_relative` later_

You should find the saved response from the API in the folder we specified before.

Now, instead of returning the parsed JSON from the response we could create a couple of data structures to store the information we care about. For that we can use the new [Data](https://docs.ruby-lang.org/en/3.2/Data.html) core class.

```diff
class EpicStoreAdapter
+ GameData = Data.define(
+   :title,
+   :product_slug,
+   :url_slug,
+   :image_url,
+   :promotions
+ )
+ PromotionData = Data.define(
+   :start_date,
+   :end_date,
+   :discount_type,
+   :discount_percentage
+ )

def initialize(base_url)
end
...
```

The `Data` class is similar to `Struct` and it shares most of their implementation. We'll store only a selection of attributes from games and their promotions.

We are going to create a new private method to map the JSON response to an array of Games.

```diff
+ private
+
+ def build_games(games_data)
+   games_data.map do |game|
+     thumbnail_image = game["keyImages"].find { |image| image["type"] == "Thumbnail" }
+
+     GameData.new(
+       title: game["title"],
+       product_slug: game["productSlug"],
+       url_slug: game["urlSlug"],
+       image_url: thumbnail_image&.fetch("url", nil)
+     )
+   end
+ end
```

As you can see is pretty straight-forward, the only attribute a bit more complex to fetch is the thumbnail image that we have to find in the array of images.

There is still one attribute missing, `promotions`. Promotions have their own structure so we are going to create another to map them into `Data` structures.

```diff
+ def build_promotions(promotional_offers)
+   promotions = []
+   promotional_offers.each do |offer|
+     offer["promotionalOffers"].each do |promotional_offer|
+       promotions << PromotionData.new(
+         start_date: Time.parse(promotional_offer["startDate"]),
+         end_date: Time.parse(promotional_offer["endDate"]),
+         discount_type: promotional_offer["discountSetting"]["discountType"],
+         discount_percentage: promotional_offer["discountSetting"]["discountPercentage"]
+       )
+     end
+   end

+   promotions
+ end
```

Here we traverse the JSON to get the data we want (check the full JSON you have saved for details)

Now we can update our `build_games` method to add promotions. The JSON contains both current promotions and future ones, we can save both.

```diff
   def build_games(games_data)
    games_data.map do |game|
+     active_promotions = build_promotions(game.dig("promotions", "promotionalOffers") || [])
+     upcoming_promotions = build_promotions(game.dig("promotions", "upcomingPromotionalOffers") || [])
      thumbnail_image = game["keyImages"].find { |image| image["type"] == "Thumbnail" }

      GameData.new(
        title: game["title"],
        product_slug: game["productSlug"],
        url_slug: game["urlSlug"],
        image_url: thumbnail_image&.fetch("url", nil),
+       promotions: active_promotions + upcoming_promotions
      )
    end
  end
```

Finally we have to update our `get_free_games` method to, instead of returning the parsed JSON, create the needed structures.

```diff
  def get_free_games
    response = @connection.get("/freeGamesPromotions")
-     JSON.parse(response.body)
+     parsed_response = JSON.parse(response.body)
+     games_data = parsed_response["data"]["Catalog"]["searchStore"]["elements"]
+     build_games(games_data)
  end
```

But now our spec is failing, we have to update it accordingly:

```diff
  # spec/app/epic_store_adapter_spec.rb

  require_relative "../../app/epic_store_adapter"

  RSpec.describe EpicStoreAdapter do
    let(:adapter) { EpicStoreAdapter.new("https://store-site-backend-static.ak.epicgames.com") }

    it "fetches current selection of free games" do
      VCR.use_cassette("free_games") do
-       response = adapter.get_free_games
-       games = response["data"]["Catalog"]["searchStore"]["elements"]
-       expect(games.first["title"]).to eq("Borderlands 3 Season Pass")
+       games = adapter.get_free_games
+       expect(games.first.title).to eq("Borderlands 3 Season Pass")
      end
    end
  end
```

And that's it for now, we have implemented the request to the Epic Store and mapped the data into our own data structures.

In the following part we will save the data into a database.