Creating software requires from us building common functionalities. In order to not reinvent the wheel again, developers are using packages. In Ruby such packages are called Ruby gems. Gems are self-contained pieces of code shared publicly by other developers for you to use in your own application.

As a developer, writing a package is a stepping stone to a whole new level. By creating your own gem you can contribute back to the Ruby developers community and build your personal brand by presenting your code to a larger audience.

Only practice makes perfect. Writing a Ruby gem is like creating a tiny application but it requires a different approach. You have to think of how to solve only one problem, isolate the logic responsible for this and allow other developers to use it in their applications with success. You also learn how to organize your code so it’s readable and extendable for other developers and it’s challenging because it requires taking a wider perspective.

This may seem overwhelming at first, but in this article, I'll show you how to build a simple Ruby gem from scratch and publish it.

Building Ruby gem from scratch - ButterCMS use case

ButterCMS is a headless CMS and Content API. It has its own Ruby gem already but in this tutorial, we will build it from a scratch, in a slightly different way. We will use the ButterCMS API documentation and we will follow Test Driven Development rules in order to create high-quality code without any bugs.

Preparation

We want to fully integrate our ButterCMS account with any Ruby on Rails app. The only requirement is setting the API key in the application initializer in order to get the data from your account. You can get your API key by signing in on the https://buttercms.com account and it will be visible on the welcome page. After doing this we should be able to use models with the ButterCMS prefix, for example ButterCMS::Blog.

To achieve our main goal, we will use two other gems inside our gem: RSpec and RestClient. RSpec is a testing framework and will be a development dependency, meaning we only need it when building the Ruby gem. RestClient is a library for performing requests, which we will use to send requests to the API.

The gem file structure is quite simple. Here are the main parts:

butter_cms.gemspec - in this file, we store information about the gem and its dependencies

/spec - this is the directory for our tests

lib/butter_cms.rb - this is the main file of the gem, we will require all other files here

lib/butter_cms - this is the directory for the main logic

Our code should be also well-documented which means that for each method we have to add proper comments so that later we can create a documentation without any additional actions.

The Tutorial

Gem configuration

Start with creating a new repository on Github. Then create a new directory on your own machine for our gem source:

mkdir butter_cms cd butter_cms/

Now it’s time to create an empty GIT repository, add the first commit, and push changes to the remote repo:

echo "# butter_cms" >> README.md git init git add README.md git commit -m "first commit" git remote add origin git@github.com:your_username/butter_cms.git git push -u origin master

Note that you should replace your_username/butter_cms part with your Github username and the repository name you created.

Now we can create the butter_cms.gemspec file which will contain all information about our gem. Create it and add the following contents:

Gem::Specification.new do |s| s.name = 'butter_cms_v2' s.date = '2018-08-15' s.summary = "Ruby wrapper for the ButterCMS API" s.authors = ["Paweł Dąbrowski"] s.email = 'dziamber@gmail.com' s.homepage ='http://github.com/rubyhero/butter_cms' s.license = 'MIT' end

Replace the above text to match your own personal information.

This is a great start, but when you run gem build butter_cms.gemspec it will fail with the following error:

missing value for attribute version

To fix this error, let’s add the version number for our gem. We will put the current version into a separate file. Create lib/butter_cms/version.rb and add the following contents:

module ButterCMS module Version module_function # Gem current version # # @return [String] def to_s "0.0.0.1" end end end

Note that we also added comments that will be automatically transformed into documentation when the gem is pushed. Now, we have to update our gemspec file and add the version attribute:

lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'butter_cms/version'

Gem::Specification.new do |s| s.name = 'butter_cms_v2' s.date = '2018-08-15' s.summary = "Ruby wrapper for the ButterCMS API" s.authors = ["Paweł Dąbrowski"] s.email = 'dziamber@gmail.com' s.homepage = 'http://github.com/rubyhero/butter_cms' s.license = 'MIT' s.version = ButterCMS::Version end

You can now run gem build butter_cms.gemspec which will create the butter_cms_v2-0.0.0.1.gem file. There is no sense in installing this gem locally, as it does nothing yet.

Tests configuration

