Guide to Building a Ruby Gem Wrapper for an API

Last Updated: February 06, 2018


I'm always eager for a code review. If you have suggestions for this gem, please comment at the bottom of the post.

Backstory for Why I Built this Gem

I own a business called FitnessTexter. We set up text message marketing campaigns for fitness businesses. Once we have their campaign up and running, we send them 3 PDF window signs in the mail. Printing these window signs and mailing them used to be a tedious job.

I had to walk to Kinkos, print off the signs, fold and stuff them in an envelope, and then drop the envelope in a mail box. Normally, this would take about 45 minutes from start to finish. Doing this 5 days per week got to be a problem. I decided to find a postal mail API that would solve my problems.

After doing a bit of Googling, I found TryPaper.com. They offered a RESTful API, but didn't have a working Ruby gem to use. In order to quickly start using their service, I decided to write a simple Ruby script that I could run from the command line.

Below is a PasteBin of the Ruby script that I was running from command line. It's quite simple.
1. Run 'ruby uploader.rb' in the command line.
2. It prints out a list of all PDF files currently in the directory.
3. Per TryPaper API requirements, Ruby reads the file, converts it to Base64, and loads it into a variable.
4. It prompts for all the address requirements and stores them in local variables.
5. After completing all the address inputs, it confirms the correct mailing address.
6. Type "N" and the address entry loop restarts. If address is correct, it makes an HTTP POST to the TryPaper API.
7. The POST header contains producion API key. Body contains recipient address, return address ID (from TryPaper dashboard), and Base64 encoded file.
8. TryPaper receives file and sends back a 201 or 400 response. Wah-lah! API success!
9. The second HTTP POST deals with spooling. Learn about spooling at TryPaper if you're interested.


However, I wasn't happy with just having a local Ruby script. I'm a strong believer in open source code. I've benefited immensely from numerous open source projects *cough* Rails *cough*, and I wanted to give back to the community.

Therefore, I set out on creating my first legit open source gem. I emailed the team at TryPaper (mostly .NET guys) and told them I wanted to build them a gem. Their product had saved my business countless hours and I felt like they deserved a gem so more Ruby devs could implement their API. They were stoked and said they couldn't wait to see the finished product.

Building a Ruby Gem for an External 3rd Party API

The hardest part of building a Ruby Gem is getting started. Before building this gem, I had never built a gem before, let alone a gem that was going to utilize a 3rd party API. I could build a pretty good-looking Rails app in a few days, with all sorts of bells and whistles. I could write a nice command line Ruby script. Could I build a production-quality Ruby gem? I had no idea. I was nervous to say the least.

Prior to starting the gem, I had sent an email to Katrina Owens, the creator of Exercism.io I just wanted to get a feel for her coding career path. As a new-on-the-block Ruby developer(I've been doing this since April 2014), I felt like I was "late to the game." Every developer bio I've read seems to say the same thing:

"I've been coding since I was 2 years old. I wrote my first program at 5. By 10, I was a professor at MIT."

You get the drift. Reading those sorts of bios makes a new developer think it's a pointless endeavor. You think to yourself:

"Geez, I'm 27 years old and I just started coding. I'll never achieve their greatness."

Wrong! Katrina started coding when she was 27, and look at what a rockstar she is! There's hope for us newbies! She also gave me a word of advice. I'll paraphrase:

"There are a lot of developers who don't challenge themselves. You'll only get better if you do projects that seem too hard."

This project was intimidating & challenging. Meaning I could only become a better developer as a result.

Challenge accepted! Time to cut a gem!

Creating the Gem and Filling out Gemspec

Creating the directory for your gem is super easy when you use Bundler:

bundle gem trypaper

Done! That was simple. That gives you a base from which to work. Now that you have your directory set up, go ahead and fill in the gemspec info.

First things first, we need to set up the dependencies of the gem. These are going to be the gems that are required to run the gem, either in development stage or in production. Since my gem doesn't require any other gems in production, I only have to worry about development dependencies.

Bundler and Rake come preconfigured when you use the Bundle command to build the gem. For testing purposes, I've chosen to use RSpec(since I've familiar with the DSL) and VCR, which is a gem that helps immitate HTTP requests during tests so that you don't need to make HTTP request every single time you run a test.

gemspec


Doing It the TDD Way. Starting with the Spec Helper


The spec_helper.rb file is required in all your testing specs. It sets important variables that you will need throughout your test, as well as configures RSpec, and any other testing gems. Make sure you REQUIRE your gem, as well any any external testing gems in this file. This will make sure that each of your subsequent spec files has these gems required. Otherwise, you'd have to require each gem in every single testing spec.

I copied and pasted the VCR configuration from their GitHub page. I included the webmock hook_into configuration, because I'll be using webmock to disable external HTTP requests from happening after the initial request. Each request will be saved as a yml file in the 'spec/fixtures/cassttes' directory for using in later tests.

Take some time and read all about VCR. It's a pretty rad gem. I was completely baffled by this gem at the start. I didn't really understand it's purpose. To get a better understanding of how it worked, I looked at a very simple Thesaurus API gem that I've used before,Dinosaurus.

