Uno! Use Sinatra to Implement a REST API

Card game

The REpresentational State Transfer (REST) architecture provides a very convenient mechanism to shuttle data between clients and servers. Web services and protocols, like HTTP, have been using the REST architecture for many years, so it is a well-tested and mature concept.

Sinatra can be used to effectively implement the REST architecture. Sinatra’s DSL (Domain Specific Language) seems custom-tuned to the spirit of REST. In fact, Sinatra’s routing language includes the same verbs used by HTTP, including GET, POST, PUT and DELETE. Because these REST API transactions look and behave very much like HTTP, network devices will treat your REST transactions in virtually the same way.

In this article, we’ll look at a server-side implementation of REST using Sinatra. We’ll also look at a simple client-side REST application to validate the Sinatra server implementation. We’ll demonstrate using a simple card game, where a single deck of cards will be held by the server and dealt out to four clients. In the end, we’ll see how simple it is to create applications that run across clients and servers, using REST to help shuttle data back and forth.

A quick question before we get started: Why not use Ruby on Rails to move data around? Well, you certainly could use Rails (or any other web framework), but consider that Rails not only retrieves data, but it renders the view as well. This rendering process puts an extra load on the server, especially when the data is highly dynamic and cannot be cached. When it comes to online games, for example, data continuously changes. It’s therefore better to let the clients render their own view rather than forcing the server to re-render the view each time a player makes a move.

Setting Up Sinatra

Before installing Sinatra, it helps to understand how it works and what is required to make it work.

As mentioned above, Sinatra is a DSL. It essentially wraps another program, called “Rack,” and makes it more palatable to Ruby programmers. For example, Sinatra allows you to write the following “route,” which captures a client HTTP GET request for a page located at http://www.example.com/getmypage.html:

get '/getmypage.html' do
  'Hello World!'
end

With the above code running on the server under Sinatra, a browser pointed toward the URL would retrieve a simple “Hello World” response. (Note that, for simplicity, I didn’t wrap the response in HTML tags.)

Rack provides a standard interface between Ruby web frameworks (like Sinatra and Rails) and the actual web server (like WEBrick or Thin).

In this example, we’ll use Sinatra + Rack + Thin as the basis for our server.

To set up this environment, make sure you have installed the gems for Sinatra, Rack and Thin. Generally, when you install Sinatra, it will install Rack. Thin, will not automatically install, so you’ll need to do that manually.

$ gem install sinatra
$ gem install thin

Once Sinatra has been installed, you simply need to require it in your source files. It will automatically use the Thin web server.

You can run this very simple file, myapp.rb, as shown below, and test it locally. (This test file was lifted directly from the GitHub Sinatra page.)

# myapp.rb
require 'sinatra'
get '/' do
  'Hello World'
end

Run the file with this command line:

ruby myapp.rb

You can then view the results on your local web browser at http://localhost:4567

Note that the TCP socket is automatically assigned to port 4567. You can change it to the standard port 80 when you deploy the application.

Also note that, as a DSL, Sinatra allows you to implement PUT, POST and DELETE as well as the GET verb. For example, we could process a client’s POST request as follows:

post '/join' do
  # process the POST command here ...
end

The above code could be placed in the myapp.rb file, just below the previous get route. The Sinatra routine simply scans down the file, from beginning to end, looking for verbs and processing them accordingly.

See the Sinatra website for excellent documentation as well as tips for getting started with this lightweight and efficient web DSL.

The Card Game

We’ll implement the beginnings of an entertaining card game called Uno, played with all cards of the deck, including two jokers. The main purpose of this game is to illustrate the Sinatra REST API in action, so we’ll not fully develop the game logic.

We create a deck of cards as a single array of strings, where each string unambiguously describes a card. For example, “4-diamond” represents the four-of-diamonds card.

We’ll create three card-playing functions for the server:

  1. join_game — allow a player to join the game
  2. deal — shuffle and deal cards to each player
  3. get_cards — present dealt cards to a player

We’ll limit the game to four players, and the server will only run one game at a time.

To implement all this logic, we’ll develop two files:

  1. uno-server.rb — Contains the game-player logic as well as the Sinatra REST API server code
  2. uno-client.rb — Contains the client player logic

The core of the card game runs within the UnoServer class, as shown below. Note that this class is completely independent of the server, though the server code will be placed into the same file.