The last step before we start the development process is to configure the test environment. We will be following Test Driven Development (TDD) rules using RSpec - it means that we will write a test first, it will fail and we will fix the test by writing code. We will repeat such order for each new class, feature or method.



Open butter_cms.gemspec and add this line:

s.add_development_dependency "rspec", '~> 3.7', '>= 3.7.0'

Now create spec/spec_helper.rb :

require 'rspec'

Update butter_cms.gemspec again to let Ruby know that we want to auto-load our spec helper:

s.files = Dir['spec/helper.rb']

The last step is to create Rakefile . This is needed in order to be able to run rake spec and run all tests. Create Rakefile in the main directory with the following code:

require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) # If you want to make this the default task task default: :spec

API key configuration storage

To access our ButterCMS account we have to pass api_key to each request we want to send. To make the gem configuration easier, we will provide a nice way to define api_key in the Rails initializer.

We have to create a Configuration class first. By default we should use PLEASE PROVIDE VALID API KEY as the api_key value. It should also be possible to define the api_key inside the Rails initializer.

Let’s start by writing a test which expects the api key. Create spec/lib/butter_cms/configuration_spec.rb file and add the following:

require 'spec_helper' describe ButterCMS::Configuration do describe 'initialization' do it 'sets default value for the api_key attribute' do configuration = described_class.new expect(configuration.api_key).to eq('PLEASE PROVIDE VALID API KEY') end end end

Run rake spec to run the tests. The test will not pass because we didn’t add the Configuration class yet. Create lib/butter_cms/configuration.rb and add the following code:

module ButterCMS class Configuration attr_accessor :api_key def initialize @api_key = 'PLEASE PROVIDE VALID API KEY' end end end

Now let Ruby know that we want to load all files inside the lib directory. In order to do that we have to update butter_cms.gemspec :

s.files = Dir['spec/helper.rb', 'lib/**/*.rb']

The last step is to create the main file located under lib/butter_cms.rb path:

require 'butter_cms/configuration'

and load it in the spec_helper :

require 'rspec' require 'butter_cms'

Now we can run rake spec and the test should pass. Thanks to using TDD approach we know that our code is working as expected. Instead of creating code first and then test it we created expectations and we created code that satisfies them.

Setting API key

Now we have a place for storing the api_key , but we want to provide an easy way to set it. We want something like this:

ButterCMS.configure do |config| config.api_key = "api_key" end

Since we are following TDD principles, let’s start by writing a test in spec/lib/butter_cms_spec.rb :

require 'spec_helper' describe ButterCMS::Configuration do describe 'initialization' do it 'sets default value for the api_key attribute' do configuration = described_class.new expect(configuration.api_key).to eq('PLEASE PROVIDE VALID API KEY') end end end

For the test to pass, we have to update lib/butter_cms.rb :

require 'butter_cms/configuration' module ButterCMS class << self attr_accessor :configuration end # Returns the configuration class instance if block given # # @return [ButterCMS::Configuration] def self.configure self.configuration ||= ::ButterCMS::Configuration.new yield(configuration) if block_given? end end ButterCMS.configure unless ButterCMS.configuration

Performing requests

Now that we have a way to set the api_key, we can start performing requests. In order to do this we will use the excellent and popular RestClient gem. Add the following line to butter_cms.gemspec :

s.add_runtime_dependency 'rest-client', '~> 2.0'

Let’s take a look at the example API url:

https://api.buttercms.com/v2/posts/?page=1&page_size=10&auth_token=api_key

We need to write a service for parsing params. It should form a query string from the given options hash (if present) and add the auth_token param at the end.

Following TDD principles, create spec/lib/butter_cms/url_params_service_spec.rb and add two tests:

require 'spec_helper' describe ButterCMS::UrlParamsService do describe '.call' do it 'returns query string with the auth_token param' do api_key = 'api_key' allow(ButterCMS.configuration).to receive(:api_key).and_return(api_key) options = { page: 1, per_page: 10 } expect(described_class.call( options )).to eq("?page=1&per_page=10&auth_token=api_key") end it 'returns query string with the auth_token param when given options are blank' do api_key = 'api_key' allow(ButterCMS.configuration).to receive(:api_key).and_return(api_key) expect(described_class.call).to eq("?auth_token=api_key") end end end