Basically, as I understand it, it's a way to make an HTTP request to a website, and instead of dealing with the HTTP response, it actually saves the response into a directory that you tell it to. From there, everytime you make a similar HTTP request, your tests will just use the VCR cassette response, instead of making the HTTP request again and again. And believe me, when you run your specs tens or hundreds of times during the build, you'll appreciate how speedy those VCR cassettes are.

spec_helper.rb


Creating the Recipient Spec File

One of the least hard, but most important, aspects of creating a gem is figuring out what you want to call your classes. You're going to be using these classes throughout your implementation files and specs files, so make sure they make sense. Otherwise, once you realize that "class PersonWhoRecievesThePDFDocument" is too wordy, you'll have to go back and change everything! Believe me!

So, once you've required 'spec_helper' in your testing spec file, you can start writing your tests. Notice that you need to take into account namespacing when you write your test files. Since my entire gem is inside of the TryPaper module, I need to make sure that I access the classes using "TryPaper::{ClassNameHere}".

In order to make the tests follow the DRY rule, you'll want to refactor any code that is used more than once. Using RSpec's "let" method, I assigned the variable "receiver" to be an instance of the Recipient class. I'll be using "receiver" througout all the test, so this will save me many repeated lines of code.

The first test is making sure that the class even exists. If "receiver" doesn't exist, it couldn't possible be an instance of the "TryPaper::Recipient" class.

After the "existence" test, I tested to make sure that the recipient class had setter methods for all the needed address components.

