Ever thought gem world magic? You assume you must be a fantastic developer to create yours? This time has gone. Today, you are going to create your amazing gem by your own!

Lets get started!

Requirements

Before starting, we need to set up a couple of things. Firstly, you need to sign up to RubyGems. All existing gems are stored in this place. You must have an account for publishing your gem.

Finally, you only require your imagination! Find a great name for your gem, because each one must have a uniq name. For my part, my gem will be named Lombard. Anytime you see Lombard, just replace it by your custom gem’s name.

Everybody look - what’s going down?

Here, we are going to build a simple gem for fetching weather forecast and displays it in our terminal. Our gem should work for any city.

Ready? So saddle up cow-boy!

Build the fetcher

First round

First of all, create a new project folder and add these files and folders:

$ tree
.
|-- lombard.gemspec
|-- lib/
    |-- lombard.rb

lombard.gemspec is your definition file: it describes your gem. Name, version number, dependencies etc. will be defined in it.

Here is a sample of gemspec file. Use it as yours:

require File.dirname(__FILE__) + '/lib/lombard'

Gem::Specification.new do |s|
  s.name = 'lombard'
  s.version = '1.0.0'
  s.date = Date.today.to_s # Publication date
  s.summary = 'Fetches weather to your terminal'

  # Uncomment options below if you wish

  # Add a description
  # s.description = 'My fantastic description'

  # Set homepage (public repository)
  # s.homepage = 'https://github.com/acadet/lombard'

  # That's you!
  # s.authors = [ 'Adrien Cadet' ]

  # If you want people to get in touch with you
  # s.email = [email protected]'

  # Files to include in your build
  # s.files = [ 'lib/lombard.rb' ]

  # Executable script if any
  # s.executables = [ 'lombard' ]

  # License
  # s.license = "MIT"
end

lombard.rb is your main file. It will wraps your Ruby code and should be in lib folder. Open it and paste this code:

class Lombard
    def self.run
        puts "I am so awesome!"
    end
end

You are almost done! We coded a first version of our gem. Now, we need to build and install it on our local machine. No worries, it is really easy.

For building, run this command:

gem build lombard.gemspec

Then, install outcome:

gem install ./lombard-1.0.0.gem

Finally, we need to test our library. Just start ruby machine and run these commands:

$ irb
>> require 'lombard'
=> true
>> Lombard.run
I am so awesome!

Nice! You have just crafted our own build. Wasn’t so hard, was it?

Have you seen it? For building you need to give to gem executable your gemspec file. It will generate a .gem file, whose name is relative to current version. Your gem is then ready to be installed anywhere. Just run gem install to do it.

Time for dirty job

Well, our gem is not very useful at this point. We need to improve that. However, we are not a shitty developer. We must define our roadmap before starting coding.

Here, we would like to fetch weather forecast for any city, which is provided by user. Then, we want to use pulled data for displaying a fancy output to user.

Weather API

For fetching forecasts, we are going to use the excellent OpenWeatherMap’s API. I let you skim the documentation if you feel interested. In this tutorial, endpoint we should use is http://api.openweathermap.org/data/2.5/weather?q=My_city. q parameter is provided city.

Using your terminal, you can easily check what this API returns:

$ curl -X GET http://api.openweathermap.org/data/2.5/weather?q=London
{"coord":{"lon":-0.13,"lat":51.51},"sys":{"type":3,"id":60992,"message":0.0233,"country":"GB","sunrise":1422603671,"sunset":1422636386},"weather":[{"id":800,"main":"Clear","description":"Sky is Clear","icon":"01n"}],"base":"cmc stations","main":{"temp":274.45,"humidity":90,"pressure":979.7,"temp_min":274.45,"temp_max":274.45},"wind":{"speed":2,"gust":5,"deg":202},"rain":{"3h":0},"snow":{"3h":0},"clouds":{"all":0},"dt":1422661212,"id":2643743,"name":"London","cod":200}

Woah, so much information! Alright, have a closer look to this… Here I am only interested by:

  • City
  • Country
  • Forecast description (sunny, most cloudy etc.)
  • Temperature

Using this JSON object, I will need these fields then:

  • City ~> name
  • Country ~> sys.country
  • Forecast content ~> weather[0].main and weather[0].description
  • Temperature ~> main.temp.

Nice! We are ready for pulling… Oh wait. How are we going to do that?

When you HTTParty, you must party hard!

As you may have guessed, we are going to use HTTParty. It is a famous and very simple Ruby library to send HTTP requests.

First, install it:

$ gem install httparty

Once done, you can easily check if everything is fine. Just pull forecast again but use HTTParty this time:

$ httparty http://api.openweathermap.org/data/2.5/weather?q=London
> {"coord"=>{"lon"=>-0.13, "lat"=>51.51},
 "sys"=>
  {"type"=>3,
   "id"=>60992,
   "message"=>0.0233,
   "country"=>"GB",
   "sunrise"=>1422603671,
   "sunset"=>1422636386},
 "weather"=>
  [{"id"=>800, "main"=>"Clear", "description"=>"Sky is Clear", "icon"=>"01n"}],
 "base"=>"cmc stations",
 "main"=>
  {"temp"=>274.45,
   "humidity"=>90,
   "pressure"=>979.7,
   "temp_min"=>274.45,
   "temp_max"=>274.45},
 "wind"=>{"speed"=>2, "gust"=>5, "deg"=>202},
 "rain"=>{"3h"=>0},
 "snow"=>{"3h"=>0},
 "clouds"=>{"all"=>0},
 "dt"=>1422661212,
 "id"=>2643743,
 "name"=>"London",
 "cod"=>200}