Now we have to satisfy these tests by creating a UrlParamsService class in lib/butter_cms/url_params_service.rb :

module ButterCMS class UrlParamsService # Returns the query part of the request to the API # # @return [String] def self.call(options = {}) options[:auth_token] = ButterCMS.configuration.api_key "?" << options.map { |(key, value)| "#{key.to_s}=#{value}" }.join("&") end end end

Finally, we can load our new service by editing lib/butter_cms.rb :

require 'butter_cms/url_params_service'

Now when we run rake spec, all specs should pass.

Now that we have a service for parsing params, we can create a class responsible for sending requests. Because API URL won’t change often, we should have it saved somewhere. In order to do this, create a new file lib/butter_cms/requests/api.rb and add the following contents:

module ButterCMS module Requests module API URL = "https://api.buttercms.com/v2/".freeze end end end

Ensure it’s loaded by updating lib/butter_cms.rb :

require 'butter_cms/requests/api'

It’s time to create the request class. Create a test for it in spec/lib/butter_cms/requests/get_spec.rb :

require 'spec_helper' describe ButterCMS::Requests::Get do describe '.call' do it 'performs request' do options = { page: 1 } path = "posts" query = "?page=1&auth_token=api_token" allow(ButterCMS::UrlParamsService).to receive(:call).with( options ).and_return(query) full_url = "https://api.buttercms.com/v2/posts?page=1&auth_token=api_token" response = double('response') allow(RestClient).to receive(:get).with(full_url).and_return(response) result = described_class.call("posts", options) expect(result).to eq(response) expect(ButterCMS::UrlParamsService).to have_received(:call).with( options ).once expect(RestClient).to have_received(:get).with(full_url).once end end end

and implement the logic by creating lib/butter_cms/requests/get.rb :

require 'rest_client' module ButterCMS module Requests class Get # Returns response from the request to the given API endpoint # # @return [RestClient::Response] def self.call(path, options = {}) full_url = [ ::ButterCMS::Requests::API::URL, path, ::ButterCMS::UrlParamsService.call(options) ].join ::RestClient.get(full_url) end end end end

As always, don’t forget to automatically load our new file by updating lib/butter_cms.rb :

require 'butter_cms/requests/get'

Parsing the response

We now know how to make a request, but we still have to take care of parsing the response out of the JSON format. We have to decode the response and transform it into objects - just like what normally happens in Rails when you request records from a database.

Let’s start by creating a new test. Create spec/lib/butter_cms/parsers/posts_spec.rb and add the following tests:

require 'spec_helper' describe ButterCMS::Parsers::Posts do let(:response_body) do { "meta" => { "count" => 3, "next_page" => 3, "previous_page" => 1 }, "data" => [ { "url" => "sample-title" } ] }.to_json end let(:response) { double(body: response_body) } describe '#next_page' do it 'returns next page' do parser = described_class.new(response) expect(parser.next_page).to eq(3) end end describe '#previous_page' do it 'returns previous page' do parser = described_class.new(response) expect(parser.previous_page).to eq(1) end end describe '#count' do it 'returns the total count of posts' do parser = described_class.new(response) expect(parser.count).to eq(3) end end describe '#posts' do it 'returns posts' do parser = described_class.new(response) expect(parser.posts).to eq([{ "url" => "sample-title" }]) end end end

We want to be able to fetch the following values from the posts response:

count - the total count of posts available in the database

next_page - the number of the next page of the results if available

previous_page - the number of the previous page of the results if available

posts - array with our posts and associated objects

Now it’s time to satisfy the tests we wrote by creating a new file lib/butter_cms/parsers/posts.rb with the following code:

require 'json' module ButterCMS module Parsers class Posts def initialize(response) @response = response end # Returns the number of the next page or nil if not available # # @return [String] def next_page parsed_json['meta']['next_page'] end # Returns the number of the previous page or nil if not available # # @return [String] def previous_page parsed_json['meta']['previous_page'] end # Returns the count of existing posts # # @return [String] def count parsed_json['meta']['count'] end # Returns array of posts attributes available in the response # # @return [Array] def posts parsed_json['data'] end private attr_reader :response def parsed_json @parsed_json ||= ::JSON.parse(response.body) end end end end