The second last test makes sure that the end-user can only instantiate the class with string arguments, and not integers. The last test makes sure that the "formatted_address" method (which hasn't been written yet) will return a hash. This hash is what will be passed sent to the TryPaper API.

recipient_spec.rb

Creating the Document Spec File

The document_spec file is pretty simple. We just need to make sure that the class exists, that the class accepts a file when initialized, and that they class has a method that can convert the file into Base64 format. Simple enough.

document_spec.rb


Creating the Mailer Spec File

This file is the bread and butter of the Gem. The mailer file is where everything comes together to make the HTTP request.

First thing, I need to make sure the class exists. After that, we're testing to see that there are getter/setter methods for doc and recipient. The final test(before the HTTP submit tests), is making sure that the Mailer class can accept an optional array argument. This optional array will hold any "optional printing tags" that the end-user wants to send to TryPaper.

The last three tests get a little more complex. The first (of the last three) makes sure that the gem lets you upload and send a PDF file. The second test makes sure you can't upload and send a TXT file (TryPaper only accepts PDF files), and the last test makes sure that you can add those optional tags when you upload a PDF document.

They utilize the VCR gem to make an initial request to the TryPaper API. They record the response into a designated file (that doesn't exist before you run the tests for the first time).

When you run the tests for the first time, it will create a file in your "fixtures/cassettes" directory, and name it whatever you name it. Name the cassette something that makes sence to your tests. See how I named mine what I was testing. As you can see below the mailer.rb code, I have three VCR cassette YML files that match the three names that I used in the tests below.

If you run your test once than once, it will not recreate the YAML file. Therefore, if you need to make changes to tests and get a fresh YAML file, just erase the cassette that goes by the same name as the one in the test, and it will record a new cassette the next time you run your testing suite.

Hope you're not too confused! It took me a while to grasp this concept, so don't worry if you're overwhelmed. Give your brain some time to digest all this. Maybe read the VCR docs to get a better understanding. Reading gem docs is the best thing you can do to get a better understanding of what a gem does and how it does it.

mailer_spec.rb

Using VCR Gem to Mimic HTTP Requests

Below are the three VCR cassettes that were created during my tests. Once they've been created, you can write other tests based on a 201 "successful" HTTP request or a 400 "unsuccessful" HTTP request, without having to make more API calls. You just tell VCR to use the cassette that already exists(your 201, 500, 400 cassette, etc) and your tests will be speedy as ever!

pdf_document.yml

good_document_with_tags.yml



bad_document.yml

Writing Implementation Code...The Fun Part of TDD!

Once you've got all your tests written and failing, it's time to get some green in your life.


Creating the Document Gem File

This is a pretty simple class to create. Make sure you wrap all your classes in "module GemName". This creates the proper namespacing so that you can instantiate your gem with "GemName::ClassName.new.(arg1, arg2, etc)"

Since I knew my ReadMe would be quite descriptive, I made this class pretty basic. The end user must first a PDF file into a variable. That variable is then used when they instantiate the document class.

In order to get the tests to pass, I need to make sure I have getter methods, so that's where the "attr_reader" comes in.


document.rb

Creating the Recipient Gem File


The recipient file is not that complex either. Just a matter of writing it so that the recipient object can pass the correct recipient info to the TryPaper API. They require certain field to be sent to their API, in a certain format, JSON.

Once again, make sure you wrap your class in the correct module name of your gem. After that, I wanted to create a special error(InvalidDataError) that would fire if any recipient information wasn't in the proper String format.

I chose to use attr_accessor for the address input field so that, if the person didn't want to configure the entire object using a block(see configure method), they could manually set each address value.

I used attr_reader for the "formatted address" method, because that method needs to be read-only. No use giving someone the ability to mess up the JSON format that it returns. The formatted address return value is exactly what the TryPaper API is expecting for the address fields.

I wrote a private method called "check_attributes" that is used in conjunction with the "formatted_address" method. It takes all the given address components and runs then through a block to make sure that they are all in String format. If they aren't, the "InvalidDataError" error will fire before the gem can send the faulty address fields to the API. My goal was to make scrub all data before it even gets to the API so as to reduce the amount of 400 error API responses.

The configure method is something I'm VERY proud of. Blocks are one of the most important parts of the Ruby language. I'll admit that my knowledge of building methods that take blocks was rather shaky. I did some reading and looked into other gems that had similar configure methods. This method yields "self", which is the recipient object. By yielding itself, it gives you the ability to set all the address fields using a simple block. See example below:

Using a configure method that takes a block



recipient.rb


Creating the Mailer Gem File


This is truly the meat and potatoes of this gem. This is where it all comes together.

Because I'll be working with some JSON and making an HTTP request, I need to require both 'json' and 'net/http'. Once again, make sure you wrap you method inside the correct ruby gem module name.

I included two new error types, but looking back, I never actually used these errors in this class. I'll need to pull them out on my next refactor.

Since both the document and the recipient need to be set after instantiation, I used attr_accessor for those. The other info is available as convenience methods using attr_reader.

When the user instantiates a Mailer object, they need to include their TryPaper API key, as well as their return address ID. These are both created in the TryPaper dashboard. If they so desire, they can include an array of printing tags. There are 5 tags available for use, and they are all located in the Tags documentation of the TryPaper API.

The "send_data" method is what actually gets sent to the API. Before this method can be called, the end-user must set the document variable using the attr_accessor method. They can also set the recipient variable using the attr_accessor method, or they can set it directly inside the client using the "configure" method that was supplied in the recipient class.

The "submit" method is a basic HTTP Post. TryPaper requires the post be made over an SSL connection, so I needed to make sure to set SSL to true. Form there, I build the headers to include the API key and the content-type.

The "send_data" information is converted into JSON and then we make the HTTP Post call using the "request" method method supplied by the "net/http" library.

A future refactor will include dealing with the reponse from the API. Right now, as it stands, you can go into the TryPaper API logs to see the response. However, I should really deal with that information from inside the Gem. I will utilize those errors that I created at the top of this class to deal with various response codes.




mailer.rb

Creating the Version File


Once you've finished all your code, it's time to bump the version. Every change to your gem needs to be noted in your version number. Here's a good Gem Pattern article about the different types of changes and where you should document them. Major version changes vs minor version changes, etc.

version.rb

Creating the Main Gem File

After reading numerous blog posts about building gems and scouring the Twilio-Ruby gem(which I consider amazing and best-practice worthy), I realized that the most common pattern to structure a gem is to have your main file include all your other necessary files. So, as you can see in the snippet below, the most important file in the entire gem is just a bunch of "requires" of all the other source files. Simple enough!

trypaper.rb

Creating the Finished ReadMe File


Writing the ReadMe file is just the cherry on top. After all that hard work, you get to tell your Ruby brothers and sisters how they get to use your precious little gem. Use MarkDown to make it look pretty. Give a good example usage, so that people will know how to use your gem out of the box.

Readme


Take-Aways from Building the TryPaper.com API Gem

One of the best things I did before creating this gem was writing the Ruby script that you see at the very top of this blog post. Writing that script and learning the ins-and-outs of the TryPaper API were very important. If you don't understand the API you're working with, you'll never know how to build a gem that interacts with the API in question.

Also, for your first gem project, find something that interests you. You want to be excited to complete your gem. If you're not stoked to complete your gem and push it to RubyGems.org, you'll never overcome the difficult times that you're sure to interact. For me, completing this gem was necessary before I could build TryPaper's web app. I was stoked to finish the gem so I could start the app. Also, since the TryPaper guys have helped my business save 20 hours per month, I thought they deserved a Ruby gem. They have a great product and having a Ruby gem is only going to help their business grow.

I need to thank Matt and Ben from QuickLeft. Reading their blog posts about creating your first gem and wrapping external APIs in gems gave me the inspiration to build my own gem. Also want to thank David Tuite for answering some VCR questions for me.

Closing Thoughts on Building My First Ruby Gem

All I can say is, holy cow, what a sense of accomplishment! From having never built a gem, to building a gem I was very proud of; it was a great experience.

I learned a lot during this build and more importantly, I internalized that I COULD write good Ruby code, I COULD learn new libraries that I was unfamiliar with, and that I COULD hope to be a great Ruby developer one day.

It's a step by step process my brothers and sisters. You're not going to go from novice developer to Katz in a day, a week, a month, or even a year. But damnit man, just keep coding, keep learning, and don't let anything hold you back!