the making of korgi

korgi is what I put together to write lazy links to Rails resources. It is an extension of html-pipeline, which essentially makes the following syntax available:

$+image.1$
=> /uploads/image/file/1/thumb_pic.jpg

$#post.3$
=> /posts/3

Anyway, see the README for detail usage, it’s actually pretty cool. This post is meant to document the problems I had to work through, so I can look back and laugh at myself some time later. I knew something would go wrong because I aced the regex in one try XD


For some reason, every time I have an idea for a Ruby gem, it always has to be a hard-to-google one. The part I struggled most was getting Korgi::NamedRouteFilter to work. Here were my goals:

  • needs access to url_for so I can actually build the links
  • needs to be configurable
  • needs to be testable

getting url_for to work

Easy enough, right?

# lib/korgi/named_route_filter.rb
require "rails/all"
require "html/pipeline"
module Korgi
class NamedRouteFilter < ::HTML::Pipeline::Filter
include ActionView::Helpers
include Rails.application.routes.url_helpers
end
end

Well, here’s the funny part. I actually wrote the tests first, and my tests were passing! However, as I learned later, they were passing only because:

  • the code logic itself was correct (duh)
  • the tests were run through a dummy Rails app
  • korgi/named_route_filter was not required in lib/korgi.rb

Once I noticed that it was missing in lib/korgi.rb and required it, RSpec started to choke:

NoMethodError: undefined method `routes' for nil:NilClass

Umm… okay? After much digging, and I mean A LOT OF DIRT, because googling for whatever keywords you can come up based on the information above will get you absolutely nowhere.

Then it hit me. Maybe I need a Railtie to, I don’t know, tie the gem and Rails together?

Developing a Rails extension does not require implementing a railtie, but if you need to interact with the Rails framework during or after boot, then a railtie is needed.

Looks promising. With the right keywords, Stack Overflow finally proves helpful:

# lib/korgi/railtie.rb
require "rails/railtie"
module Korgi
class Railtie < ::Rails::Railtie
config.after_initialize do
require "korgi/named_route_filter"
end
end
end
# lib/korgi.rb
require "korgi/engine"
require "korgi/railtie"
module Korgi
end

What (I think) happened was that, korgi/named_route_filter was loaded before Rails was ready, so for the life of typing ducks and patching monkeys, it could not recognise Rails.application. By loading it through Railtie, ensuring that it was loaded after Rails, tests starting passing again!

making the gem configurable

All the cool gems come with this, so this was fairly easy to implement:

# lib/korgi/config.rb
require "active_support/configurable"
module Korgi
include ActiveSupport::Configurable
config.instance_eval do
self.named_routes = {}
self.file_uploads = {}
end
end

testing the gem

Most of this is already documented here with a working example. It’s in Mandarin but the demo code should speak for itself, so I won’t go into details again.

testing file uploads in the dummy app

Working with CarrierWave was interesting though. Basically, you need to do a fixture_file_upload and close the file as a factory:

# spec/factories/image.rb
include ActionDispatch::TestProcess
FactoryGirl.define do
factory :image do
id { 1 }
file { fixture_file_upload("spec/photo/test.jpg", "image/jpg") }
after(:create) do |image, proxy|
proxy.file.close
end
end
end

Since we’re using FactoryGirl, you also have to add factories to the path:

# spec/rails_helper.rb
FactoryGirl.definition_file_paths = [File.expand_path("../factories", __FILE__)]
FactoryGirl.find_definitions

Set up CarrierWave in the dummy app as you would usually do:

# spec/dummy/app/model/image.rb
class Image < ApplicationRecord
mount_uploader :file, ImageUploader
end
# spec/dummy/app/uploader/image_uploader.rb
class ImageUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
storage :file

def cache_dir
"#{Rails.root}/public/uploads/tmp"
end

def store_dir
"#{Rails.root}/public/uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end

# implement versions
end
# migration
def change
create_table :images do |t|
t.string :file
t.timestamps
end
end

$ cd spec/dummy
$ rake db:migrate

After all this work, we finally have an image to test with:

before { FactoryGirl.create(:image) }

wait, where do I put gems that the dummy app needs?

You should require them in the engine:

# lib/korgi/engine.rb
require "rails/engine"
require "carrierwave"
require "mini_magick"
module Korgi
class Engine < ::Rails::Engine
end
end

EDIT: actually, on second thought, don’t do this.

  • the dummy app is only for testing purposes
  • you don’t want to pollute korgi.gemspec with gems that are not run time dependencies (plus, for CI’s sake, it’s so much easier to put development_dependencies in Gemfile instead)
  • the plugin generator doesn’t create spec/dummy/Gemfile for a good reason so let’s keep it that way

So, put them in the dummy app’s config instead:

# Gemfile
group :test do
gem "mini_magick"
end
# spec/dummy/config/application.rb
Bundler.require(*Rails.groups)
require "korgi"
require "mini_magick"

nuke ’em all

I also like to nuke stuff after tests:

# spec/rails_helper.rb
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end

config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end

config.after(:all) do
FileUtils.rm_rf(Dir["#{Rails.root}/public/uploads"])
end
end

And that’s about it! So here’s a cute Corgi picture I found on the Internet, because endings are hard, and who doesn’t like cute Corgis?

corgi

photo credit Daniel Stockman

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.