The last step is to let our gem know that we want this file to be loaded. In order to do this we have to update lib/butter_cms.rb :

require 'butter_cms/parsers/posts'

In the posts response, we have access to the following associations:

author - a hash of author data attributes

tags - an array of tags associated with the given post

categories - an array of categories associated with the given post

Now, our job is to create the ButterCMS::Post.all method which will return an array of ButterCMS::Post objects. We should have access to the associated objects just like in Rails.

First, we have to create a base class which will dynamically replace a hash of attributes with a class with attributes. Create spec/lib/butter_cms/resource_spec.rb and add new tests:

require 'spec_helper' describe ButterCMS::Resource do describe 'attributes assignment' do it 'assigns attributes from the given hash' do attributes = { 'title' => 'my title', 'slug' => 'sample-slug' } resource = described_class.new(attributes) expect(resource.title).to eq(attributes['title']) expect(resource.slug).to eq(attributes['slug']) end end end

As you can see, for each key and value in the given hash we have to create an attribute in our class. We want to do this dynamically, so the following definition would not work:

module ButterCMS class Resource def initialize(attributes) @title = attributes['title'] @slug = attributes['slug'] end attr_reader :title, :slug end end

Instead, we have to do this dynamically without knowing the names of attributes. Create lib/butter_cms/resource.rb and add the following code:

module ButterCMS class Resource def initialize(attributes = {}) attributes.each do |attr, value| define_singleton_method(attr) { attributes[attr] } end end end end

As always, open lib/butter_cms.rb and load our new file:

require 'butter_cms/resource'

Now, we have to create our models: Post, Category, Tag and Author. We do not need specs for them now because we will create empty classes.

lib/butter_cms/post.rb :

module ButterCMS class Post < ButterCMS::Resource end end

lib/butter_cms/category.rb :

module ButterCMS class Category < ButterCMS::Resource end end

lib/butter_cms/tag.rb :

module ButterCMS class Tag < ButterCMS::Resource end end

lib/butter_cms/author.rb :

module ButterCMS class Author < ButterCMS::Resource end end

Now we can move to the step where we implement the ButterCMS::Post.all method. For each association in the single post, we have to create a parser class which will turn the attributes into one class with those attributes.

First create a spec for the author association. The file should be named lib/butter_cms/parsers/author_object_spec.rb :

require 'spec_helper' describe ButterCMS::Parsers::AuthorObject do describe '.call' do it 'returns new instance of ButterCMS::Author' do attributes = { name: 'John Doe' } author = described_class.call(attributes) expect(author).to be_instance_of(ButterCMS::Author) expect(author.name).to eq('John Doe') end end end

Now, satisfy the test:

module ButterCMS module Parsers class AuthorObject # Returns the new instance of author object from the given attributes # # @return [ButterCMS::Author] def self.call(author) ::ButterCMS::Author.new(author) end end end end

and load files inside the lib/butter_cms.rb :

require 'butter_cms/author' require 'butter_cms/parsers/author_object'

Now it’s time for the categories association. This time our class logic will be a little more complicated than the class for author association. Create spec/lib/butter_cms/parsers/categories_objects_spec.rb :

require 'spec_helper' describe ButterCMS::Parsers::CategoriesObjects do describe '.call' do it 'returns the array of new instances of ButterCMS::Category' do category_attributes = { name: 'Category 1' } another_category_attributes = { name: 'Category 2' } attributes = [category_attributes, another_category_attributes] categories = described_class.call(attributes) expect(categories.first).to be_instance_of(ButterCMS::Category) expect(categories.first.name).to eq('Category 1') expect(categories.last).to be_instance_of(ButterCMS::Category) expect(categories.last.name).to eq('Category 2') expect(categories.size).to eq(2) end end end

Satisfy the test by creating a CategoriesObjects class in lib/butter_cms/parsers/categories_objects.rb :

module ButterCMS module Parsers class CategoriesObjects # Returns array of category objects created from given array of attributes # # @return [Array<ButterCMS::Category>] def self.call(categories) categories.map do |category_attributes| ::ButterCMS::Category.new(category_attributes) end end end end end