# Ruby Uno Server
require 'sinatra' 
require 'json' 

class UnoServer 
  attr_reader :deck, :pool, :hands, :number_of_hands 
  MAX_HANDS = 4 
  def initialize 
    @hands = Array.new 
    @number_of_hands = 0 
    @pool = Array.new 
    @deck = %w(2-diamond 3-diamond 4-diamond 5-diamond 6-diamond 7-diamond 8-diamond 9-diamond 10-diamond) 
    @deck.concat %w(2-heart 3-heart 4-heart 5-heart 6-heart 7-heart 8-heart 9-heart 10-heart) 
    @deck.concat %w(2-club 3-club 4-club 5-club 6-club 7-club 8-club 9-club 10-club) 
    @deck.concat %w(2-spade 3-spade 4-spade 5-spade 6-spade 7-spade 8-spade 9-spade 10-spade) 
    @deck.concat %w(jack-diamond jack-heart jack-club jack-spade) 
    @deck.concat %w(queen-diamond queen-heart queen-club queen-spade) 
    @deck.concat %w(king-diamond king-heart king-club king-spade) 
    @deck.concat %w(ace-diamond ace-heart ace-club ace-spade) 
    @deck.concat %w(joker joker) 
  end 

  def join_game player_name 
    return false unless @hands.size < MAX_HANDS 
    player = { 
        name: player_name, 
        cards: [] 
    } 
    @hands.push player 
    true 
  end 

  def deal 
    return false unless @hands.size > 0 
    @pool = @deck.shuffle 
    @hands.each {|player| player[:cards] = @pool.pop(5)} 
    true 
  end 

  def get_cards player_name 
    cards = 0 
    @hands.each do |player| 
      if player[:name] == player_name 
        cards = player[:cards].dup 
        break 
      end 
    end 
    cards 
  end 

end

Note the call to the Sinatra library in line 2.

The card deck is built in lines 12 through 20 as one big array. Shuffled versions of this deck will be placed into the pool array, from which cards will be dealt.

Players join the game using the join_game method in line 23. Players provide a name, and the number of players is limited to four (MAX_HANDS).

Cards are dealt using the deal method in line 33. Essentially, the deck is shuffled, and the results are placed in the pool. Each player is then provided five cards from the pool.

Finally, players can get a look at their cards using the get_cards method on line 40. They just need to identify themselves, and they will be provided a copy of their cards.

The second half of this file contains the exciting part: the implementation of the Sinatra REST API.

uno = UnoServer.new 

###### Sinatra Part ###### 

set :port, 8080 
set :environment, :production 

get '/cards' do 
  return_message = {} 
  if params.has_key?('name') 
    cards = uno.get_cards(params['name']) 
    if cards.class == Array 
      return_message[:status] == 'success' 
      return_message[:cards] = cards 
    else 
      return_message[:status] = 'sorry - it appears you are not part of the game' 
      return_message[:cards] = [] 
    end 
  end 
  return_message.to_json 
end 

post '/join' do 
  return_message = {} 
  jdata = JSON.parse(params[:data],:symbolize_names => true) 
  if jdata.has_key?(:name) && uno.join_game(jdata[:name]) 
    return_message[:status] = 'welcome' 
  else 
    return_message[:status] = 'sorry - game not accepting new players' 
  end 
  return_message.to_json 
end 

post '/deal' do 
  return_message = {} 
  if uno.deal 
    return_message[:status] = 'success' 
  else 
    return_message[:status] = 'fail' 
  end 
  return_message.to_json 
end

Note in line 53 that we instantiate a single Uno game as the uno object. The Sinatra REST API in the subsequent lines will use this object.

Lines 57 and 58 allow us to determine the environment of this implementation. We wish to use TCP port 8080, and we set up a production environment. The production environment optimizes for speed, at the cost of providing less debugging information.

The REST API addresses the three main functions set up in the card game: the addition of players (join), the dealing of cards (deal) and the viewing of the cards (cards). Each has been provided with its own URL.

Note the use of the GET and POST verbs in this implementation. When users want to see their cards, they issue a GET command using the URL http://localhost:8080/cards. This GET command includes a JSON structure that indicates the name of the player. The code simply calls the local uno object’s get_cards method to get the cards. If get_cards returns with an array, the array is posted back to the user. If an array is not returned, the user is not a player in this game, so a “sorry” message is returned.

