DEV Community

loading...

In SemVeritas

olistik profile image olistik ・3 min read

It's more than once that I had to deal with a Gemfile in which dependencies are a long and unsorted list with a lot of them without a version at all:

gem 'puma'
gem 'sinatra'
# ...
# a lot of other dependencies here
# ...
gem 'rake'

Alt Text

Sometimes I have to upgrade a dependency or maybe I just want to have a quick picture of the dependencies distribution my project is relying on.

To do that, I open the Gemfile.lock and then scan for the specific version of the gem.

This is a time consuming task and:

I'd much rather prefer to have a Gemfile with a sorted list of gems, each of them providing a "sane" version requirement.

By "sane" I mean a requirement that is:

  1. not too loose (eg. no version at all or something like > 2)
  2. not too strict (eg. = 3.5.1)

RubyGems suggests to use the pessimistic version constraint which is based on the fact that SemVer is a strongly encouraged practice we all should follow when authoring ruby gems.

Assuming that one of our dependencies is currently set to version 2.3, I'd be tempted to set ~> 2 as the version requirement because it is based on the assumption that, if the gem author follow the SemVer specification, there shouldn't be any breaking changes until version 3.

Since we don't live in an ideal world, it's often better to be a little more conservative and let only the patch version upgrade freely every time we perform a bundle update. This leads us to prefer ~> 2.3 instead.

Back to my needs, wouldn't it be awesome to have a little tool to help me update my Gemfile suggesting the sane version requirement for each dependency?

Alt Text

A couple of hours later, after digging through DuckDuckGo, StackOverflow and the RubyDocs, here's the hackish script I came up with:

Alt Text

#!/usr/bin/env ruby

require 'bundler'

parser = Bundler::LockfileParser.new(Bundler.read_file(Bundler.default_lockfile))

definition = Bundler.definition

groups = {}
gems = definition.current_dependencies.each_with_object({}) do |current, memo|
  memo[current.name] = {
    groups: current.groups,
  }
  current.groups.each do |group|
    groups[group] ||= []
    groups[group] << current.name
  end
  memo
end

gem_names = gems.keys.sort

parser.specs.select do |spec|
  gem_names.include?(spec.name)
end.each do |spec|
  gems[spec.name][:suggested_version] = spec.version.approximate_recommendation
end

groups.each do |group_name, group_gems|
  puts "group :#{group_name} do" unless group_name == :default
  group_gems.each do |gem|
    print "\t" unless group_name == :default
    puts "gem \"#{gem}\", \"#{gems[gem][:suggested_version]}\""
  end
  puts "end" unless group_name == :default
end

If you call it little-gemfile-helper and:

chmod u+x little-gemfile-helper

If you move into a project containing a Gemfile.lock and a Gemfile such as:

source 'https://rubygems.org'

gem 'puma'
gem 'sinatra'
gem 'rake'

group :test do
  gem 'rack-test'
end

and launch our little helper:

./little-gemfile-helper

We would get displayed a useful reference to quickly update our Gemfile:

gem "puma", "~> 3.9"
gem "sinatra", "~> 1.4"

group :test do
  gem "rack-test", "~> 0.6"
end

Alt Text

Happy versioning! 🤓

Alt Text

Discussion (1)

Collapse
olistik profile image
olistik Author

A friend of mine pointed out that there's already a gem doing what I need and much more: pessimize.

It's good to know that the same need is shared by other people. 🙂

It's also interesting to understand how the gem achieves its goals.
Instead of relying on Bundler's parsing features, it defines a parser on its own.
Also, by overwriting the Gemfile, it follows a path that I didn't wanted to follow because for me it's more than enough to have a quick reference to update the Gemfile, manually handling corner cases.

Forem Open with the Forem app