Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 465 lines (275 sloc) 29.696 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, Bundler a...
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
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 a 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.
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, Bundler a...
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, Bundler a...
authored
20
ac34d19 @martinos Adding gem-scaffold directory and changing links
martinos 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, Bundler a...
authored
22
cd882f9 @radar Remove extra space after "Gemfile"
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 the gems that our library depends on all in the _gemspec_.
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
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, Bundler a...
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, Bundler a...
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, Bundler a...
authored
30
ac34d19 @martinos Adding gem-scaffold directory and changing links
martinos 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 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, Bundler a...
authored
32
ac34d19 @martinos Adding gem-scaffold directory and changing links
martinos 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 to 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, Bundler a...
authored
34
ac34d19 @martinos Adding gem-scaffold directory and changing links
martinos authored
35 * [**lib/foodie/version.rb**](gem-scaffold/foodie/lib/foodie/version.rb): Defines a `Foodie` constant 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, Bundler a...
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. T...
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, Bundler a...
authored
44
3876d4a @radar Use add_development_dependency rather than adding them to the Gemfile. T...
authored
45 s.add_development_dependency "rspec", "~> 2.0.0.beta.22"
46
47 Because we have the `gemspec` method call in our _Gemfile_, Bundler will add this gem automatically to a group called "development" which then we can reference any time we want to load these gems with this line:
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, Bundler a...
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
b90cae6 @radar Mention that the Gemfile.lock file is really, really, really, really, RE...
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 and 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).
56
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
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, Bundler a...
authored
60
61 Bundler has detected our gem and has loaded the gemspec and our gem is bundled just like every other gem.
62
63 To run the `rspec` command for our bundle, we must use `bundle exec rspec`. This will use the bundled version of rspec rather than the system version. We can run it now by running `bundle exec rspec spec` to test precisely nothing. At least it works, right?
64
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
65 We can write our first test with this framework now in place. For testing, we 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 this content:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
66
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
67 describe Foodie::Food do
fc31576 @radar Switch to using food-related examples to have a running theme
authored
68 it "broccoli is gross" do
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
69 Foodie::Food.portray("Broccoli").should eql("Gross!")
fc31576 @radar Switch to using food-related examples to have a running theme
authored
70 end
71
72 it "anything else is delicious" do
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
73 Foodie::Food.portray("Not Broccoli").should eql("Delicious!")
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
74 end
75 end
76
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
77 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, Bundler a...
authored
78
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
79 module Foodie
fc31576 @radar Switch to using food-related examples to have a running theme
authored
80 class Food
fb74521 @radar portrayal -> portray. thing -> word
authored
81 def self.portray(food)
fc31576 @radar Switch to using food-related examples to have a running theme
authored
82 if food.downcase == "broccoli"
83 "Gross!"
84 else
a109f00 @radar foodie.rb should be outputting "Delicious!"
authored
85 "Delicious!"
fc31576 @radar Switch to using food-related examples to have a running theme
authored
86 end
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
87 end
88 end
89 end
90
91 We can then require this file at the top of our spec file by using this line:
92
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
93 require 'foodie/food'
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
94
95 When we run our specs with `bundle exec rspec spec` this test will pass:
96
5b8f6c6 @radar There are two examples at this point, not one
authored
97 2 example, 0 failures
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
98
99 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!
100
101 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.
102
103 ## Using other gems
104
fc31576 @radar Switch to using food-related examples to have a running theme
authored
105 We're now going to use Active Support's `pluralize` method by calling it using a method from our gem.
106
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
107 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, Bundler a...
authored
108
109 s.add_dependency "activesupport", "3.0.0"
110
111 If we wanted to specify a particular version we may use this line:
112
113 s.add_dependency "activesupport", ">= 2.3.8"
114
115 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.
116
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
117 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, Bundler a...
authored
118
fb74521 @radar portrayal -> portray. thing -> word
authored
119 it "pluralizes a word" do
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
120 Foodie::Food.pluralize("Tomato").should eql("Tomatoes")
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
121 end
122
123 Of course when we run this spec with `bundle exec rspec spec` it will fail:
124
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
125 Failure/Error: Foodie::Food.pluralize("Tomato").should eql("Tomatoes")
126 undefined method `pluralize' for Foodie::Food:Class
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
127
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
128 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, Bundler a...
authored
129
130 require 'active_support/inflector'
131
fb74521 @radar portrayal -> portray. thing -> word
authored
132 Next, we can define the `pluralize` method like this:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
133
fb74521 @radar portrayal -> portray. thing -> word
authored
134 def self.pluralize(word)
135 word.pluralize
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
136 end
137
138 When we run `bundle exec rspec spec` both our specs will pass:
139
fb74521 @radar portrayal -> portray. thing -> word
authored
140 ...
141 3 examples, 0 failures
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
142
143 This brings another checkpoint where it'd be a good idea to commit our efforts so far.
144
145 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.
146
147 It deserves one.
148
149 ## Testing a command line interface
150
151 Before we go jumping headlong into giving our gem the best darn CLI a gem-with-only-two-methods-that-both-return-useless-strings it 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.
152
62d2c7e @radar Use correct link syntax for "BAM" aruba link
authored
153 Like "Aruba". [BAM](http://github.com/aslakhellesoy/aruba)
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
154
155 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.
156
3876d4a @radar Use add_development_dependency rather than adding them to the Gemfile. T...
authored
157 We will define a new development dependency in _foodie.gemspec_ now for the Cucumber things:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
158
3876d4a @radar Use add_development_dependency rather than adding them to the Gemfile. T...
authored
159 s.add_development_dependency "cucumber"
160 s.add_development_dependency "aruba"
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
161
162 Hot. Let's run `bundle install` to get these awesome tools set up.
163
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
164 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 in it, fill it with this juicy code:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
165
fc31576 @radar Switch to using food-related examples to have a running theme
authored
166 Feature: Food
167 In order to portray or pluralize food
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
168 As a CLI
fc31576 @radar Switch to using food-related examples to have a running theme
authored
169 I want to be as objective as possible
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
170
fc31576 @radar Switch to using food-related examples to have a running theme
authored
171 Scenario: Broccoli is gross
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
172 When I run "foodie portray broccoli"
fc31576 @radar Switch to using food-related examples to have a running theme
authored
173 Then the output should contain "Gross!"
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
174
fc31576 @radar Switch to using food-related examples to have a running theme
authored
175 Scenario: Tomato, or Tomato?
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
176 When I run "foodie pluralize --word Tomato"
fc31576 @radar Switch to using food-related examples to have a running theme
authored
177 Then the output should contain "Tomatoes"
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
178
179 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.
180
181 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.
182
183 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:
184
185 bundle exec cucumber features/
186
187 See those yellow things? They're undefined steps:
188
189 When /^I run "([^"]*)"$/ do |arg1|
190 pending # express the regexp above with the code you wish you had
191 end
192
193 Then /^the output should contain "([^"]*)"$/ do |arg1|
194 pending # express the regexp above with the code you wish you had
195 end
196
197 We can define them by requiring Aruba. In Cucumber, all _.rb_ files in the _features/support_ directory are 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:
198
d5dfdab @radar Change aruba require to aruba/cucumber. Fixes GH-10 [rberger]
authored
199 require 'aruba/cucumber'
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
200
d5dfdab @radar Change aruba require to aruba/cucumber. Fixes GH-10 [rberger]
authored
201 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, Bundler a...
authored
202
203 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:
204
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
205 sh: foodie: command not found
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
206
6a75e46 @radar Be explicit about where the bin directory is.
authored
207 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, Bundler a...
authored
208
209 #!/usr/bin/env ruby
210 print "nothing."
211
b6ccd64 @radar Added notes for making the executable, executable.
authored
212 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:
213
214 chmod +x bin/foodie
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
215
216 Alright so we've got the executable file, now what? If we re-run our features we get *nothing* for the output. Nothing! Literally!
217
218 got: "nothing."
219
220
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
221 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, Bundler a...
authored
222
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
223 require 'foodie/cli'
224 Foodie::CLI.start
90c2bac @radar Writing a recipe generator
authored
225
226
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
227 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, Bundler a...
authored
228
90c2bac @radar Writing a recipe generator
authored
229 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, Bundler a...
authored
230
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
231 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, Bundler a...
authored
232
41b5d95 @radar We write a generator, not a "generation"
authored
233 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, Bundler a...
authored
234
235 ## Crafting a CLI
236
6b3a702 @radar Fix segue between "Crafting a CLI" section and its previous
authored
237 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?).
238
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
239 Let's define the _lib/foodie/cli.rb_ file now like this:
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
240
6b3a702 @radar Fix segue between "Crafting a CLI" section and its previous
authored
241 require 'thor'
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
242 module Foodie
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
243 class CLI < Thor
244
245 end
246 end
247
6b3a702 @radar Fix segue between "Crafting a CLI" section and its previous
authored
248 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, Bundler a...
authored
249
250 s.add_dependency "thor"
251
252 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:
253
fc31576 @radar Switch to using food-related examples to have a running theme
authored
254 Could not find task "portray"
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
255 ...
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
256 Could not find task "pluralize"
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
257
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
258 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, Bundler a...
authored
259
fc31576 @radar Switch to using food-related examples to have a running theme
authored
260 desc "portray ITEM", "Determines if a piece of food is gross or delicious"
261 def portray(name)
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
262 puts Foodie::Food.portray(name)
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
263 end
264
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
265 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, Bundler a...
authored
266
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
267 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, Bundler a...
authored
268
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
269 require 'foodie/food'
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
270
271 When we re-run our features using `bundle exec cucumber features` our first scenario will pass:
272
273 2 scenarios (1 failed, 1 passed)
274 4 steps (1 failed, 3 passed)
275
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
276 The second and third are still failing because we haven't defined the `pluralize` task for them. 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, Bundler a...
authored
277
278
fc31576 @radar Switch to using food-related examples to have a running theme
authored
279 desc "pluralize", "Pluralizes a word"
417fe26 @radar method_option is used like this
authored
280 method_option :word, :aliases => "-w"
fc31576 @radar Switch to using food-related examples to have a running theme
authored
281 def pluralize
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
282 puts Foodie::Food.pluralize(options[:word])
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
283 end
284
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
285 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, Bundler a...
authored
286
287 When we run our scenarios again with `bundle exec cucumber features` both scenarios will be passing:
288
289 2 scenarios (2 passed)
290 4 steps (4 passed)
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
291
292 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_options...
authored
293
294 If we want to add more options later on, we can define them by using the `method_options` helper like this:
295
296 method_options :word => :string, :uppercase => :boolean
297 def pluralize
298 # accessed as options[:word], options[:uppercase]
299 end
300
301 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, Bundler a...
authored
302
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
303 This introduction should have wet 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, Bundler a...
authored
304
305 With our features and specs all passing now, we're at a good point to commit our code.
306
fc31576 @radar Switch to using food-related examples to have a running theme
authored
307 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, Bundler a...
authored
308
90c2bac @radar Writing a recipe generator
authored
309 ## Testing a generator
735d286 @radar WIP - guide for developing a gem using RSpec, Cucumber, Aruba, Bundler a...
authored
310
90c2bac @radar Writing a recipe generator
authored
311 You saw that pun coming, right? Yeah, pretty obvious.
312
313 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:
314
5c89370 @radar Call our gem Foodie (foo-deeeeee).
authored
315 foodie recipe dinner steak
90c2bac @radar Writing a recipe generator
authored
316
317 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.
318
319 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:
320
321 Feature: Generating things
fb74521 @radar portrayal -> portray. thing -> word
authored
322 In order to generate many a thing
323 As a CLI newbie
1269cf8 @radar Further information about how to create a generator.
authored
324 I want foodie to hold my hand, tightly
90c2bac @radar Writing a recipe generator
authored
325
326 Scenario: Recipes
1269cf8 @radar Further information about how to create a generator.
authored
327 When I run "foodie recipe dinner steak"
90c2bac @radar Writing a recipe generator
authored
328 Then the following files should exist:
329 | dinner/steak.txt |
330 Then the file "dinner/steak.txt" should contain:
331 """
1269cf8 @radar Further information about how to create a generator.
authored
332 ##### Ingredients #####
0847492 @radar Mention that the `template` method treats files like an ERB template, an...
authored
333 Ingredients for delicious steak go here.
1269cf8 @radar Further information about how to create a generator.
authored
334
335
336 ##### Instructions #####
0847492 @radar Mention that the `template` method treats files like an ERB template, an...
authored
337 Tips on how to make delicious steak go here.
90c2bac @radar Writing a recipe generator
authored
338 """
0847492 @radar Mention that the `template` method treats files like an ERB template, an...
authored
339
340 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.
341
342 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
343
344
345 Then /^the file "([^"]*)" should contain:$/ do |file, content|
346 check_file_content(file, content, true)
347 end
348
66b311b @radar Complete gem development guide, first draft.
authored
349 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?
350
351 ## Writing a generator
352
353 Well, because currently we don't have a `recipe` task that does this for us defined in `Foodie::CLI`. We can define invoke a generator class just like we invoke a CLI class:
1269cf8 @radar Further information about how to create a generator.
authored
354
355 desc "recipe", "Generates a recipe scaffold"
356 def recipe(group, name)
66b311b @radar Complete gem development guide, first draft.
authored
357 Foodie::Generators::Recipe.start([group, name])
1269cf8 @radar Further information about how to create a generator.
authored
358 end
359
66b311b @radar Complete gem development guide, first draft.
authored
360 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_:
361
362 require 'foodie/generators/recipe'
363
364 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_:
365
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
366 require 'thor/group'
367 module Foodie
368 module Generators
369 class Recipe < Thor::Group
370 include Thor::Actions
66b311b @radar Complete gem development guide, first draft.
authored
371
586a2a4 @rajeshduggal Minor typo fixes to gem-development.md
rajeshduggal authored
372 argument :group, :type => :string
373 argument :name, :type => :string
374 end
375 end
376 end
1269cf8 @radar Further information about how to create a generator.
authored
377
66b311b @radar Complete gem development guide, first draft.
authored
378 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`
379
380 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 ran 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
381
66b311b @radar Complete gem development guide, first draft.
authored
382 def create_group
383 empty_directory(group)
1269cf8 @radar Further information about how to create a generator.
authored
384 end
66b311b @radar Complete gem development guide, first draft.
authored
385
e0ff59f @radar Update guide to explain that template will parse files using ERB
authored
386 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
387
388 def copy_recipe
389 template("recipe.txt", "#{group}/#{name}.txt")
390 end
e0ff59f @radar Update guide to explain that template will parse files using ERB
authored
391
392 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
393
394 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:
395
396 create dinner
397 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
398
66b311b @radar Complete gem development guide, first draft.
authored
399 The first line tells us that the _dinner_ directory has been created. Nothing too fancy there.
400
401 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:
402
403 def self.source_root
404 File.dirname(__FILE__) + "/recipe"
405 end
406
407 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_:
408
409 ##### Ingredients #####
0847492 @radar Mention that the `template` method treats files like an ERB template, an...
authored
410 Ingredients for delicious <%= name %> go here.
66b311b @radar Complete gem development guide, first draft.
authored
411
412
413 ##### Instructions #####
0847492 @radar Mention that the `template` method treats files like an ERB template, an...
authored
414 Tips on how to make delicious <%= name %> go here.
415
416 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
417
0847492 @radar Mention that the `template` method treats files like an ERB template, an...
authored
418 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
419
420 3 scenarios (3 passed)
421 7 steps (7 passed)
422
0847492 @radar Mention that the `template` method treats files like an ERB template, an...
authored
423 Amazing stuff, hey?
424
66b311b @radar Complete gem development guide, first draft.
authored
425 ## Releasing the gem
426
aef425a @radar Mention that files need to be added to a repository before they can be r...
authored
427 If we haven't already, we should commit all the files for our repository:
428
429 git add .
430 git commit -m "The beginnings of the foodie gem"
431
432 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
433
66b311b @radar Complete gem development guide, first draft.
authored
434 The final step before releasing our gem is to give it a summary and description in the _foodie.gemspec_ file.
435
436 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.
437
7cc670b @radar Use "we" rather than "you" here
authored
438 Secondly, it creates a tag for the current commit reflecting the current version and will push it to the set up 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
439
440 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.
441
49ccef8 @radar Mention the way of how to correctly release a new version of a gem.
authored
442 If we want to release a second version of our gem, we should make your 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.
443
e9e4cbd @radar Cover using the very useful gem-release gem by Sven Fuchs
authored
444 If we want to make this process a little easier we could install the "gem-release" gem with:
445
446 gem install gem-release
447
448 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:
449
450 gem bump --to minor # bumps to the next minor version
451 gem bump --to major # bumps to the next major version
452 gem bump --to 1.1.1 # bumps to the specified version
453
454 For more information, check out the ["gem-release" GitHub repository homepage](http://github.com/svenfuchs/gem-release).
455
66b311b @radar Complete gem development guide, first draft.
authored
456 ## Summary
457
458 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.
459
460 **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
461
79c6b41 @radar Thank Andre for his help
authored
462 **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.**
463
340082c @radar Link to source code example at the end of the gem-development guide
authored
464 ** 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.