And load the new files in lib/butter_cms.rb :

require 'butter_cms/category' require 'butter_cms/parsers/categories_objects'

We will repeat the same action for the tags. Create tests in spec/lib/butter_cms/parsers/tags_objects_spec.rb :

require 'spec_helper' describe ButterCMS::Parsers::TagsObjects do describe '.call' do it 'returns the array of new instances of ButterCMS::Tag' do tag_attributes = { name: 'Tag 1' } another_tag_attributes = { name: 'Tag 2' } attributes = [tag_attributes, another_tag_attributes] tags = described_class.call(attributes) expect(tags.first).to be_instance_of(ButterCMS::Tag) expect(tags.first.name).to eq('Tag 1') expect(tags.last).to be_instance_of(ButterCMS::Tag) expect(tags.last.name).to eq('Tag 2') expect(tags.size).to eq(2) end end end

Then satisfy those tests by creating a TagsObjects class in lib/butter_cms/parsers/tags_objects.rb :

module ButterCMS module Parsers class TagsObjects # Returns array of tag objects created from given array of attributes # # @return [Array<ButterCMS::Tag>] def self.call(tags) tags.map do |tag_attributes| ::ButterCMS::Tag.new(tag_attributes) end end end end end

Load the new files in lib/butter_cms.rb :

require 'butter_cms/tag' require 'butter_cms/parsers/tags_objects'

Finally, we can create a parser class for our post attributes. Parser should handle all associations and classes we created in the previous steps.

Add tests in spec/lib/butter_cms/parsers/post_object_spec.rb :

require 'spec_helper' describe ButterCMS::Parsers::PostObject do describe '.call' do it 'returns new ButterCMS::Post instance' do category_attributes = { 'name' => 'Category 1' } author_attributes = { 'name' => 'John Doe' } tag_attributes = { 'name' => 'Tag 1' } post_attributes = { 'title' => 'post title', 'categories' => [category_attributes], 'author' => author_attributes, 'tags' => [tag_attributes] } category = instance_double(ButterCMS::Category) tag = instance_double(ButterCMS::Tag) author = instance_double(ButterCMS::Author) post = instance_double(ButterCMS::Post) allow(ButterCMS::Parsers::TagsObjects).to receive(:call).with( [tag_attributes] ).and_return([tag]) allow(ButterCMS::Parsers::CategoriesObjects).to receive(:call).with( [category_attributes] ).and_return([category]) allow(ButterCMS::Parsers::AuthorObject).to receive(:call).with( author_attributes ).and_return(author) updated_attributes = { 'title' => 'post title', 'categories' => [category], 'author' => author, 'tags' => [tag] } allow(ButterCMS::Post).to receive(:new).with( updated_attributes ).and_return(post) result = described_class.call( post_attributes ) expect(result).to eq(post) expect(ButterCMS::Parsers::TagsObjects).to have_received(:call).with( [tag_attributes] ).once expect(ButterCMS::Parsers::CategoriesObjects).to have_received(:call).with( [category_attributes] ).once expect(ButterCMS::Parsers::AuthorObject).to have_received(:call).with( author_attributes ).once end end end

This spec is larger than the other specs because we have to stub other parsers. By stubbing, I mean calling a mock instead of the original class. Thanks to the stubbing we can control given class behavior without calling the logic. We are doing this because we only want to test the current class body not the code in other classes. In this class we will connect all the pieces of the response into one ButterCMS::Post object with the relevant associations.

Let’s satisfy the test by creating a PostObject class in lib/butter_cms/parsers/post_object.rb :

module ButterCMS module Parsers class PostObject # Returns the new instance of post with the associations included # # @return [ButterCMS::Post] def self.call(post_attributes) updated_post_attributes = { 'tags' => ::ButterCMS::Parsers::TagsObjects.call(post_attributes.delete('tags')), 'categories' => ::ButterCMS::Parsers::CategoriesObjects.call(post_attributes.delete('categories')), 'author' => ::ButterCMS::Parsers::AuthorObject.call(post_attributes.delete('author')) } ::ButterCMS::Post.new(post_attributes.merge(updated_post_attributes)) end end end end

The last step is to update lib/butter_cms.rb :

