Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 463 lines (274 sloc) 29.461 kb
9fe2f84 @radar Add title to gem development guide
authored
1 # Developing a RubyGem using Bundler
2
fda19eb @radar André with a "cool" e
authored
3 Bundler is a tool created by Carl Lerche, Yehuda Katz, André Arko and various superb contributors for managing Rubygems dependencies in Ruby libraries. Bundler 1.0 was released around the same time as Rails 3 and it's the Rails project where Bundler is probably most well-known usage occurs. But remember, Bundler isn't just for Rails!
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
4
5 Did you know that you can use Bundler for not only gem dependency management but also for writing our own gems? It's really easy to do this and Bundler provides a couple of things to help you along this path.
6
7 ## But first, why?
8
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
9 Why should we create a gem? Can't we just throw in some code into our *other* library and use that instead? Sure, we can do that. But then what if we want to use the code elsewhere, or we want to share it? This is why a gem is perfect. We can code our library and gem separately from each other and just have the library require the gem. If we want to use the gem in another library, then it's just a tiny modification rather than a whole slew of copying.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
10
11 Also: Sharing is caring.
12
13 ## Getting Started
14
15 To begin to create a gem using Bundler, use the `bundle gem` command like this:
16
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
17 bundle gem foodie
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
18
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
19 We call our gem `foodie` because this gem is going to do a couple of things around food, such as portraying them as either "Delicious!" or "Gross!". Stay tuned.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
20
90928d4 @sumbach Improve noun/verb and verb/verb agreement; minor punctuation changes
sumbach authored
21 This command creates a [scaffold directory](gem-scaffold/foodie) for our new gem and, if we have Git installed, initializes a Git repository in this directory so we can start committing right away. The files generated are:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
22
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
23 * [**Gemfile**](gem-scaffold/foodie/Gemfile): Used to manage gem dependencies for our library's development. This file contains a `gemspec` line meaning that Bundler will include dependencies specified in _foodie.gemspec_ too. It's best practice to specify all the gems that our library depends on in the _gemspec_.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
24
ac34d19 @martinos Adding gem-scaffold directory and changing links
martinos authored
25 * [**Rakefile**](gem-scaffold/foodie/Rakefile): Requires Bundler and adds the `build`, `install` and `release` Rake tasks by way of calling _Bundler::GemHelper.install\_tasks_. The `build` task will build the current version of the gem and store it under the _pkg_ folder, the `install` task will build _and_ install the gem to our system (just like it would do if we `gem install`'d it) and `release` will push the gem to Rubygems for consumption by the public.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
26
ac34d19 @martinos Adding gem-scaffold directory and changing links
martinos authored
27 * [**.gitignore**](gem-scaffold/foodie/.gitignore): (only if we have Git). This ignores anything in the _pkg_ directory (generally files put there by `rake build`), anything with a _.gem_ extension and the _.bundle_ directory.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
28
ac34d19 @martinos Adding gem-scaffold directory and changing links
martinos authored
29 * [**foodie.gemspec**](gem-scaffold/foodie/foodie.gemspec): The Gem Specification file. This is where we provide information for Rubygems' consumption such as the name, description and homepage of our gem. This is also where we specify the dependencies our gem needs to run.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
30
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
31 * [**lib/foodie.rb**](gem-scaffold/foodie/lib/foodie.rb): The main file to define our gem's code. This is the file that will be required by Bundler (or any similarly smart system) when our gem is loaded. This file defines a `module` which we can use as a namespace for all our gem's code. It's best practice to put our code in...
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
32
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
33 * [**lib/foodie**](gem-scaffold/foodie/lib/foodie): here. This folder should contain all the code (classes, etc.) for our gem. The _lib/foodie.rb_ file is there for setting up our gem's environment, whilst all the parts of it go in this folder. If our gem has multiple uses, separating this out so that people can require one class/file at a time can be really helpful.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
34
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
35 * [**lib/foodie/version.rb**](gem-scaffold/foodie/lib/foodie/version.rb): Defines a `Foodie` module and in it, a `VERSION` constant. This file is loaded by the _foodie.gemspec_ to specify a version for the gem specification. When we release a new version of the gem we will increment a part of this version number to indicate to Rubygems that we're releasing a new version.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
36
37 There's our base and our layout, now get developing!
38
39 ## Testing our gem
40
41 For this guide, we're going to use RSpec to test our gem. We write tests to ensure that everything goes according to plan and to prevent future-us from building a time machine to come back and kick our asses.
42
3876d4a @radar Use add_development_dependency rather than adding them to the Gemfile…
authored
43 To get started with writing our tests, we'll create a _spec_ directory at the root of gem by using the command `mkdir spec`. Next, we'll specify in our _foodie.gemspec_ file that `rspec` is a development dependency by adding this line inside the `Gem::Specification` block:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
44
6ddcb86 Update rspec to the latest version (~> 2.6)
Jason Noble & Mike Gehard authored
45 s.add_development_dependency "rspec", "~> 2.6"
3876d4a @radar Use add_development_dependency rather than adding them to the Gemfile…
authored
46
90928d4 @sumbach Improve noun/verb and verb/verb agreement; minor punctuation changes
sumbach authored
47 Because we have the `gemspec` method call in our _Gemfile_, Bundler will automatically add this gem to a group called "development" which then we can reference any time we want to load these gems with the following line:
3876d4a @radar Use add_development_dependency rather than adding them to the Gemfile…
authored
48
49 Bundler.require(:default, :development)
50
51 The benefit of putting this dependency specification inside of _foodie.gemspec_ rather than the _Gemfile_ is that anybody who runs `gem install foodie --dev` will get these development dependencies installed too. This command is used for when people wish to test a gem without having to fork it or clone it from GitHub.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
52
53 When we run `bundle install`, rspec will be installed for this library and any other library we use with Bundler, but not for the system. This is an important distinction to make: any gem installed by Bundler will not muck about with gems installed by `gem install`. It is effectively a sandboxed environment. It is best practice to use Bundler to manage our gems so that we do not have gem version conflicts.
54
90928d4 @sumbach Improve noun/verb and verb/verb agreement; minor punctuation changes
sumbach authored
55 By running `bundle install`, Bundler will generate the **extremely important** _Gemfile.lock_ file. This file is responsible for ensuring that every system this library is developed on has the *exact same* gems so it should always be checked into version control. For more information on this file [read "THE GEMFILE.LOCK" section of the `bundle install` manpage](https://github.com/carlhuda/bundler/blob/1-0-stable/man/bundle-install.ronn#L226-246).
b90cae6 @radar Mention that the Gemfile.lock file is really, really, really, really,…
authored
56
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
57 Additionally in the `bundle install` output, we will see this line:
58
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
59 Using foodie (0.0.1) from source at /path/to/foodie
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
60
90928d4 @sumbach Improve noun/verb and verb/verb agreement; minor punctuation changes
sumbach authored
61 Bundler detects our gem, loads the gemspec and bundles our gem just like every other gem.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
62
90928d4 @sumbach Improve noun/verb and verb/verb agreement; minor punctuation changes
sumbach authored
63 We can write our first test with this framework now in place. For testing, first we create a folder called _spec_ to put our tests in (`mkdir spec`). We then create a new RSpec file for every class we want to test at the root of the _spec_ directory. If we had multiple facets to our gem, we would group them underneath a directory such as _spec/facet_; but this is a simple gem, so we won't. Let's call this new file _spec/foodie_spec.rb_ and fill it with the following:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
64
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
65 describe Foodie::Food do
fc31576 @radar Switch to using food-related examples to have a running theme
authored
66 it "broccoli is gross" do
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
67 Foodie::Food.portray("Broccoli").should eql("Gross!")
fc31576 @radar Switch to using food-related examples to have a running theme
authored
68 end
69
70 it "anything else is delicious" do
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
71 Foodie::Food.portray("Not Broccoli").should eql("Delicious!")
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
72 end
73 end
74
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
75 When we run `bundle exec rspec spec` again, we'll be told the `Foodie::Food` constant doesn't exist. This is true, and we should define it in _lib/foodie/food.rb_ like this:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
76
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
77 module Foodie
fc31576 @radar Switch to using food-related examples to have a running theme
authored
78 class Food
fb74521 @radar portrayal -> portray. thing -> word
authored
79 def self.portray(food)
fc31576 @radar Switch to using food-related examples to have a running theme
authored
80 if food.downcase == "broccoli"
81 "Gross!"
82 else
a109f00 @radar foodie.rb should be outputting "Delicious!"
authored
83 "Delicious!"
fc31576 @radar Switch to using food-related examples to have a running theme
authored
84 end
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
85 end
86 end
87 end
88
89 We can then require this file at the top of our spec file by using this line:
90
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
91 require 'foodie/food'
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
92
93 When we run our specs with `bundle exec rspec spec` this test will pass:
94
5b8f6c6 @radar There are two examples at this point, not one
authored
95 2 example, 0 failures
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
96
97 Great success! If we're using Git (or any other source control system), this is a great checkpoint to commit our code. Always remember to commit often!
98
99 It's all well and dandy that we can write our own code, but what if we want to depend on another gem? That's easy too.
100
101 ## Using other gems
102
fc31576 @radar Switch to using food-related examples to have a running theme
authored
103 We're now going to use Active Support's `pluralize` method by calling it using a method from our gem.
104
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
105 To use another gem, we must first specify it as a dependency in our _foodie.gemspec_. We can specify the dependency on the `activesupport` gem in _foodie.gemspec_ by adding this line inside the `Gem::Specification` object:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
106
107 s.add_dependency "activesupport", "3.0.0"
108
109 If we wanted to specify a particular version we may use this line:
110
111 s.add_dependency "activesupport", ">= 2.3.8"
112
113 However, relying on a version simply greater than the latest-at-the-time is a sure-fire way to run into problems later on down the line. Try to always use `~>` for specifying dependencies.
114
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
115 When we run `bundle install` again, the `activesupport` gem will be installed for us to use. Of course, like the diligent TDD/BDD zealots we are, we will test our `pluralize` method before we code it. Let's add this test to _spec/food\_spec.rb_ now inside our `describe Foodie::Food` block:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
116
fb74521 @radar portrayal -> portray. thing -> word
authored
117 it "pluralizes a word" do
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
118 Foodie::Food.pluralize("Tomato").should eql("Tomatoes")
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
119 end
120
121 Of course when we run this spec with `bundle exec rspec spec` it will fail:
122
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
123 Failure/Error: Foodie::Food.pluralize("Tomato").should eql("Tomatoes")
124 undefined method `pluralize' for Foodie::Food:Class
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
125
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
126 We can now define this `pluralize` method in _lib/foodie/food.rb_ by first off requiring the part of Active Support which contains the `pluralize` method. This line should go at the top of the file, just like all good `require`s do.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
127
128 require 'active_support/inflector'
129
fb74521 @radar portrayal -> portray. thing -> word
authored
130 Next, we can define the `pluralize` method like this:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
131
fb74521 @radar portrayal -> portray. thing -> word
authored
132 def self.pluralize(word)
133 word.pluralize
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
134 end
135
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
136 When we run `bundle exec rspec spec` our specs will pass:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
137
fb74521 @radar portrayal -> portray. thing -> word
authored
138 ...
139 3 examples, 0 failures
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
140
141 This brings another checkpoint where it'd be a good idea to commit our efforts so far.
142
143 It's great that we're able to call our gem's methods now (all two of them!) and get them to return strings, but everybody knows that the best gems come with command line interfaces (hereafter, "CLI"). You can tell right now just how uncool this gem is because it doesn't have a CLI, right? It needs one. It craves one.
144
145 It deserves one.
146
147 ## Testing a command line interface
148
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
149 Before we go jumping headlong into giving our gem the best darn CLI a gem-with-only-two-methods-that-both-return-useless-strings can have, let's consider how we're going to test this first. We're zealots, remember? Now if only there was a tool we could use. It would have to have a cool name, of course.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
150
318ca31 @JoshCheek Link to Aruba appears to have changed.
JoshCheek authored
151 Like "Aruba". [BAM](https://github.com/cucumber/aruba)
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
152
153 David Chelimsky and Aslak Hellesøy teamed up to create Aruba, a CLI testing tool, which they both use for RSpec and Cucumber, and now we too can use it for testing our gems. Oh hey, speaking of Cucumber that's also what we're going to be using to define the Aruba tests. Human-code-client-readable tests are the way of the future, man.
154
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
155 We will define new development dependencies in _foodie.gemspec_ now for the Cucumber things:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
156
3876d4a @radar Use add_development_dependency rather than adding them to the Gemfile…
authored
157 s.add_development_dependency "cucumber"
158 s.add_development_dependency "aruba"
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
159
160 Hot. Let's run `bundle install` to get these awesome tools set up.
161
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
162 Our CLI is going to have two methods, which correspond to the two methods which we have defined in `Foodie::Food`. We will now create a _features_ directory where we will make sweet, sweet love to Aruba to write tests for our CLI. In this directory we'll create a new file called _features/food.feature_ and fill it with this juicy code:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
163
fc31576 @radar Switch to using food-related examples to have a running theme
authored
164 Feature: Food
165 In order to portray or pluralize food
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
166 As a CLI
fc31576 @radar Switch to using food-related examples to have a running theme
authored
167 I want to be as objective as possible
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
168
fc31576 @radar Switch to using food-related examples to have a running theme
authored
169 Scenario: Broccoli is gross
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
170 When I run "foodie portray broccoli"
fc31576 @radar Switch to using food-related examples to have a running theme
authored
171 Then the output should contain "Gross!"
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
172
fc31576 @radar Switch to using food-related examples to have a running theme
authored
173 Scenario: Tomato, or Tomato?
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
174 When I run "foodie pluralize --word Tomato"
fc31576 @radar Switch to using food-related examples to have a running theme
authored
175 Then the output should contain "Tomatoes"
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
176
177 These scenarios test the CLI our gem will provide. In the `When I run` steps, the first word inside the quotes is the name of our executable, the second is the task name, and any further text is arguments or options. Yes, it *is* testing what appears to be the same thing as our specs. How very observant of you. Gold star! But it's testing it through a CLI, which makes it *supremely awesome*. Contrived examples are _in_ this year.
178
179 The first scenario ensures that we can call a specific task and pass it a single argument which then becomes the part of the text that is output. The second scenario ensures effectively the same thing, but we pass that value in as an option rather than an argument.
180
181 To run this feature, we use the `cucumber` command, but of course because it's available within the context of our bundle, we use `bundle exec cucumber` like this:
182
183 bundle exec cucumber features/
184
185 See those yellow things? They're undefined steps:
186
187 When /^I run "([^"]*)"$/ do |arg1|
188 pending # express the regexp above with the code you wish you had
189 end
190
191 Then /^the output should contain "([^"]*)"$/ do |arg1|
192 pending # express the regexp above with the code you wish you had
193 end
194
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
195 We can define them by requiring Aruba. In Cucumber, all _.rb_ files in the _features/support_ directory are automatically required. To prove this to ourselves, we can add a _features/support/setup.rb_ file (create the _support_ directory first) and put in this single line:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
196
d5dfdab @radar Change aruba require to aruba/cucumber. Fixes GH-10 [rberger]
authored
197 require 'aruba/cucumber'
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
198
d5dfdab @radar Change aruba require to aruba/cucumber. Fixes GH-10 [rberger]
authored
199 This loads the Cucumber steps provided by Aruba which are the same steps our Cucumber features need to be awesome.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
200
201 We have to re-run `bundle exec cucumber features`, just to see what happens next. We see red. Red like the blood incessantly seeping from the walls. It contains this cryptic message:
202
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
203 sh: foodie: command not found
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
204
6a75e46 @radar Be explicit about where the bin directory is.
authored
205 OK, so it's not *that* cryptic. It just means it can't find the executable file for our gem. No worries, we can create a _bin_ directory at the root of our gem, and put a file in it named _foodie_. This file has no extension because it's an *executable* file rather than a script. We don't want to go around calling `foodie.rb` everywhere, do we? No, no we don't. We will fill this file with this content:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
206
207 #!/usr/bin/env ruby
208 print "nothing."
209
b6ccd64 @radar Added notes for making the executable, executable.
authored
210 If this file was completely empty, we would run into a non-friendly `Errno::ENOEXEC` error. Hey, speaking of running, we should `chmod` this file to be an executable from our terminal:
211
212 chmod +x bin/foodie
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
213
214 Alright so we've got the executable file, now what? If we re-run our features we get *nothing* for the output. Nothing! Literally!
215
216 got: "nothing."
217
218
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
219 Our _bin/foodie_ file is empty, which results in this Nothing Travesty. Get rid of the `print "nothing."` line and replace it with all the code required to run our CLI, which consists of two lines:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
220
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
221 require 'foodie/cli'
222 Foodie::CLI.start
90c2bac @radar Writing a recipe generator
authored
223
224
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
225 Boom! When we run `bundle exec cucumber features` again it will whinge that there's no _foodie/cli_ file to require. Before we go into what this file does, we should explain the code on the _other_ line of the _bin/foodie_ file. The `start` method fires up our `CLI` class and will look for a task that matches the one we ask for.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
226
90c2bac @radar Writing a recipe generator
authored
227 Ok, so it's therefore obvious that the next step is to create this file, but what does it do?
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
228
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
229 This new _lib/foodie/cli.rb_ file will define the command line interface using another gem called `Thor`. Thor was created by Yehuda Katz (& collaborators) as an alternative to the Rake build tool. Thor provides us with a handy API for defining our CLI, including usage banners and help output. The syntax is very similar to Rake. Additionally, Rails and Bundler both use Thor for their CLI interface as well as their generator base. Yes, Thor even does generators!
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
230
41b5d95 @radar We write a generator, not a "generation"
authored
231 For now we'll just look at how we can craft a CLI using Thor and then afterwards, if you behave, we'll look at how to write a generator using it too.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
232
233 ## Crafting a CLI
234
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
235 To make this CLI work we're going to need to create a `Foodie::CLI` class and define a `start` method on it. Or you know, there's probably a gem out there for us to use. Like [Thor](http://github.com/wycats/thor). Named after the badass lightning god from Norse mythology, this gem is definitely on the fast-track to being just as badass. This gem is what we'll be using to build our CLI interface and then later on the generator (if you behave, remember?).
6b3a702 @radar Fix segue between "Crafting a CLI" section and its previous
authored
236
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
237 Let's define the _lib/foodie/cli.rb_ file now like this:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
238
6b3a702 @radar Fix segue between "Crafting a CLI" section and its previous
authored
239 require 'thor'
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
240 module Foodie
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
241 class CLI < Thor
242
243 end
244 end
245
6b3a702 @radar Fix segue between "Crafting a CLI" section and its previous
authored
246 The `Thor` class has a series of methods -- such as the `start` method we reference back in `bin/foodie` -- that we can use to create this CLI. Oh, by the way, our class doesn't have to be called `CLI`, it's just best practice to do so. We don't magically get this `Thor` class; we need to tell our _gemspec_ that we depend on this gem by adding this line underneath our previous _add\_dependency_:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
247
248 s.add_dependency "thor"
249
250 To install this new dependency, we use `bundle install`. When we run `bundle exec cucumber features` again, we'll see that it's now complaining that it could not find the tasks we're calling:
251
fc31576 @radar Switch to using food-related examples to have a running theme
authored
252 Could not find task "portray"
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
253 ...
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
254 Could not find task "pluralize"
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
255
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
256 Thor tasks are defined as plain ol' methods, but with a slight twist. To define the `portray` task in our `Foodie::CLI` class we will write this inside the `Foodie::CLI` class:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
257
fc31576 @radar Switch to using food-related examples to have a running theme
authored
258 desc "portray ITEM", "Determines if a piece of food is gross or delicious"
259 def portray(name)
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
260 puts Foodie::Food.portray(name)
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
261 end
262
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
263 The `desc` method is the "slight twist" here. The method defined after it becomes a task with the given description. The first argument for `desc` is the usage instructions for the task whilst the second is the short description of what that task accomplishes. The `portray` method is defined with a single argument, which will be the first argument passed to this task on the command line. Inside the `portray` method we call `Foodie::Food.portray` and pass it this argument.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
264
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
265 In the `Foodie::CLI` class we're referencing the `Foodie::Food` class without requiring the file that defines it. Under the `require 'thor'` at the top of this file, put this line to require the file that defines `Foodie::Food`:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
266
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
267 require 'foodie/food'
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
268
269 When we re-run our features using `bundle exec cucumber features` our first scenario will pass:
270
271 2 scenarios (1 failed, 1 passed)
272 4 steps (1 failed, 3 passed)
273
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
274 The second is still failing because we haven't defined the `pluralize` task. This time rather than defining a task that takes an argument, we'll define a task that reads in the value from an option passed to the task. To define the `pluralize` task we use this code in `Foodie::CLI`:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
275
276
fc31576 @radar Switch to using food-related examples to have a running theme
authored
277 desc "pluralize", "Pluralizes a word"
417fe26 @radar method_option is used like this
authored
278 method_option :word, :aliases => "-w"
fc31576 @radar Switch to using food-related examples to have a running theme
authored
279 def pluralize
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
280 puts Foodie::Food.pluralize(options[:word])
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
281 end
282
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
283 Here there's the new `method_option` method we use which defines, well, a method option. It takes a hash which indicates the details of an option how they should be returned to our task. Check out the Thor README for a full list of valid types. We can also define aliases for this method using the `:aliases` option passed to `method_option`. Inside the task we reference the value of the options through the `options` hash and we use `Foodie::Food.pluralize` to pluralize a word.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
284
285 When we run our scenarios again with `bundle exec cucumber features` both scenarios will be passing:
286
287 2 scenarios (2 passed)
288 4 steps (4 passed)
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
289
90928d4 @sumbach Improve noun/verb and verb/verb agreement; minor punctuation changes
sumbach authored
290 We can try executing the CLI app by running `bundle exec bin/foodie portray broccoli`.
9a2068a @radar Explain that we can specify multiple options by using the method_opti…
authored
291
292 If we want to add more options later on, we can define them by using the `method_options` helper like this:
293
294 method_options :word => :string, :uppercase => :boolean
295 def pluralize
296 # accessed as options[:word], options[:uppercase]
297 end
298
299 In this example, `options[:word]` will return a `String` object, whilst `options[:uppercase]` will return either `true` or `false`, depending on the value it has received.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
300
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
301 This introduction should have whet your appetite to learn more about Thor and it's encouraged that you do that now. Check out `Bundler::CLI` for a great example of using Thor as a CLI tool.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
302
303 With our features and specs all passing now, we're at a good point to commit our code.
304
fc31576 @radar Switch to using food-related examples to have a running theme
authored
305 It was aforementioned that we could use Thor for more than just CLI. That we could use it to create a generator. This is true. We can even create generator*s*, but let's not get too carried away right now and just focus on creating the one.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
306
90c2bac @radar Writing a recipe generator
authored
307 ## Testing a generator
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundle…
authored
308
90c2bac @radar Writing a recipe generator
authored
309 You saw that pun coming, right? Yeah, pretty obvious.
310
311 We're going to mix it up a bit and add a new feature to our gem: a generator for a _recipes_ directory. The idea is that we can run our generator like this:
312
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
313 foodie recipe dinner steak
90c2bac @radar Writing a recipe generator
authored
314
315 This will generate a _recipes_ directory at the current location, a _dinner_ directory inside that and then a _steak.txt_ file inside that. This _steak.txt_ file will contain the scaffold for a recipe, such as the ingredients and the instructions.
316
317 Thankfully for us, Aruba has ways of testing that a generator generates files and directories. Let's create a new file called _features/generator.feature_ and fill it with this content:
318
319 Feature: Generating things
fb74521 @radar portrayal -> portray. thing -> word
authored
320 In order to generate many a thing
321 As a CLI newbie
1269cf8 @radar Further information about how to create a generator.
authored
322 I want foodie to hold my hand, tightly
90c2bac @radar Writing a recipe generator
authored
323
324 Scenario: Recipes
1269cf8 @radar Further information about how to create a generator.
authored
325 When I run "foodie recipe dinner steak"
90c2bac @radar Writing a recipe generator
authored
326 Then the following files should exist:
327 | dinner/steak.txt |
328 Then the file "dinner/steak.txt" should contain:
329 """
1269cf8 @radar Further information about how to create a generator.
authored
330 ##### Ingredients #####
0847492 @radar Mention that the `template` method treats files like an ERB template,…
authored
331 Ingredients for delicious steak go here.
1269cf8 @radar Further information about how to create a generator.
authored
332
333
334 ##### Instructions #####
0847492 @radar Mention that the `template` method treats files like an ERB template,…
authored
335 Tips on how to make delicious steak go here.
90c2bac @radar Writing a recipe generator
authored
336 """
0847492 @radar Mention that the `template` method treats files like an ERB template,…
authored
337
338 It's important to note that the word after "delicious" both times is "steak", which is *very* delicious. It's also the last argument we pass in to the command that we run, and therefore should be a dynamic variable in our template. We'll see how to do this soon.
339
340 When we run this feature we'll be told that there's an undefined step and a failing scenario; we'll look at the undefined step first. Aruba currently doesn't have a step itself defined for multi-line file content matching, so we will define one ourselves inside _features/step\_definitions/aruba\_ext\_steps.rb_ using Aruba's own helpers:
1269cf8 @radar Further information about how to create a generator.
authored
341
342
343 Then /^the file "([^"]*)" should contain:$/ do |file, content|
344 check_file_content(file, content, true)
345 end
346
66b311b @radar Complete gem development guide, first draft.
authored
347 Now for our failure. It's saying that it cannot find the _dinner/steak.txt_ file that we asked the generator to do. Why not?
348
349 ## Writing a generator
350
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
351 Well, because currently we don't have a `recipe` task that does this for us defined in `Foodie::CLI`. We can define a generator class just like we define a CLI class:
1269cf8 @radar Further information about how to create a generator.
authored
352
353 desc "recipe", "Generates a recipe scaffold"
354 def recipe(group, name)
66b311b @radar Complete gem development guide, first draft.
authored
355 Foodie::Generators::Recipe.start([group, name])
1269cf8 @radar Further information about how to create a generator.
authored
356 end
357
66b311b @radar Complete gem development guide, first draft.
authored
358 The first argument for this method are the arguments passed to the generator. We will need to require the file for this new class too, which we can do by putting this line at the top of _lib/foodie/cli.rb_:
359
360 require 'foodie/generators/recipe'
361
362 To define this class, we inherit from `Thor::Group` rather than `Thor`. We will also need to include the `Thor::Actions` module to define helper methods for our generator which include the likes of those able to create files and directories. Because this is a generator class, we will put it in a new namespace called "generators", making the location of this file _lib/foodie/generators/recipe.rb_:
363
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
364 require 'thor/group'
365 module Foodie
366 module Generators
367 class Recipe < Thor::Group
368 include Thor::Actions
66b311b @radar Complete gem development guide, first draft.
authored
369
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
370 argument :group, :type => :string
371 argument :name, :type => :string
372 end
373 end
374 end
1269cf8 @radar Further information about how to create a generator.
authored
375
66b311b @radar Complete gem development guide, first draft.
authored
376 By inheriting from `Thor::Group`, we're defining a generator rather than a CLI. When we call `argument`, we are defining arguments for our generator. These are the same arguments in the same order they are passed in from the `recipe` task back in `Foodie::CLI`
377
4dfaa1f @sumbach Fix minor typos and improve word ordering
sumbach authored
378 To make this generator, ya know, generate stuff we simply define methods in the class. All methods defined in a `Thor::Group` descendant will be run when `start` is called on it. Let's define a `create_group` method inside this class which will create a directory using the name we have passed in.
1269cf8 @radar Further information about how to create a generator.
authored
379
66b311b @radar Complete gem development guide, first draft.
authored
380 def create_group
381 empty_directory(group)
1269cf8 @radar Further information about how to create a generator.
authored
382 end
66b311b @radar Complete gem development guide, first draft.
authored
383
e0ff59f @radar Update guide to explain that template will parse files using ERB
authored
384 To put the file in this directory and to save our foodie-friends some typing, we will use the `template` method. This will copy over a file from a pre-defined source location and evaluate it as if it were an ERB template. We will define a `copy_recipe` method to do this now:
66b311b @radar Complete gem development guide, first draft.
authored
385
386 def copy_recipe
387 template("recipe.txt", "#{group}/#{name}.txt")
388 end
e0ff59f @radar Update guide to explain that template will parse files using ERB
authored
389
390 If we had any ERB calls in this file, they would be evaluated and the result would be output in the new template file.
66b311b @radar Complete gem development guide, first draft.
authored
391
392 It's been an awful long time since we ran something. Hey, here's an idea! Let's run our generator! We can do this without using Cucumber by running `bundle exec bin/foodie recipe dinner steak`, but just this once. Generally we'd test it solely through Cucumber. When we run this command we'll be told all of this:
393
394 create dinner
395 Could not find "recipe.txt" in any of your source paths. Please invoke Foodie::Generators::Recipe.source_root(PATH) with the PATH containing your templates. Currently you have no source paths.
1269cf8 @radar Further information about how to create a generator.
authored
396
66b311b @radar Complete gem development guide, first draft.
authored
397 The first line tells us that the _dinner_ directory has been created. Nothing too fancy there.
398
399 The second line is more exciting though! It's asking us to define the `source_root` method for our generator. That's easy! We can define it as a class method in `Foodie::Generators::Recipe` like this:
400
401 def self.source_root
402 File.dirname(__FILE__) + "/recipe"
403 end
404
405 This tells our generator where to find the template. Now all we need to do is to create the template, which we can put at _lib/foodie/generators/recipe/recipe.txt_:
406
407 ##### Ingredients #####
0847492 @radar Mention that the `template` method treats files like an ERB template,…
authored
408 Ingredients for delicious <%= name %> go here.
66b311b @radar Complete gem development guide, first draft.
authored
409
410
411 ##### Instructions #####
0847492 @radar Mention that the `template` method treats files like an ERB template,…
authored
412 Tips on how to make delicious <%= name %> go here.
413
414 When we use the `template` method, the template file is treated like an ERB template which is evaluated within the current `binding` which means that it has access to the same methods and variables as the method that calls it.
66b311b @radar Complete gem development guide, first draft.
authored
415
0847492 @radar Mention that the `template` method treats files like an ERB template,…
authored
416 And that's all! When we run `bundle exec cucumber features` all our features will be passing!
66b311b @radar Complete gem development guide, first draft.
authored
417
418 3 scenarios (3 passed)
419 7 steps (7 passed)
420
0847492 @radar Mention that the `template` method treats files like an ERB template,…
authored
421 Amazing stuff, hey?
422
66b311b @radar Complete gem development guide, first draft.
authored
423 ## Releasing the gem
424
aef425a @radar Mention that files need to be added to a repository before they can b…
authored
425 If we haven't already, we should commit all the files for our repository:
426
427 git add .
428 git commit -m "The beginnings of the foodie gem"
429
430 This is because the `foodie.gemspec` file uses `git ls-files` to detect which files should be added to the gem when we release it.s
431
66b311b @radar Complete gem development guide, first draft.
authored
432 The final step before releasing our gem is to give it a summary and description in the _foodie.gemspec_ file.
433
434 To release the first version of our gem we can use the `rake release` command, providing we have committed everything. This command does a couple of things. First it builds the gem to the _pkg_ directory in preparation for a push to Rubygems.org.
435
90928d4 @sumbach Improve noun/verb and verb/verb agreement; minor punctuation changes
sumbach authored
436 Second, it creates a tag for the current commit reflecting the current version and pushes it to the git remote. It's encouraged that we host the code on GitHub so that others may easily find it.
66b311b @radar Complete gem development guide, first draft.
authored
437
438 If this push succeeds then the final step will be the push to Rubygems.org which will now allow other people to download and install the gem.
439
90928d4 @sumbach Improve noun/verb and verb/verb agreement; minor punctuation changes
sumbach authored
440 If we want to release a second version of our gem, we should make our changes and then commit them to GitHub. Afterwards, we will bump the version number in _lib/foodie/version.rb_ to whatever we see fit, make another commit to GitHub with a useful message such as "bumped to 0.0.2" and then run `rake release` again.
49ccef8 @radar Mention the way of how to correctly release a new version of a gem.
authored
441
e9e4cbd @radar Cover using the very useful gem-release gem by Sven Fuchs
authored
442 If we want to make this process a little easier we could install the "gem-release" gem with:
443
444 gem install gem-release
445
446 This gem provides several methods for helping with gem development in general, but most helpful is the `gem bump` command which will bump the gem version to the next patch level. This method also takes options to do these things:
447
448 gem bump --to minor # bumps to the next minor version
449 gem bump --to major # bumps to the next major version
450 gem bump --to 1.1.1 # bumps to the specified version
451
452 For more information, check out the ["gem-release" GitHub repository homepage](http://github.com/svenfuchs/gem-release).
453
66b311b @radar Complete gem development guide, first draft.
authored
454 ## Summary
455
456 Whilst this isn't an _exhaustive_ guide on developing a gem, it covers the basics needed for gem development. It's really, _really_ recommended that you check out the source for Bundler, Rails and RSpec for great examples of gem development.
457
458 **If you've found any errors for this guide or if you have any suggestions, please file an issue on http://github.com/radar/guides.**
340082c @radar Link to source code example at the end of the gem-development guide
authored
459
79c6b41 @radar Thank Andre for his help
authored
460 **I'd like to thank [Andre Arko](http://github.com/indirect) for his involvement in the Bundler project and for answering my questions about it. Without his help, this guide would have been difficult to write.**
461
340082c @radar Link to source code example at the end of the gem-development guide
authored
462 ** If you're looking for the complete source code for this example it can be found [here](http://github.com/radar/guides/tree/master/gem-development/foodie)**
Something went wrong with that request. Please try again.