If you seriously think about Ruby career, at some point you’ll need to maintain a gem. And I don’t even talk about open source contributions, which I totally recommend. But to maintain a gem, or a project, there is a chance you’ll want to easily decide, what version of software people would like to use.
In this episode, I’m going to show you a few tips by examples.
Writing the gem
We want to write a very cool gem: a "geek detector"
We start by running the gem generator that bundler
gives us.
$ bundler gem geek_detector
Then we fill in our gem's gemspec.
require_relative "lib/geek_detector/version"
Gem::Specification.new do |spec|
spec.name = "geek_detector"
spec.version = GeekDetector::VERSION
spec.authors = ["Federico Iachetti"]
spec.email = ["iachetti.federico@gmail.com"]
spec.summary = "Find geeks easily."
spec.description = "Find all the geeks. This gem is for HanamiMastery usage"
spec.homepage = "<https://geek-detector.example.com>"
spec.license = "MIT"
spec.required_ruby_version = ">= 2.6.0"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "<http://github.com/404>"
spec.metadata["changelog_uri"] = "<http://github.com/404>"
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\\\\\\\\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
end
And we write the main functionality of our gem.
We'll create a module_function
,which will allow to call methods on our GeekDetector
module either directly on it or by including it into a class.
Finally, we write the geek?
methodthat will match our input to a regular expressionand return a boolean depending on the result of that operation.
require_relative "geek_detector/version"
module GeekDetector
module_function
def geek?(input)
!!(input =~ /geek/)
end
end
It can detect a geek inside either a string or a symbol.
And can detect that there's no geek in sight.
GeekDetector.geek?("I'm a geek") # => true
GeekDetector.geek?(:i_am_a_geek) # => true
GeekDetector.geek?("I'm not") # => false
You tried it a bunch and you think it might be ready to see the light of day and be used by everyone in the world, YAY!
Versioning the Gem
Now is the time to put a version number to it. What should we write? 1.0? 0.1? 0.0.1? what does that even mean?
When I need to version my gems, I like to use Semantic versioning.
Semantic versioning is a set of rules and requirements that dictate how version numbers should be constructed.
Lets open the version.rb
file.
Using Semantic versioning, a version is made out of three main parts separated by dots: the MAJOR,MINORand PATCH numbers
module GeekDetector
VERSION = "MAJOR.MINOR.PATCH"
end
Each of this parts have a purpose and a reason to change.
Let's start by talking about the first version we'll add to your gem. You always need to have all three parts of the version (MAJOR, MINOR and PATCH), and It's not a good practice to start everything with zero. So our options are limited to either 0.0.1and 1.0.0.
module GeekDetector
VERSION = "0.0.1"
VERSION = "1.0.0"
end
Which one would I choose? I'm a developer, so my response to this is obviously it depends.
By convention, anything previous to 1.0.0 is considered not yet stable so, if you're just putting this out for the world to try it out, I'd advise to use 0.0.1. But if you're confident it's ready for production, 1.0.0 will be a better choice.
The definition of stable is up to each developer. In my case, I consider something is stable when I've tried it enough that I like how it's used. In other words, when I expect there will be no significant changes to it's API.
For this example we'll go with 0.0.1,because we just tried it in isolation and we want to make sure it's stable.
module GeekDetector
VERSION = "0.0.1"
end
First feedback, fix a bug
After a few days, we get feedback of people that say that our geek detector is not working properly. They say it can't find a geek in the "I'm a GEEK" string.
GeekDetector.geek?("I'm A GEEK") # => false
And here's our problem,the regular expression is case-sensitive, which we don't want.
def geek?(input)
!!(input =~ /geek/)
end
But we can fix it with a small tweak.
def geek?(input)
!!(input =~ /geek/i)
end
And when we try it again, it now works
GeekDetector.geek?("I'M A GEEK") # => true
Now that we made a change, we need to update the version number.Since all we did was fix a bug, keeping the code backwards-compatible, we need to update the PATCH number.
Or, in other words, the PATCH number only changes when we fix bugs in a backwards-compatible way. We just increase it by one.
module GeekDetector
VERSION = "0.0.2"
end
Something to note is that once we reach the number 9(in any of the parts), we don't carry over to the next one, but just increase it to 10.
module GeekDetector
- VERSION = "0.0.9"
+ VERSION = "0.0.10"
end
Second feedback, implementing a feature
Having fixed this bug, we catch another piece of feedback.
Our geek detector was so useful that more and more people are using it, but they're asking for our library to also detect nerds.
GeekDetector.geek?("I'M A nerd") # => false
Again, this is easy enough.
def geek?(input)
!!(input =~ /(geek)|(nerd)/i)
end
Which gives us the expected result.
GeekDetector.geek?("I'M A nerd") # => true
Our code is still backwards compatible, but we didn't fix a bug. This time we added a new feature. So in this case, the part to increment is the middle one, the MINOR version,resetting the PATCH version to zero.
module GeekDetector
VERSION = "0.1.0"
end
Going Live!
Now, say our library has been out for quite some time and we haven't had any more bugs nor features reported. So we can decide to bump our major version to 1.0.0,since we consider our gem is now stable and ready for production.
So this is one reason the major version might change: being ready!
module GeekDetector
VERSION = "1.0.0"
end
So far, while on version previous to 1.0.0 we didn't care about our major version, it was always kept at zero. Even though it didn't happen, if we'd broken backwards compatibility, we wouldn't change the major version, because, we considered the project in a non-stable state.
But for now on, we'll need to keep an eye on our compatibility, since it can break other people's code (previous to 1 this was not as bad a concern because having the major version at 0, unstability is implied).
Breaking backwards compatibility
But say we decide that we don't want to maintain symbols anymore, it doesn't make sense to allow the user to receive anything other than strings in our method. Maybe they cause problems down the line.
So we solve this by raising an error if our parameter is not a string.
def geek?(input)
raise unless input.is_a?(String)
!!(input =~ /(geek)|(nerd)/i)
end
And we can see it in action.
GeekDetector.geek?("I'm a geek") # => true
GeekDetector.geek?("I'm not") # => false
GeekDetector.geek?("I'm A GEEK") # => true
GeekDetector.geek?("I'm A nerd") # => true
GeekDetector.geek?(:i_am_a_geek) # => RuntimeError:
# ~> RuntimeError
# ~>
# ~> /sandbox/geek_detector/lib/geek_detector.rb:9:in `geek?'
# ~> xmptmp-inhcmbgO.rb:8:in `<main>'
Now we've broken our backwards-compatibility. Now is the time for 2.0.0.
We only change major versions if we've broken our backwards compatibility.
module GeekDetector
VERSION = "2.0.0"
end
Using bump to update the version
If you don't like updating versions by hand, here's an option for you. The bumpgem can help us update either part of our gem's version automatically, without any fuzz.
Let's start by installing it
$ gem install bump
Fetching bump-0.10.0.gem
Successfully installed bump-0.10.0
1 gem installed
If we run bump current
,we're shown the current gem version.This doesn't produce any change, it just displays the version.
$ bump current
2.0.0
We can also find in which file the version is being set by running bump file
.
$ bump file
lib/geek_detector/version.rb
And we can display what'd be the next versions, either for the PATCH,MINORor MAJORversions.
$ bump show-next patch
Resolving dependencies...
2.0.1
$ bump show-next minor
Resolving dependencies...
2.1.0
$ bump show-next major
Resolving dependencies...
3.0.0
Now, lets do something more interesting.
Lets upgrade to the the next patch version
$ bump patch
Resolving dependencies...
Fetching gem metadata from <http://localhost:9898/>.
Resolving dependencies...
Using rake 13.0.6
Using bundler 2.3.19
Using diff-lcs 1.5.0
Using rspec-support 3.11.1
Using geek_detector 2.0.1 (was 0.1.0) from source at `.`
Using rspec-core 3.11.0
Using rspec-expectations 3.11.1
Using rspec-mocks 3.11.1
Using rspec 3.11.0
Bundle complete! 3 Gemfile dependencies, 9 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
[main c53f142] v2.0.1
2 files changed, 2 insertions(+), 2 deletions(-)
2.0.1
We can now see that the current version is 2.0.1
$ bump current
2.0.1
And that a commit was made. This was all done automatically, without any "direct" intervension from us.
$ git log
* c53f142 - (HEAD -> main) v2.0.1 (45 seconds ago) <Federico Iachetti>
* 4dc1187 - Version 2.0.0 (3 minutes ago) <Federico Iachetti>
* 16fa068 - Version 1.0.0 (5 minutes ago) <Federico Iachetti>
* 19dc1df - Version 0.1.0 (5 minutes ago) <Federico Iachetti>
* f01a6d9 - Version 0.0.2 (6 minutes ago) <Federico Iachetti>
* 5fa02c9 - Added example for upper case (6 minutes ago) <Federico Iachetti>
* 645c3a3 - Version 0.0.1 (7 minutes ago) <Federico Iachetti>
* ccca2ef - Initial commit (7 minutes ago) <Federico Iachetti
We can do the same for MINOR and MAJOR versions.
But lets say we want to bump the MINOR versionwithout actually making a commit. We can do it using the --no-commit
flag.
$ bump minor --no-commit
Fetching gem metadata from <http://localhost:9898/>.
Resolving dependencies...
Using rake 13.0.6
Using bundler 2.3.19
Using diff-lcs 1.5.0
Using rspec-support 3.11.1
Using geek_detector 2.1.0 (was 2.0.1) from source at `.`
Using rspec-core 3.11.0
Using rspec-expectations 3.11.1
Using rspec-mocks 3.11.1
Using rspec 3.11.0
Bundle complete! 3 Gemfile dependencies, 9 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
2.1.0
If we show the git status,we can see that the version.rb
and Gemfile.lock
files have been changed, but not commited.
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: Gemfile.lock
modified: lib/geek_detector/version.rb
no changes added to commit (use "git add" and/or "git commit -a")
Lets take a peek at the version
file
module GeekDetector
VERSION = "2.1.0"
end
Now we can rework our code and commit by hand when we're ready.
$ git add -A
$ git commit -m "Releasing version 2.1.0"
[main 33f1fc3] Releasing version 2.1.0
2 files changed, 2 insertions(+), 2 deletions(-)
We can also add a tag along with the commit
$ bump major --tag
Fetching gem metadata from <http://localhost:9898/>.
Resolving dependencies...
Using rake 13.0.6
Using bundler 2.3.19
Using diff-lcs 1.5.0
Using geek_detector 3.0.0 (was 2.1.0) from source at `.`
Using rspec-support 3.11.1
Using rspec-core 3.11.0
Using rspec-expectations 3.11.1
Using rspec-mocks 3.11.1
Using rspec 3.11.0
Bundle complete! 3 Gemfile dependencies, 9 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
[main 3571c3e] v3.0.0
2 files changed, 2 insertions(+), 2 deletions(-)
3.0.0
Which we can corroborate by running git log
$ git log
* 3571c3e - (HEAD -> main, tag: v3.0.0) v3.0.0 (54 seconds ago) <Federico Iachetti>
* 33f1fc3 - Releasing version 2.1.0 (2 minutes ago) <Federico Iachetti>
* c53f142 - v2.0.1 (9 minutes ago) <Federico Iachetti>
* 4dc1187 - Version 2.0.0 (11 minutes ago) <Federico Iachetti>
* 16fa068 - Version 1.0.0 (13 minutes ago) <Federico Iachetti>
* 19dc1df - Version 0.1.0 (13 minutes ago) <Federico Iachetti>
* f01a6d9 - Version 0.0.2 (14 minutes ago) <Federico Iachetti>
* 5fa02c9 - Added example for upper case (14 minutes ago) <Federico Iachetti>
* 645c3a3 - Version 0.0.1 (15 minutes ago) <Federico Iachetti>
* ccca2ef - Initial commit (15 minutes ago) <Federico Iachetti>
Lastly, say we wrote the version somewhere on the repository, the README
for example.
# Version
We're currently on version 3.0.0
We can update this number every time we bump,by using the --replace-in
flagand telling bump
the file where it has to look for it.
$ bump patch --replace-in README.md
Fetching gem metadata from <http://localhost:9898/>.
Resolving dependencies...
Using rake 13.0.6
Using bundler 2.3.19
Using diff-lcs 1.5.0
Using geek_detector 3.0.1 (was 3.0.0) from source at `.`
Using rspec-support 3.11.1
Using rspec-expectations 3.11.1
Using rspec-mocks 3.11.1
Using rspec-core 3.11.0
Using rspec 3.11.0
Bundle complete! 3 Gemfile dependencies, 9 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
[main e2be473] v3.0.1
3 files changed, 4 insertions(+), 6 deletions(-)
3.0.1
Now, if we look at the README, it was properly updated
# Version
We're currently on version 3.0.1
Summary
Following the semantic versioning in a gem or project is a nice skill to have. It’s helpful not only to track the progress of the project, identify bugs or manage versions, but also it’s a clean message to our users. They feel more safe during upgrades, and by knowing what they could expect, the can plan upgrading their systems accordingly.
Let me know in the comments if you follow the semantic versioning in your projects, or if you have other versioning system worth of mentioning!
Thanks
I want to especially thank my recent sponsors,
for supporting this project, I really appreciate it!
Also shot-out for Roman Synkevych for the great cover image!
If you want to support us, check out our Github sponsors page or join Hanami Mastery PRO!
Add your suggestion to our discussion panel!
I'll gladly cover them in the future episodes! Thank you!