require 'butter_cms/post' require 'butter_cms/parsers/post_object'

We will now move to the fetch service creation. It will be responsible for returning the array of post objects for the given request options. Create tests in spec/lib/butter_cms/posts_fetch_service_spec.rb :

require 'spec_helper' describe ButterCMS::PostsFetchService do describe '#posts' do it 'returns posts from given request' do request_options = { page: 1, per_page: 10 } post_attributes = { title: 'Post title' } parser = instance_double(ButterCMS::Parsers::Posts, posts: [post_attributes]) post = instance_double(ButterCMS::Post) response = double('response') allow(ButterCMS::Requests::Get).to receive(:call).with( "posts", request_options ).and_return(response) allow(ButterCMS::Parsers::Posts).to receive(:new).with(response).and_return(parser) allow(ButterCMS::Parsers::PostObject).to receive(:call).with( post_attributes ).and_return(post) service = described_class.new(request_options) result = service.posts expect(result).to eq([post]) expect(ButterCMS::Parsers::PostObject).to have_received(:call).with( post_attributes ).once expect(ButterCMS::Requests::Get).to have_received(:call).with( "posts", request_options ).once end end end

Create lib/butter_cms/posts_fetch_service.rb :

module ButterCMS class PostsFetchService def initialize(request_options) @request_options = request_options end # Returns array of post objects with the associated records included # # @return [Array<ButterCMS::Post>] def posts parser.posts.map do |post_attributes| ::ButterCMS::Parsers::PostObject.call(post_attributes) end end private attr_reader :request_options def response ::ButterCMS::Requests::Get.call("posts", request_options) end def parser @parser ||= ::ButterCMS::Parsers::Posts.new(response) end end end

Load the new file in lib/butter_cms.rb :

require 'butter_cms/posts_fetch_service'

We also need a method inside ButterCMS::PostsFetchService which will detect if there are any ‘next’ pages to fetch. We will name it #more_posts?:

describe '#more_posts?' do it 'returns true if there are more posts to fetch' do request_options = { page: 1, per_page: 10 } post_attributes = { title: 'Post title' } parser = instance_double(ButterCMS::Parsers::Posts, next_page: 2) response = double('response') allow(ButterCMS::Requests::Get).to receive(:call).with( "posts", request_options ).and_return(response) allow(ButterCMS::Parsers::Posts).to receive(:new).with(response).and_return(parser) service = described_class.new(request_options) result = service.more_posts? expect(result).to eq(true) expect(ButterCMS::Requests::Get).to have_received(:call).with( "posts", request_options ).once end it 'returns false if there are no more posts to fetch' do request_options = { page: 1, per_page: 10 } post_attributes = { title: 'Post title' } parser = instance_double(ButterCMS::Parsers::Posts, next_page: nil) response = double('response') allow(ButterCMS::Requests::Get).to receive(:call).with( "posts", request_options ).and_return(response) allow(ButterCMS::Parsers::Posts).to receive(:new).with(response).and_return(parser) service = described_class.new(request_options) result = service.more_posts? expect(result).to eq(false) expect(ButterCMS::Requests::Get).to have_received(:call).with( "posts", request_options ).once end end

Implementation is simple:

# Returns true if the next page is available, false otherwise # # @return [Boolean] def more_posts? !parser.next_page.nil? end Yes, it's time to move to the ButterCMS::Postclass. We will implement the ButterCMS::Post.allclass which will return all available posts. Create new test spec/lib/butter_cms/post_spec.rb: require 'spec_helper' describe ButterCMS::Post do describe '.all' do it 'returns all available posts' do request_options = { page_size: 10, page: 1 } post = instance_double(ButterCMS::Post) fetch_service = instance_double(ButterCMS::PostsFetchService, posts: [post], more_posts?: true ) allow(ButterCMS::PostsFetchService).to receive(:new).with( request_options ).and_return(fetch_service) another_post = instance_double(ButterCMS::Post) another_request_options = { page_size: 10, page: 2 } another_fetch_service = instance_double(ButterCMS::PostsFetchService, posts: [another_post], more_posts?: false ) allow(ButterCMS::PostsFetchService).to receive(:new).with( another_request_options ).and_return(another_fetch_service) expect(described_class.all).to eq([post, another_post]) end end end

