Mike Slinn
Mike Slinn

Gem ‘Subclassing’ and Introspection

Published 2023-04-06. Last modified 2023-04-07.
Time to read: 4 minutes.

This page is part of the ruby collection, categorized under gem.

Ruby gems provide a handy way of packaging reusable Ruby code for use in Ruby programming projects. Bundler simplifies and automates the process of downloading, installing and maintaining compatible versions of Ruby gems dependencies for a project. Ruby gems are also very easy to publish.

My jekyll_plugin_support gem contains classes called JekyllBlock, JekyllTag, JekyllBlockNoArgParsing, and JekyllTagNoArgParsing. These classes are meant to be subclassed. New Jekyll plugins are created around these subclasses, and these plugins are normally packaged as gems also.

To clarify, while gems cannot be subclassed, when the jekyll_plugin_support gem is added to a project as a dependency, at least one of the four classes that I mentioned are meant to be subclassed. The subclasses are used to define a Jekyll plugin.


Recently I wanted to add optional functionality into jekyll_plugin_support that would be available to JekyllBlock and JekyllTag subclasses.

The new jekyll_plugin_support functionality required it to recognize when it was being invoked by a gem, and to obtain the information stored in the invoking gem’s Gem::Specification.

This required a deep dive into how Jekyll loads plugins, how Liquid dispatches tags, and the inner workings of Ruby gems. This article is not going to drag you through all the details, instead, just the most important details will be discussed.

Ruby Gem Definitions

Ruby gems are defined by a Gem::Specification, normally saved in a .gemspec file. If you have ever looked at a Ruby gem’s source code, this is familiar to you. For example, here is the gem specification for jekyll_plugin_support:

require_relative 'lib/jekyll_plugin_support/version'

