In this article, I'm going to be digging into Ruby, a great programming language. I'll cover the basics of syntax, Rubygems, and then dive into a real example, building a website to search for beer using the BreweryDB API. You've undoubtedly heard a bit about Ruby, but maybe you haven't played around with it yet.
What's So Good About Ruby?
Ruby is fun.
I've been through a fair number of languages in my programming career. My early days were with ColdFusion and ASP, then I moved onto PHP and finally settled on C# for many years. For the past several years, my languages of choice have been Objective-C and Ruby. Of all the languages I've used, Ruby is the one that gives me the most joy to write. Many parts of the language exist solely to make you, the programmer, happier and more productive (cue Radiohead music).
Ruby's popularity skyrocketed with the popularity of Rails, a Web framework built using Ruby. Although most people started with Ruby along with Rails in 2004, Ruby has been around since 1995. There's a saying the Ruby community: I came for the Rails, and stayed for the Ruby.
This is certainly true in my experience. Today. I use Ruby to power Web applications, for background job processing, and for general scripting tasks. It deploys my software, runs my build (even for iOS projects), and generates my blog. Ruby is everywhere.
“Okay, okay. I'm already sold on Ruby,” I hear you say. Great, now let's get it installed on your system.
Installing Ruby
The version of Ruby I'll cover in this article is 2.1.1, which is the most recently released version at the time of writing. If you're on a Mac, you already have Ruby installed, however I recommend using a Ruby version manager such as rvm, rbenv, or chruby. Doing so allows you to freely try out multiple Ruby versions, but also the articles and tutorials you'll find online will almost certainly use one of these tools.
I use rbenv, so that's what I'll cover here. If you're on Windows, you might want to look into Pik, which offers similar functionality.
The easiest way to get started on a Mac is with Homebrew. If you don't have Homebrew installed on your Mac, install that first by going to https://brew.sh/ and following the instructions. Once Homebrew is installed, launch Terminal.app
and type the following:
$ brew install rbenv ruby-build
This installs rbenv and ruby-build onto your system. There are some post-install instructions for rbenv that you'll need to pay attention to as well, so that it's loaded in your shell properly.
Next, you need to install the current version of Ruby onto your system, which you can do by typing:
$ rbenv install 2.1.1
This will take a few minutes, so now is a good time to make a cup of coffee. When you're finished, you'll have a new Ruby installed. To make Ruby refer to this new version, type:
$ rbenv global 2.1.1
You can verify that it worked by closing and opening up a new Terminal and typing:
$ ruby -v
2.1.1p76 ruby 2.1.1p76 (2014-02-24 revision 45161) [x86_64-darwin13.0]
If you see a different version of Ruby than the one you just installed, check the instructions for setting up rbenv to make sure it's loaded properly.
Basics of Ruby
Ruby is both an object-oriented language and a scripting language. It comes with a quick interactive REPL (read-eval-print-loop) called IRB, which stands for Interactive Ruby. You can use IRB to familiarize yourself with Ruby. Let's start it up now. In Terminal, type:
$ irb
You'll be greeted with a prompt. Anything you type here will be evaluated. Let's start with a number:
irb(main):001:0> 5
=>
I typed 5, and it evaluated to 5. In Ruby, everything is an object, so you can inspect its class:
irb(main):002:0> 5.class
=> Fixnum
You can see that 5 is an instance of the Fixnum
class. You can also work with strings:
irb(main):003:0> "Hello there"
=> "Hello there"
irb(main):005:0> "Hello there".reverse
=> "ereht olleH"
irb(main):004:0> "Hello there".length
=> 11
Indexing a string is super powerful as well. You can grab a substring just like this:
irb(main):006:0> string = "Hello there"
"there"
=> "Hello there"
irb(main):006:0> string[0..4]
=> "Hello"
irb(main):006:0> string[5..-1]
Here, I declared a new variable string
, which contained the contents "Hello there"
. The String
class defines the []
method that allows you to index the string (methods don't have to be only alphanumeric characters). Passing [0..4]
literally passes a range of indices. In the third example, you used the -1
to indicate the end of the string. You can also pass -2, -3
, etc., which start from the other end of the string. You can even index by regular expressions:
irb(main)> "This article is about Ruby"[/ruby/i]
=> "Ruby"
irb(main)> This article is about Ruby"[/python/i]
=> nil
Note that it will just return the first instance of a match, if one is found. If a match isn't found, it returns nil. There are many other ways to perform regular expression matches, but this one is sometimes just what you need.
In Ruby, conditionals work with truthy and falsy values. This means false
and nil
cause a conditional test to fail, and any other object causes it to pass. With the example above, you could write:
if article_text[/ruby/i]
puts "Article mentions Ruby"
end
Another thing to note is that in Ruby, strings can be represented in multiple ways. The most common ways are with single quotes (') and double quotes ("). Double quotes allow for string interpolation, like this:
"The current date is #{Time.now}"
You can execute any Ruby code inside of the #{}
marks and the result is inserted into the string. Interpolation doesn't work with single-quoted strings. As a result, a common practice is to prefer single-quoted strings unless you're doing string interpolation. You'll find that Rubyists don't always follow this, sometimes using single and double quotes interchangeably even within the same file. I prefer consistency, so if you catch me doing that, tell me!
Symbols
Symbols are pervasive in Ruby applications. You'll use them all the time as identifiers, keys, and enum-esque values. You can define a symbol like this:
:foo
Symbols are descriptive, but they are also statically declared. A symbol is only created once, and any time it's used, it refers to the same memory. This makes it useful for enum-like values, such as statuses:
status = :payment_failed
They are also great as keys for a Hash, a collection similar to a Dictionary in .NET:
name = attributes[:name]
You can think of symbols as flexible with strings, with downsides like mutability and performance taken care of. While they are extremely flexible, it's still just as easy to mistype a symbol and introduce bugs. Symbols don't offer any safety in this regard.
Classes
You can define your own classes in Ruby like this:
class Player
def move(x, y)
# ...
end
end
This declares a new class Player
that has an instance method move
. The move
method takes two parameters, x
and y
. To use this class, you first must create an instance of it:
player = Player.new
player.move(10, 2)
In Ruby, parentheses are optional when calling methods, as long as there is no ambiguity.
Ruby classes act much like classes in other languages. You can maintain private state, implement public and private methods to define behavior, and inherit classes to share behavior.
Unlike Java or C#, Ruby offers another method for sharing functionality, through Modules.
Modules
Modules can be thought of as a group of behavior that you can invoke directly, or include in other classes. They offer a way to share behavior across many classes where inheritance isn't an option.
You declare a module much like declaring a class:
module Combatant
def attack(other)
puts "#{self.name} is attacking #{other.name}"
other.life -= 10
end
end
You can then include this module in another class and have the methods available to the class. If you're following along in IRB, feel free to “redeclare” the Player
class like this:
class Player
include Combatant
attr_reader :name
attr_accessor :life
def initialize(name)
@name = name
@life = 100
end
end
What you're doing here is re-opening the class, which is perfectly fine to do in Ruby. Classes are not read-only after creation. You can always add methods to a class by re-opening it. There are a few other concepts added to the class in order to complete the example. First, I gave the class a reader
method for the name
variable. You might think of these as properties, but in Ruby they're called attributes. Name
is read-only, but you also declare a read-write attribute called life
. You also create a constructor to take in a name as an argument and initialize life
to 100
.
Now that you've done this, you can create new player
classes and they'll inherit this new behavior, including the behavior from the Combatant
module. Notice that I called attack
on player
, and it appeared to be just like any other method. This is called a mixin, and it can be a good alternative to inheritance when you need to share behavior.
player = Player.new("Mr. Pink")
player.move(25, 1)
enemy = Player.new("Mr. Black")
player.attack(enemy)
That's enough IRB for now. Just type exit
to get back to your shell.
Gems
One of the most powerful things about Ruby is the thriving community of open source contributions called gems. Gems are libraries that are packaged up for you to use in your Ruby projects. In this article, I'll be using a few gems, and you'll use Bundler to manage them. Bundler itself is a gem, so let's start by installing that.
$ gem install bundler
With rbenv, you have to execute a tiny command when you install gems with binaries to make them available in your path:
$ rbenv rehash
This is a minor annoyance, but allows rbenv to be ignorant of changes to your shell unless you tell it to expect a change. Now you'll have the bundle
command available to use:
$ bundle -v
Bundler version 1.5.3
Bundler is the de-facto way of managing dependencies in Ruby applications these days, and as such, is one of the few gems that you'll install directly. The rest of the gems you depend on will go in a Gemfile
and you'll use Bundler to install them. Bundler figures out the appropriate dependencies to install and make sure that their versions won't conflict with things you've declared in the Gemfile
.
Don't worry if this sounds complex to you. For these purposes, bundler will be very easy to work with, since you'll only be using a few gems.
Now that you're familiar with the basics, let's jump head-first into a real example application. What follows is going to be a whirlwind tour of lots of concepts in Ruby development. The example app is as simple as possible, while still exhibiting aspects of Ruby that are interesting. If you don't understand a specific topic, don't worry. Feel free to open an issue on the Github repository for questions and I'll do my best to answer them quickly.
The Example Application
You're going to build a beer-searching Web application, backed by the BreweryDB API. To follow along, you'll need your own API Key, so you can sign up with them if you want to run the example locally. To sign up, head over to https://www.brewerydb.com/ and register for an API Key.
A completed example is available online at https://beer-lookup.herokuapp.com/. The code for this example is available at https://github.com/subdigital/beer-lookup.
You'll also need a good text editor. If you don't already have one in mind, TextMate 2.0 is free and excellent. You can find it at https://github.com/textmate/textmate. Sublime Text is also a very popular choice and is available cross-platform. You can find Sublime Text at https://www.sublimetext.com/. I'll be using Vim.
The first step is to declare the dependencies you're going to be using. For that, you need a Gemfile. Open your Terminal and type the following:
$ mkdir beersearch
$ cd beersearch
$ bundle init
This creates a Gemfile
in the folder you just created. You're going to use Sinatra (http://sinatrarb.com/) to serve up the website. Sinatra is an incredibly light-weight Web application framework.
Sinatra is a gem, so you'll add this to the Gemfile
. You'll also want an add-on to Sinatra called sinatra-contrib, which will offer
class reloading functionality during development. Finally, you'll need an easy way to make network requests. The simplest gem I know for this is httparty
.
Open Gemfile
in your editor and replace it with the contents of Listing 1.
Listing 1: Gemfile
source 'https://rubygems.org'
gem 'sinatra'
gem 'sinatra-contrib'
gem 'httparty'
Here, you have declared the three gems you depend on. You can install them all (and any of their subsequent dependencies) by running:
$ bundle install
Installing backports (3.6.0)
Installing json (1.8.1)
Installing multi_xml (0.5.5)
Installing httparty (0.13.0)
Installing multi_json (1.9.0)
Installing rack (1.5.2)
Installing rack-protection (1.5.2)
Installing rack-test (0.6.2)
Installing tilt (1.4.1)
Installing sinatra (1.4.4)
Installing sinatra-contrib (1.4.2)
Using bundler (1.5.3)
Your bundle is complete!
Use `bundle show [gemname]` to see where a
bundled gem is installed.
Now that the gems are installed, you're ready to create a simple Sinatra application.
First Steps with Sinatra
You're ready to get started. Let's create a file called server.rb
that will house your Sinatra application. Let's start with something really basic, as shown in Listing 2.
Listing 2: A Simple Sinatra application (server.rb)
require 'rubygems'
require 'bundler/setup'
require 'sinatra'
get '/' do
'Hi, there!'
end
Save the file and then run the following in Terminal:
$ ruby server.rb
This runs the application, which will host a mini-Web server for you to interact with. Once it has loaded, open up http://localhost:4567
in your browser. You should see your message!
Let's break this down and examine each part. First, you require rubygems and bundler/setup, which allows you to require
any of the gems you've specified in your Gemfile. Then you require
sinatra, which loads the Sinatra library. When loading Sinatra in this way, the file itself becomes a Sinatra application. The get ‘/’ line is actually a method call, but it is done in such a way to make it more like a Domain Specific Language (DSL) for describing routes and behaviors for a Web application.
Unless you explicitly render a template, Sinatra treats the return value as the response body, hence your string is rendered directly. That's not very useful for websites though, so you'll learn how to render a full-fledged HTML template next.
Rendering a Template
Rendering plain-text isn't very exciting. What I'd like to do is render a template and pull in stylesheets and JavaScripts that the application depends on to look nice and function properly.
To do this you'll do a little bit of organization. You'll need two directories to store the public assets and templates. In Terminal, type the following:
$ mkdir views
$ mkdir public
I'll be using jQuery in this application, so you can download it and place it in the public
folder. An easy way to do that is with wget:
$ wget http://code.jquery.com/jquery-1.11.0.min
.js -O public/jquery.min.js
If you don't have wget
on your system, it's easily installed with homebrew, just type brew install wget. If you don't want to install wget, manually save the file at the URL above.
Next, you need to add the template. You're going to use a single template for the entire example to keep things easy to understand. In your editor, open a new file called index.erb
. The contents are shown in Listing 3.
Listing 3: Creating the HTML Template (index.erb)
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Beer Search</title>
</head>
<body>
<header>
<h1>Beer Lookup</h1>
</header>
<div id="content">
<form id="beer-search" method="get" accept-charset="utf-8">
<input type="text" class="search"
name="q" placeholder="Search for a beer.>
</form>
<div id="results"> </div>
</div>
<footer>
Powered by <a href="http://brewerydb.com" target="_blank">BreweryDB</a>.
</footer>
</body>
</html>
Save the file as index.erb
in the views
folder. The erb
extension tells Sinatra that you have an HTML template with Ruby embedded inside. ERB stands for Embedded Ruby, and ships in the Ruby Standard Library, which means that you can use it anywhere.
In the template above, we haven't included any dynamic behavior, so it renders as a plain HTML page.
In order to get the Sinatra app to render this page, open up server.rb
and add/change the contents to match Listing 4.
Listing 4: Telling Sinatra to Render the Template (server.rb)
require 'rubygems'
require 'bundler/setup'
require 'sinatra'
require 'sinatra/reloader' # quick reloading during development
get '/' do
erb :index
end
With that change, by default, Sinatra looks for a file called index.erb
in the views
folder. You can change all this if you want, but let's stick with the defaults for now.
Note that you also required sinatra/reloader
at the top of the file. This handy class comes from sinatra-contrib
and reloads the class with every request. Without it, you would have to stop and restart the Ruby process to get it to pick up changes.
To get the changes to appear, you'll have to restart the server (for the last time). Go back to Terminal and press Ctr +C. Then press the Up arrow to recall the last command and hit Enter to run it again.
Refreshing your browser, you should see the new template, shown in Figure 2.
The page doesn't look great, but you'll use CSS to style it in a bit. First, let's make it functional. The form also doesn't have a submit button, so you'll handle that with jQuery by capturing the enter key. To tackle both of these, you'll need to add some static assets to the template.
Adding JavaScript and CSS Assets
You first need to tell Sinatra where the “root” of the site is. Sinatra serves up static files like .js and .css files from this folder. Open up server.rb
and add this near the top:
set :public_folder,
File.join(File.dirname(__FILE__), 'public')
This is setting the public folder relative to the current path. It's a good practice to use File.join
instead of hard coding path separators like “/” in your paths. Doing so means your apps will likely have trouble working on Windows.
In Ruby, when you're building a path, you often want it relative to the current file. In the code snippet above, you use the special constant __FILE__
to refer to the path of the current file. You then get only the containing directory with File.dirname
. You can use File.join
to add the path components together. It might look a bit odd at first, but you'll find this pattern very handy when writing Ruby applications.
You've already created the public folder and placed jQuery there. You just need to reference it in the template. Open up the index.erb
file and add the following, just underneath the <title>
tag:
<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="app.js"></script>
<link type="text/css" rel="stylesheet" href="styles.css">
Here, there are three static assets referenced: jQuery
, app.js
, and styles.css
. The latter two don't exist yet, but you're about to create them.
Let's start with the CSS. Create a new file called styles.css
in the public
folder. Enter the stylesheet definition shown in Listing 5. These styles should make the application look more presentable.
Listing 5: Initial styling of the app (public/styles.css)
body {
background-color: #fff;
margin: 0;
padding: 0;
font-size: 16px;
font-family: 'Avenir Next', 'Helvetica', sans-serif;
color: #444;
}
a:link, a:visited {
color: #A33;
}
a:hover, a:active {
color: #E44;
}
header {
font-weight: bold;
text-align: center;
}
header h1 {
margin: 0;
padding: 18px;
}
#content {
padding: 20px;
padding-top: 0;
width: 600px;
margin: auto;
text-align: center;
}
form#beer-search {
margin-bottom: 2em;
}
input.search {
padding: 8px;
font-size: 1.8em;
border-radius: 30px;
-webkit-border-radius: 30px;
border: solid 1px #777;
text-indent: 18px;
background-color: #fff;
}
input.search:focus {
border: solid 1px #111;
background-color: #efefef;
outline: none;
}
footer {
color: #888;
font-size: 0.9em;
text-align: center;
margin-top: 5em;
margin-bottom: 1em;
}
Now let's add the app's JavaScript file. Create a new file called app.js
in the public
folder. Enter the JavaScript shown in Listing 6.
Listing 6: The app's JavaScript (public/app.js)
$(function() {
$("input.search").keydown(function(event) {
if (event.keyCode == 13) { /* enter */
event.preventDefault();
$(this).parent("form").submit();
}
});
});
The app.js
file waits until the DOM has been loaded, then adds a keydown
handler for the input field. If the key pressed is ENTER, it intercepts the key stroke and submits the surrounding form. Any other keydown event is passed on to the text field as normal.
Now that you have the static assets created and they are referenced in the template, you're ready to refresh the page. Try typing in a search term and pressing enter. The page should reload with your search term in the query string. Figure 3 shows the result.
If it doesn't work, check to see that you don't have any JavaScript errors on the page, which would happen if jQuery wasn't added to the page correctly.
Next, you need to add some server-side code to handle the search term you just typed in.
Handling the Form Submission
Let's start by handling the form submission. Notice that in Figure 3, the form action was the same index page, but with a q
parameter added to the query string. You'll also want to display this value in text box so the user knows what the search results refer to.
In Sinatra, you'll get a reference to any form parameters (query string or form post body values) in an object called params
. Inside of server.rb
, you need to set an instance variable that includes this parameter. When you set an instance variable on the server side, the same variable is available in the template.
In Ruby, instance variables are prefixed with an @
symbol. They refer to variables that are local to an instance of a class. While you're not using classes directly here, they are being used behind the scenes to facilitate this interaction.
In Ruby, instance variables are prefixed with an @ symbol.
get '/' do end
@query = params[:q]
erb :index
end
Here, I've taken the q
parameter out of the query string using the params
object. This is a Ruby Hash (similar to a Dictionary in .NET) that includes all GET and POST parameters. It will safely be nil
if the user hasn't searched yet.
In the template, you can modify the text box to display this value, but before you do, you need to consider that the user could have typed something malicious. To avoid potential attacks like XSS (Cross-Site Scripting), you need to remember to escape user input before displaying it. Open up index.erb
and replace the <input>
field definition with the following:
<input type="text" class="search" name="q"
value="<%= Rack::Utils.escape_html @query %>"
placeholder="Search for a beer.">
This is your first taste of dynamic Ruby code embedded inside of HTML. You can see that you only need to add <%= %>
tags to evaluate Ruby in the template. Another form you'll come across is <% %>
(without the equal sign). The equal sign version is when you want the result of the Ruby expression to be entered into the resulting HTML. The plain <% %>
code blocks are used for control structures like If
statements and loops
.
You now have a functioning front-end, but you haven't done an actual search yet. Let's tackle that next.
Searching Against BreweryDB's API
The next step is to take that search term and hit the BreweryDB API to do a search. API documentation for the BreweryDB API can be found at https://www.brewerydb.com/developers/doc. If you're following along, now is a good time to secure a BreweryDB API Key. You're going to keep this secret out of the repository, so you'll lean on environment variables to provide the value at runtime.
You can set an environment variable for a shell session like this:
$ export BREWERYDB_API_KEY=yourkeyhere
However, it won't last after you close Terminal. Another option is to provide it to a one time running process like this:
$ BREWERYDB_API_KEY=yourkeyhere ruby server.rb
Setting it like this keeps the environment variable around only for the life of the process you're spawning.
For more advanced support for multiple environment variables read from a local
file, I highly recommend taking a look at the dotenv
gem, which is online at https://github.com/bkeepers/dotenv.
What If I'm on Windows? These instructions on environment variables are intended for UNIX environments. On Windows, there are different instructions for setting environment variables from the command line. See https://www.thoughtco.com/using-environment-variables-2908194 for more information.
Once you've exported your environment variable, you can access the environment in the Sinatra app like this:
key = ENV['BREWERYDB_API_KEY']
Now that you're prepped on how to provide a secret API key to the application without checking it into the source repository, you can start writing the code to do the search.
You're going to place the BreweryDB code into its own class to keep things separate. First, create a directory called app
that will contain the app's classes. For a larger app, you might create further structure like app/models
or app/services
but for this tiny example, you can stick with just app
.
Create a new file called brewerydb.rb
in the app
folder and copy the contents of Listing 7.
Listing 7: A class to search BreweryDB (app/brewerydb.rb)
class Brewerydb
include HTTParty
base_uri "http://api.brewerydb.com/v2"
end
You're going to leverage httparty
to make simple HTTP requests to the BreweryDB API. This is accomplished by including the HTTParty
module in the class. This gives the class access to methods like get
, put
, post
and the like.
All of your requests will be based on the same URL. You can see how to inform HTTParty of this by using the base_uri
class method.
You can't use this class at all without an API key, so let's create a constructor that takes this key as input.
def initialize(key)
raise "You need to provide an API Key"
if key.nil?
@key = key
end
In this method, you'll take a key parameter, ensure that it is non-nil, and set an instance variable to hold onto it for later. Notice how you can suffix statements with conditionals by placing the If statement at the end of the line. I often do this to keep things to one line when it doesn't hurt readability.
Notice how I'm calling a method .nil?
and it supposedly returns true
if the object is nil
. In many languages, this causes a Null
reference/pointer error, but in Ruby, nil
is actually an instance of NilClass
. Remember how I said everything was an object? NilClass
implements the nil?
method and returns true
. It's easy and elegant.
Next, you need to implement the search
method. This is a simple GET request against the /search
endpoint on the API. For the query string parameters, you need to specify the values as shown in Table 1.
To provide this list of parameters to HTTParty, you'll pass along a Hash. Hashes can be created with curly braces, with keys and values separated by colons, like this:
{
color: "red",
flavor: "cherry
}
This syntax was introduced in Ruby 1.9. It's equivalent to this syntax (which also works):
{
:color => "red",
:flavor => "cherry"
}
The main thing to note here is that the keys are symbols and the values are strings. Unless you're targeting Ruby 1.8 (which you probably aren't), try to stick with the 1.9 syntax.
Next, you need to use this class in the Sinatra application. Open server.rb
and add a require statement near the top:
require_relative './app/brewerydb'
The require_relative
is relative to the current file. If you're on Windows, you're probably already yelling at me because I said not to hard-code paths. In this case, however, it works, presumably because require_relative
is smart enough to change them, depending on platform. Next, you want to add a method that you can use to get a reference to the API class:
def api
@api ||= Brewerydb.new(ENV['BREWERYDB_API_KEY'])
end
This allows you to get a reference to the API class and keep it around for future requests. The ||= operator is called “nil-assigning.” It's equivalent to:
def api
if @api.nil?
@api = Brewerydb.new(ENV['BREWERYDB_API_KEY'])
end
@api
end
I'll take the ||= version any day of the week. It is more elegant and expressive, and saves you trouble. Next, you need to do the search, and evaluate whether or not it was successful:
def do_search(q)
response = api.search(q)
if response.code == 200
logger.info "Response: #{response.body}"
else
logger.error = "ERROR: #{response.body}"
end
end
Here, you check the response code, looking for an HTTP 200 that indicates success. If the request was successful, log out of the response body using logger.info
, which is a built-in logger. You'll see this output in the Terminal. In the event of an error, log out the reason using logger.error
.
Now let's edit the endpoint definition to do a search if the q
param is present:
def '/' do
@query = params[:q]
do_search @query if @query
erb :index
end
You need to restart the server one last time, to provide the process with a valid API key. Press Ctr + C
in Terminal to stop the server and then run:
$ BREWERYDB_API_KEY=<yourkeyhere> ruby server.r4b
With this in place, you can try your first search.
Handling the BreweryDB Response
If you do a new search for “IPA,” you should get a ton of results. You're not rendering any UI for the search results yet, so you have to look in the console for the response. Figure 5 shows a successful response in the console.
Then, you can take a look at the console output to see what the JSON response looks like. Searching for “Endeavour” yields a single result, which makes the JSON easier to grok. The JSON response for this is shown in Listing 8. It has been truncated to make it easier to see the structure.
Listing 8: The JSON Response for the ‘Endeavour’ search
{
"numberOfPages": 1,
"currentPage": 1,
"totalResults": 1,
"data": [
{
"id": "9Jysqx",
"name": "Endeavour",
"description": "Endeavour is a double IPA. The color is
deep amber with a light, creamy head...",
"abv": "8.9",
"ibu": "76",
"glasswareId": 6,
"availableId": 1,
"styleId": 31,
"isOrganic": "N",
"status": "verified",
"createDate": "2012-11-04 22:32:25",
"updateDate": "2013-0 8-13 19:29:30",
"labels": {
"icon": "https://s3.amazonaws.com/brewerydbapi/
beer/9Jysqx/upload_aViX39-icon.png",
"medium": "https://s3.amazonaws.com/
brewerydbapi/beer/9Jysqx/upload_aViX39-medium.png",
"large": "https://s3.amazonaws.com/brewerydbapi/
beer/9Jysqx/upload_aViX39-large.png"
},
...
}
]
}
There's a lot of content here, but you're only interested in a few attributes. In Listing 8, I've omitted more than a dozen additional attributes. These might be valuable for other applications and the data is certainly interesting but you don't need it for this example. Also note that this response is paged, and you can find the current page under the data key.
To parse this list into a list of beers, you can create a new class called Beer
. Create a new file at app/beer.rb
and enter the contents of Listing 9.
Listing 9: A class to hold parsed beer data (app/beer.rb)
class Beer
attr_accessor :name, :image_url, :description
end
The attr_accessor
line tells Ruby to create read-only getter/setter
methods for the following list of instance variables. You describe the variable names using symbols. If you want read-only-getters, you can opt for attr_reader
instead.
Next, you want to build a class method that can take the JSON response representing a single Beer and return a parsed Beer
instance.
def self.from_json(json)
Beer.new.tap do |b|
b.name = json["name"]
b.description = json["description"]
b.image_url = json["labels"]["medium"] if json["labels"]
end
end
Here, you are creating a new beer
instance, and you're leveraging the tap
method to keep this entire method to one line. This allows you to avoid having an explicit return
statement and simplifies the code a little bit. You might compare this to the With
keyword in VB.NET, which offers similar functionality.
Now you just need to use this new method in your server. Open up server.rb
once again and add the following require
statement just below the last one:
require_relative './app/beer'
Still in server.rb
, add the following method near the bottom of the file.
def parse_beers(response)
return [] unless response["data"] # (1)
response["data"].map {|beer_json| # (2)
Beer.from_json(beer_json)
}
end
- If the search returned no results, the
data
key won't exist, so you bail early, returning an empty array in this case. - The next line takes the items from the array and maps them to different values. You pass a block to the
map
method that will be called for each item in the data array. Call this itembeer_json
and then use theBeer
class to convert that into a real object.
Writing a class and tucking the parsing logic inside of it allows the application to be slightly decoupled from the API structure (which could change). You definitely don't want to hard code references to an API response structure in your views.
You're ready to tie it together. Change the do_search
method to contain the following:
def do_search(q)
response = api.search(q)
if response.code == 200
@beers = parse_beers(response)
else
@error = response.body
end
end
If the response was successful, the @beers
instance variable will contain the list of beers, and will be available to the view. If the request did not succeed, you're giving the view the @error
instance variable, which you can use to display the result to the user.
Displaying the Results in the View
Now you need to modify the view to render the beers returned by the server. If there are no results, the @beers
array is empty. If no search has been made, @beers
will be nil
. Inside of the results div
you created in Listing 3, in views/index.erb
add the following code:
<% if @beers && @beers.empty? %>
Your search returned no results. :(
<% end %>
<% @beers.each do |beer| %>
<% if beer.image_url %>
<img src="<%= beer.image_url %>">
<% else %>
<span class="noimg"></span>
<% end %>
<span class="name"><%= beer.name %></span>
<span class="description"><%= beer.description %></span>
<% end if @beers %>
<% if @error %>
<%= @error %>
<% end %>
If @beers
exists and doesn't have any entries, you can display a helpful message. You can also handle the case where you've been handed an error (stored in the @error
variable) and display it to the user.
The second code block loops over each beer, yields it to the provided block as a local beer
variable, and outputs the details in an HTML structure that is easy to style.
Styling the Results
You're nearly there. Before trying it out, let's add some styles to make the results look presentable. Open up public/styles.css
and add definitions shown in Listing 10 to the bottom of the file. Remember, the code is posted online if you don't want to type all that. You can find it at https://github.com/subdigital/beer-lookup.
Listing 10: Additional CSS for styling the search results (public/styles.css)
#results ul {
margin: 20px;
list-style-type: none;
}
#results li {
min-height: 100px;
margin-bottom: 20px;
}
.beer {
clear: left;
text-align: left;
}
.beer .noimg {
width: 100px;
height: 100px;
background-color: #ddd;
float: left;
}
.beer img {
width: 100px;
float: left;
margin-right: 12px;
}
.beer span.name {
margin-left: 120px;
font-size: 1.1em;
font-weight: bold;
display: block;
}
.beer span.description {
display: block;
font-size: 0.8em;
margin-left: 120px;
}
The Finished Result
It's finally time to see the entire thing in action. Entering a search term like “No Label” (a brewery in Katy, TX) yields the results shown in Figure 6.
Try searching for other terms as well, like “imperial stout,” “dales,” or “trippel.” Don't forget to test out what happens when you get no results as well, like searching for “blah.” The application seems to be working perfectly, and looks good to boot!
Bonus Round: Deploy to Heroku
If you have a Heroku account, you can deploy this app online in about two minutes. This is completely optional, but Heroku is an amazing part of the Ruby ecosystem. It makes deploying Ruby apps a breeze. For small usages such as this, it's even free! If you don't have a Heroku account, just observe how little you have to do to get the app on the Web.
First, you need a rackup
file called config.ru
at the root to tell Heroku how to load the application. This turns out to be really easy:
require './server'
run Sinatra::Application
If you haven't already, create a git repository for your changes and commit them all. Then create your Heroku application:
$ heroku create
It will tell you the URL of your new application in the output. Next, you need to provide Heroku with the secret environment variable:
$ heroku config:set BREWERYDB_API_KEY=<yourkeyhere>
With that in place, you can push the source code for the application to Heroku.
$ git push heroku master
You can now open the URL that Heroku gave you when you created the application. Congratulations! You just deployed the Ruby application to the Web, and all before you could finish a cup of coffee.
Final Thoughts
I set off at the beginning of this article to teach the basics of Ruby, apply techniques of real-world Ruby code while building a real application. In addition, I promised a little fun along the way. While I certainly hope that you've had fun building this example, I know that I've just scratched the surface in what Ruby can do in its flexibility, expressive nature, and minimal syntax. Like riding a bike the first few times where you fall a lot and just start to get your balance, work in any new programming language is going to be difficult at first. There are things that don't make sense yet, and more advanced techniques that I didn't cover here, both because the example was incredibly simple, but also because it would require much more explanation.
I still hold by my claim that Ruby is fun and productive, and I hope this article has inspired you to start (or continue) your journey of learning Ruby. Even if you choose to stick with your current language, learning Ruby will have an impact on how you write code and look at software.
Table 1: BreweryDB API Parameters for Searching Beers
Name | Description |
type | Set this to beer, since you want beers, and not breweries, for example |
q | The search term the user typed in |
key | The API key for BreweryDB |
format | You want to deal with JSON, so specify json here. |