and satisfy the test in the previously created Post class in lib/butter_cms/post.rb :

module ButterCMS class Post < ButterCMS::Resource # Returns all available posts from the API # # @return [Array<ButterCMS::Post>] def self.all posts = [] request_options = { page_size: 10, page: 0 } more_posts = true while more_posts request_options = request_options.merge(page: request_options[:page] + 1) fetch_service = ButterCMS::PostsFetchService.new(request_options) posts = posts | fetch_service.posts more_posts = fetch_service.more_posts? end posts end end end

Now that was a lot of coding!

Find single post

We have code for pulling all posts but we want to also be able to pull only one post using its slug. Slug is a unique string used in the URL to identify given resource. It looks better than the standard id and it provides better positioning in the Google search engine. We need to create a new parser and add a new method to the ButterCMS::Post class. Create a new test in spec/butter_cms/parsers/post.rb and add the following:

require 'spec_helper' describe ButterCMS::Parsers::Post do let(:response_body) do { "meta" => { "count" => 3, "next_page" => 3, "previous_page" => 1 }, "data" => { "url" => "sample-title" } }.to_json end let(:response) { double(body: response_body) } describe '#post' do it 'returns post attributes' do parser = described_class.new(response) expect(parser.post).to eq({ "url" => "sample-title" }) end end end Satisfy it by creating logic in ButterCMS::Parsers::Postclass: module ButterCMS module Parsers class Post < ButterCMS::Parsers::Posts # Returns post attributes # # @return [Hash] def post parsed_json['data'] end end end end

Don’t forget to load the new parser class inside lib/butter_cms.rb :

require 'butter_cms/parsers/post'

Open spec/lib/butter_cms/post_spec.rb and add new a new test for the .find method:

describe '.find' do it 'raises RecordNotFound error when post does not exist for given slug' do allow(ButterCMS::Requests::Get).to receive(:call).with("posts/slug").and_raise(RestClient::NotFound) expect { described_class.find("slug") }.to raise_error(ButterCMS::Post::RecordNotFound) end it 'returns post for the given slug' do response = double('response') post_attributes = { "slug" => "some-slug" } post_object = instance_double(ButterCMS::Post) allow(ButterCMS::Requests::Get).to receive(:call).with("posts/slug").and_return(response) parser = instance_double(ButterCMS::Parsers::Post, post: post_attributes) allow(ButterCMS::Parsers::Post).to receive(:new).with(response).and_return(parser) allow(ButterCMS::Parsers::PostObject).to receive(:call).with(post_attributes).and_return(post_object) result = described_class.find("slug") expect(result).to eq(post_object) expect(ButterCMS::Parsers::PostObject).to have_received(:call).with(post_attributes).once expect(ButterCMS::Requests::Get).to have_received(:call).with("posts/slug").once end end

If post is found we want to return a new instance of ButterCMS::Post. If a post is not found, we want to raise a ButterCMS::Post::RecordNotFound error. Let’s implement it:

class RecordNotFound < StandardError; end # Returns post for given slug if available or raises RecordNotFound error # # @return [ButterCMS::Post] def self.find(slug) response = ::ButterCMS::Requests::Get.call("posts/#{slug}") post_attributes = ::ButterCMS::Parsers::Post.new(response).post ::ButterCMS::Parsers::PostObject.call(post_attributes) rescue RestClient::NotFound raise RecordNotFound end

Testing what we have done so far

Finally we’ve reached the moment that you have been waiting for from the beginning. We can now build our gem, install it locally, and then test it in the irb console.

Build it:

gem build butter_cms.gemspec

Install it:

gem install butter_cms_v2-0.0.0.1.gem

And open interactive ruby console:

irb

As you remember, we have to define the api_key first:

require 'butter_cms' ButterCMS.configure do |c| c.api_key = 'your_api_key' end

You can get your api_key for free after creating an account on buttercms.com.

Now, we can test our posts:

posts = ButterCMS::Post.all post = ButterCMS::Post.find(posts.first.slug) ButterCMS::Post.find("fake-slug")

Categories support