Gem::Specification.new do |spec|
  github = 'https://github.com/mslinn/jekyll_plugin_support'

  spec.bindir = 'exe'
  spec.authors = ['Mike Slinn']
  spec.email = ['mslinn@mslinn.com']
  spec.files = Dir['.rubocop.yml', 'LICENSE.*', 'Rakefile', '{lib,spec}/**/*', '*.gemspec', '*.md']
  spec.homepage = 'https://www.mslinn.com/jekyll_plugins/jekyll_plugin_support.html'
  spec.license = 'MIT'
  spec.metadata = {
    'allowed_push_host' => 'https://rubygems.org',
    'bug_tracker_uri'   => "#{github}/issues",
    'changelog_uri'     => "#{github}/CHANGELOG.md",
    'homepage_uri'      => spec.homepage,
    'source_code_uri'   => github,
  spec.name = 'jekyll_plugin_support'
  spec.post_install_message = <<~END_MESSAGE

    Thanks for installing #{spec.name}!

  spec.require_paths = ['lib']
  spec.required_ruby_version = '>= 2.6.0'
  spec.summary = 'Provides a framework for writing and testing Jekyll plugins'
  spec.test_files = spec.files.grep %r{^(test|spec|features)/}
  spec.version = JekyllPluginSupportVersion::VERSION

  spec.add_dependency 'facets'
  spec.add_dependency 'jekyll', '>= 3.5.0'
  spec.add_dependency 'jekyll_plugin_logger'
  spec.add_dependency 'key-value-parser'
  spec.add_dependency 'pry'

Gem Self-Discovery

A key feature of Ruby gems, essential to the code presented in this article, is that gems are not stored in a compressed format. Instead, they are stored as uncompressed files in a directory tree.

In contrast, most package management formats for other computer languages and OSes use a compressed format to store dependencies. For example: Python wheel, Java jars (also used by Maven), and Debian deb (also used by Ubuntu apt).

The following Ruby code was developed on StackOverflow. The method returns the Gem::Specification for a gem when pointed at any file in any directory within the gem.

# @param file must be a fully qualified file name
# @return Gem::Specification of gem that file points into,
# or nil if not called from a gem
def current_spec(file)
  return nil unless file.exist?

  searcher = if Gem::Specification.respond_to?(:find)
             elsif Gem.respond_to?(:searcher)

  searcher&.find do |spec|
    file.start_with? spec.full_gem_path

The current_spec method in the above code uses the safe navigation operator.

&., called “safe navigation operator”, allows to skip method call when receiver is nil. It returns nil and doesn’t evaluate method’s arguments if the call is skipped.  – From Ruby-Doc.org

If I paste the code that defines searcher into irb, the result is a Gem::Specification, which is confusing because it is actually a Gem::Specification that contains a collection of Gem::Specifications.

$ $ irb
irb(main):001:1* searcher = if Gem::Specification.respond_to?(:find)
irb(main):002:1*              Gem::Specification
irb(main):003:1*            elsif Gem.respond_to?(:searcher)
irb(main):004:1*              Gem.searcher.init_gemspecs
irb(main):005:0>            end
=> Gem::Specification 

Lets look at the first element contained within searcher:

irb(main):006:0> searcher.first
Gem::Specification.new do |s|
  s.name = "error_highlight"
  s.version = Gem::Version.new("0.3.0")
  s.installed_by_version = Gem::Version.new("0")
  s.authors = ["Yusuke Endoh"]
  s.date = Time.utc(2023, 1, 24)
  s.description = "The gem enhances Exception#message by adding a short explanation where the exception is raised"
  s.email = ["mame@ruby-lang.org"]
  s.files = ["lib/error_highlight.rb",
  s.homepage = "https://github.com/ruby/error_highlight"
  s.licenses = ["MIT"]
  s.require_paths = ["lib"]
  s.required_ruby_version = Gem::Requirement.new([">= 3.1.0.dev"])
  s.rubygems_version = "3.3.3"
  s.specification_version = 4
  s.summary = "Shows a one-line code snippet with an underline in the error backtrace"

Lets search the list for the Gem::Specification for the jekyll_plugin_support v0.6.0 gem into spec.

irb(main):007:0> spec = searcher.find_by_name('jekyll_plugin_support', '0.6.0')
Gem::Specification.new do |s|

The directory that contains an installed Gem::Specification can be obtained by calling full_gem_path:

irb(main):008:0> spec.full_gem_path
=> "/home/mslinn/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/jekyll_plugin_support-0.6.0" 

As most Ruby programmers know, a Ruby source file can discover its location by examining __FILE__. If this is done by a file within Ruby gem, then the location of the file is obtained, and one of the parent directories of the location will be location of the gem.

irb(main):008:0> file = __FILE__
=> "(irb)" 

Unfortunately, __FILE__ does not return anything useful from irb, as you can see. Let’s cheat a little and set file to a valid value for the gem we are looking at. We’ll point at lib/jekyll_plugin_support.rb within the gem:

irb(main):009:0> file = File.join(spec.full_gem_path, 'lib/jekyll_plugin_support.rb')
=> "/home/mslinn/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/jekyll_plugin_support-0.6.0/lib/jekyll_plugin_suppo..." 

irb(main):010:0> File.exist? file
=> true 

Here is an easily test to see if the file is within the gem directory tree:

irb(main):008:0> file.start_with? spec.full_gem_path
=> true 

If a gem needs to know what its Gem::Specification is, it can iterate through each of the items within searcher, and compare the value from full_gem_path against the value returned by __FILE__.

irb(main):008:0> searcher&.find do |spec|
  file.start_with? spec.full_gem_path
=> Gem::Specification.new do |s|

Now that we know how the current_spec method works, we can invoke it from a Ruby gem like this:

Ruby gem source
spec = current_spec __FILE__

The gem has obtained its own Gem::Specification through digital navel-gazing (omphaloskepsis).

The gem can now retrieve its own properties from spec.

Ruby gem source continued
@name           = spec.name
@authors        = spec.authors
@homepage       = spec.homepage
@published_date = spec.date.to_date.to_s
@version        = spec.version

... and that is the essence of how the jekyll_plugin_support subclass feature works.

About the Author

I, Mike Slinn, have been working with Ruby a long time now. Back in 2005 I was the product marketing manager at CodeGear (the company was formerly known as Borland) for their https://www.infoq.com/news/2007/05/codegear-ror-ide/ 3rd Rail IDE. 3rd Rail supported Ruby, and Ruby on Rails, at launch.

In 2006, I co-chaired the Silicon Valley Ruby Conference, on behalf of SD Forum in Silicon Valley. As you can see, I have the t-shirt. I was sole chairman of the 2007 Silicon Valley Ruby Conference.

Several court cases have come my way over the years in my capacity as a software expert witness. The court cases featured questions about IP misappropriation for Ruby on Rails programs. You can read about my experience as a software expert if that interests you.

I currently enjoy writing Jekyll plugins in Ruby for this website and others, and Ruby utilities.