When players want to join a game, they issue a POST command to the URL http://localhost::8080/join. If there is room in the current game, the user is allowed to join. If not, a “sorry” message is returned.

When players want to deal cards, they issue a POST command to the URL http://localhost:8080/deal. Similar to the above scenarios, the local uno object is sent a command to shuffle the deck and deal out five cards to each player. It can fail to deal if there are no players currently in the game.

In all the above cases, JSON-based values are returned to the players.

We’ll now look at the client code to see how the client side REST API implements the game environment.

# Ruby Uno Client 

require 'json' 
require 'rest-client' 

class UnoClient 
  attr_reader :name 

  def initialize name 
    @name = name 
  end 

  def join_game 
    response = RestClient.post 'http://localhost:8080/join', :data => {name: @name}.to_json, :accept => :json 
    puts JSON.parse(response,:symbolize_names => true) 
  end 

  def get_cards 
    response = RestClient.get 'http://localhost:8080/cards', {:params => {:name => @name}} 
    puts response 
  end 

  def deal 
    response = RestClient.post 'http://localhost:8080/deal', :data =>{}.to_json, :accept => :json 
    puts response 
  end 

end

As can be seen, the client-side communication is deceptively simple. We use the rest-client Ruby library to create minimally functional methods that mimic the game-playing methods on the server.

We provide a name for this player when instantiating the UnoClient class. Thereafter, we can join the game, deal cards, and get a copy of our cards.

Joining and dealing require POST commands, as shown in lines 13 and 23.

Getting the cards requires a GET command, as shown in line 18.

Note that we simply print out the response from the server, which is sent to us in JSON format. A fully developed game would obviously read the returned JSON structure and provide the corresponding results on the browser.

The listing below shows what happens when we instantiate clients. Using irb, five player objects for Bob, Carol, Ted, Alice and Ralph are created manually. Note the failure message when poor Ralph attempted to join the game. Also, notice that each player has been dealt a unique set of cards from the deck. All the action takes place on the server and only the results are sent back to the clients via the API. It appears that the Sinatra REST API is working!

> require './uno-client.rb'
> bob_uno = UnoClient.new 'bob'
> carol_uno = UnoClient.new 'carol'
> ted_uno = UnoClient.new 'ted'
> alice_uno = UnoClient.new 'alice'
> ralph_uno = UnoClient.new 'ralph'
> bob_uno.join_game
{:status=>"welcome"}
> carol_uno.join_game
{:status=>"welcome"}
> ted_uno.join_game
{:status=>"welcome"}
> alice_uno.join_game
{:status=>"welcome"}
> ralph_uno.join_game
{:status=>"sorry - game not accepting new players"}
> bob_uno.deal
{"status":"success"}
> bob_uno.get_cards
{"cards":["3-diamond","2-club","4-spade","9-diamond","8-spade"]}
> carol_uno.get_cards
{"cards":["9-club","5-spade","king-spade","6-club","7-heart"]}
> ted_uno.get_cards
{"cards":["4-heart","6-heart","2-spade","8-club","ace-heart"]}
> alice_uno.get_cards
{"cards":["4-club","7-club","3-club","king-heart","jack-club"]}

Summary

The REST architecture provides a very convenient mechanism to move data between clients and servers. This is especially important when you need to reduce the workload on your web server when dealing with highly dynamic data. As demonstrated in the example in this article, the Sinatra library can be used to implement the REST architecture in a very simple and straightforward manner.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Alex Walker

    Haven’t had much experience with Sinatra, but using Uno to demonstrate REST — great idea for an article, Dan. Loved that game as a kid.

  • http://ismaelmarin.info/ Ismael G Marin

    Hi Dan! I love this article, I just want to point some very small details that it might help new users: to use rest-client you need to install the gem: gem install rest-client, and I think that for an experimented user the deck creation will be far more interesting if we do:

    @deck = Array.new
    %w[diamond heart club spade].each do |card|
    (2..10).each {|index| @deck << "#{index}-#{card}" }

    %w[king queen ace jack].each do |comb|

    @deck << "#{comb}-#{card}"

    end

    end

    @deck.concat %w(joker joker)

    • danofsocal

      Thanks Ismael.

      Indeed, the user needs to install rest-client first. I like your way of generating all cards for the deck. Far more efficient.

      Best regards,

      Dan