Now that we are sure that our code is working well, we can move to the next step: categories support. We want to add the ability to pull all categories and find a given category by slug.

The categories response is quite simple. It contains only the data attribute and an array of the category attributes. We will start by creating the parser class for the response. Create spec/lib/butter_cms/parsers/categories_spec.rb and the following test:

require 'spec_helper' describe ButterCMS::Parsers::Categories do let(:response_body) do { "data" => [ { "slug" => "sample-title" } ] }.to_json end let(:response) { double(body: response_body) } describe '#categories' do it 'returns categories' do parser = described_class.new(response) expect(parser.categories).to eq([{ "slug" => "sample-title" }]) end end end

We already created similar code in ButterCMS::Parsers::Posts so it will be easier to satisfy the above test. Create lib/butter_cms/parsers/categories.rb :

require 'json' module ButterCMS module Parsers class Categories def initialize(response) @response = response end # Returns array of category attributes available in the response # # @return [Array] def categories parsed_json['data'] end private attr_reader :response def parsed_json @parsed_json ||= ::JSON.parse(response.body) end end end end

Don’t forget to load our parser class inside lib/butter_cms.rb :

require 'butter_cms/parsers/categories'

Now we have to create spec/lib/butter_cms/category_spec.rb and add a test for the ButterCMS::Category.all method:

require 'spec_helper' describe ButterCMS::Category do describe '.all' do it 'returns all categories' do response = double('response') allow(ButterCMS::Requests::Get).to receive(:call).with("categories").and_return(response) attributes = [{"slug" => "some-slug"}] parser = instance_double(ButterCMS::Parsers::Categories, categories: attributes) allow(ButterCMS::Parsers::Categories).to receive(:new).with(response).and_return(parser) category = instance_double(ButterCMS::Category) allow(ButterCMS::Parsers::CategoriesObjects).to receive(:call).with(attributes).and_return([category]) result = described_class.all expect(result).to eq([category]) expect(parser).to have_received(:categories).once expect(ButterCMS::Requests::Get).to have_received(:call).with("categories").once end end end

Update lib/butter_cms/category.rb :

module ButterCMS class Category < ButterCMS::Resource # Returns all categories # # @return [Array<ButterCMS::Category>] def self.all response = ::ButterCMS::Requests::Get.call("categories") attributes = ::ButterCMS::Parsers::Categories.new(response).categories ::ButterCMS::Parsers::CategoriesObjects.call(attributes) end end end

And we are done.

Summary

Our gem is ready for the first release. We implemented the following base functionality:

Setting the API key using nice DSL (Domain Specific Language) - it’s a more human-friendly way of setting different things in the code - in this case variable values. DSL is a one of the metaprogramming advantages

Fetching all posts with the related data included

Fetching a single post

Fetching all categories

Publishing gem

Sign up on rubygems.org if you don’t have an account already. Now, push the gem using the same credentials you used to create your account on the rubygems.org website:

gem push butter_cms_v2-0.0.0.1.gem

Pushing gem to https://rubygems.org...

Successfully registered gem: butter_cms_v2 (0.0.0.1)

Testing our gem with the Ruby on Rails application

Since our gem is published, we can create a new Rails application and see how it works:

rails new butter_cms_test_app cd butter_cms_test_app

Open Gemfile and add our gem:

gem 'butter_cms_v2'

Then run bundle install.

As you remember, we have to add your api_key in order to use ButterCMS API. Let’s create a new initializer config/initializers/butter_cms.rb and add the following contents:

require 'butter_cms' ButterCMS.configure do |c| c.api_key = "your_api_key" end

Now use rails console rails c and pull your blog posts:

ButterCMS::Post.all

And that’s it!

Ideas for future development

There are plenty of ways that you can improve the gem we just built. You can start with implementing support for all API endpoints provided by the ButterCMS or caching API responses to prevent you from fetching the same resource twice. Experiment and try out new things and approaches, this is your place to explore

What is next?

Building a Ruby gem is just the beginning of any developer’s journey. Any piece of software is a work in progress: it's never really finished and there are always ways of improving it. In the next article, we will demonstrate how to promote your gem among the Ruby community and how to write a good README to make it easier to use and and for other developers to contribute to. Stay tuned!