Amazing! We have the same result.

Here we go!

We are now ready to code from now on.

Firstly, we need to pull data from OpenWeather:

require 'httparty'
require 'uri'

class Lombard
    @@api_url = "http://api.openweathermap.org/data/2.5/weather"

    def fetch(location)
        # Send GET request
        # Do not forget to escape special characters
        response = HTTParty.get "#{@@api_url}?q=#{URI.escape(location)}"

        # Parse raw data
        JSON.parse response.body
    end
end

So easy! Let’s format this input. What about this format?

# Mountain View (United States of America)
  Clear - Sky is Clear
  18°C / 66°F

Add this method to your Lombard class:

def format(obj)
  outcome = "# #{obj['name']}"

  country = obj['sys']['country']
  if country != nil
    # Country may be missing
    outcome += " (#{country})"
  end

  outcome += "\n  #{obj['weather'].first['main']}"

  description = obj['weather'].first['description']
  if description != nil
    # Annex description may be optional
    outcome += " - #{description}"
  end

  # Temperature is in Kelvins, we need to compute
  # Celsius and Fahrenheits
  temp = obj['main']['temp']
  celsius = temp.to_f - 273.15
  fahrenheit = celsius * 1.8 + 32
  outcome += "\n  #{celsius.to_i}°C / #{fahrenheit.to_i}°F"

  outcome
end

Awesome. I think we are quite ready, right? We only need to add a run function:

def run(location)
    puts format(fetch(location))
end

That’s all folks! Your Ruby fetcher is done.

Make your gem runnable

Before deploying our gem, we are going to make it runnable, as a shell command. Thanks to that, we will be able to run our gem directly from terminal, like this: $ lombard Paris.

By chance, RubyGems made their system so straighforward, we have quite nothing to do. First of all, add a new folder and a new file to our project:

$ tree
.
|-- lombard.gemspec
|-- bin/
    |-- lombard
|-- lib/
    |-- lombard.rb

lombard file will be our executable. To make it runnable, we need to inform system it is. Run this simple command from your project folder’s root:

chmod 755 bin/lombard

Next, add this code to your executable file:

#!/usr/bin/env ruby
# First line is Shebang. It informs system what
# engine it should use to run executable file.
# Here it needs Ruby one.

require 'lombard' # You need your library

if ARGV.size < 1
  puts "No location provided"
else
  Lombard.new().run(ARGV[0])
end

Final step is to update your gemspec file:

require File.dirname(__FILE__) + '/lib/lombard'

Gem::Specification.new do |s|
  s.name = 'lombard'
  s.version = '1.0.0'
  s.date = Date.today.to_s
  s.summary = 'Fetches weather to your terminal'

  # Files to include in build
  s.files = [ 'lib/lombard.rb', 'bin/lombard' ]

  # Executable script to add
  s.executables = [ 'lombard' ]
end

Here we go! Build and install your gem again. Finally run this command:

$ lombard London
# London (GB)
  Clear - Sky is Clear
  1°C / 34°F

Wonderful!! You gem works as expected!

Hold on dude, hold on.

If I type ‘San Francisco’, it does not work.

Right on. It does not because we did not parse shell arguments correctly. Update your code in lombard runnable:

Lombard.new().run(ARGV.join(" "))

Two constants are used when parsing arguments from Shell (aka Terminal): ARGV and ARGC. First one is an array of all arguments passed as input ; second one is only the size of this array. Usually, argument array contains at least a single element, which is the name of command, of program (here lombard).

Nevertheless, in Ruby, usage is slightly different. We only use ARGV constant (and ARGV.size to get size) and it does not include program’s name. Using San Francisco as input, ARGV will be equal to ["San", "Francisco"].

In coding line above, I told Ruby to join every argument with a space character. Using same input, outcome will be "San Francisco".

Push me baby

We are almost done! Final step is pushing our gem to RubyGems. For that, we need firstly to save our credentials on local machine. With them, gem program will be able to bind our account to the gem we push.

Run this command, then enter your RubyGems password:

curl -u qrush https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials; chmod 0600 ~/.gem/credentials

You are ready to push. In your project folder, run this simple command:

gem push lombard-1.0.0.gem

Congratulations! You have just shared your gem with RubyGems’ community. Visit you profile and admire your work :)

If you want to update your gem, you need to push it again. However, gem will not let you push a gem with an existing version number. Here, we worked on 1.0.0 version. For updating it, you need to edit version field in your gemspec file.

Then, when running gem install, a lombard-1.1.0.gem will be produced (if version is 1.1.0). Push that one to update your nugget.

Go further

Once this tutorial over, you may be interested in improving this program. Indeed, it is quite simple. Here are some ideas you can implement for improving your app:

  • Improve error handling: here, no error is handled. If city does not exist, it will crash again. Besides, what happens if there is no connection?
  • Parse options: as a shell command, your program should include options such as --help, --versionetc. For parsing options, you can do that by your own or use a library. Most famous Ruby one is Trollop.
  • Add features: feel free to add extra features to your program. For example, what about allowing user to define his favourite cities and fetch forecast for all of them? Let him define them in a YAML file would be a great idea.

The gem I built is public and can be found there. Feel free to open issue in tracker if you have any questions. My program supports favorites and a couple of options.