A remote require for Ruby

I have been fascinated by Deno’s remote import since I discovered it. Experiment with me on how to make a remote require for Ruby based on Deno’s remote import.

Feb 7th, 2024
By Patricio Mac Adden

A couple of years ago I was learning about Deno. Deno is a JavaScript runtime created by Node’s creator, Ryan Dahl. I remember being fascinated by Deno’s remote import, which essentially allows you to import a module that’s stored somewhere on the internet. The most basic example goes as follows:

/**
 * remote.ts
 */
import {
  add,
  multiply,
} from "https://x.nest.land/[email protected]/source/index.js";

function totalCost(outbound: number, inbound: number, tax: number): number {
  return multiply(add(outbound, inbound), tax);
}

console.log(totalCost(19, 31, 1.2));
console.log(totalCost(45, 27, 1.15));

/**
 * Output
 *
 * 60
 * 82.8
 */

As you can see, one can simply import a file that’s stored somewhere on the internet and use it freely.

But, could something like this exist in Ruby?

Proof of concept

In Ruby, a similar feature would look like this:

require "https://gist.githubusercontent.com/patriciomacadden/68523656b306e39a4c2fd54f823f320e/raw/f6470e4a0b0352d1051e510b5613315dfdb109ff/remote_require_test.rb"

Person.new.walk

Here, the intent is to require a ruby file that’s stored in a gist. Then, we’re instantiating a Person object that’s defined in that file and calling its #walk method.

How would this work?

First, let’s not touch require just yet. Let’s create a remote_require method instead. It’d be clearer for now.

An initial implementation would be:

require "net/http"
require "tempfile"

def remote_require(url)
  content = Net::HTTP.get URI(url)

  file = Tempfile.new [File.basename(url, ".*"), File.extname(url)]

  file.write content
  file.close

  require file.path
end

This, a basic implementation of the remote_require method, takes a URL as its only argument and it doesn’t consider that URL to be wrong or not containing a Ruby file. Then, it reads the content of the given URL and stores it in the file system. After storing the file, it just requires it. You can test this implementation by just pasting the code above in an IRB session plus this:

remote_require "https://gist.githubusercontent.com/patriciomacadden/68523656b306e39a4c2fd54f823f320e/raw/f6470e4a0b0352d1051e510b5613315dfdb109ff/remote_require_test.rb"

Person.new.walk

Refining the PoC

Back to the original snippet of the proof of concept, to be able to do this with require, we’d need to monkey patch Kernel#require. Doing this is not that difficult:

require "net/http"
require "tempfile"

module Kernel
  alias original_require require
  def require(path)
    if URI.parse(path).scheme&.start_with? "http"
      remote_require path
    else
      original_require path
    end
  end

  private

  def remote_require(url)
    content = Net::HTTP.get URI(url)

    file = Tempfile.new [File.basename(url, ".*"), File.extname(url)]

    file.write content
    file.close

    original_require file.path
  end
end

In this case, we’re aliasing the require method to original_require and then creating a new require method which will decide if the given string is a URL or not. If it’s an URL it will call the remote_require method and if it isn’t it will call the original_require method. Business as usual.

Is it enough?

Of course, it’s not. Requiring files is much harder than this.

If we think about this for a moment, require deals not only with files but also with gems. So if we’d want to require a gem, say for example, Sinatra, we’d need to download it, unpack it, etc. Everything the gem command does for us. Or, if we get greedy, everything Bundler does for us. To download any gem, not only do we need to download, unpack, etc. but also download, unpack, etc. all its dependencies. And the same for all the dependency graph. As you can imagine, a very difficult task already solved by Bundler.

Moreover, to make this usable out of the box (ie not pasting this snippet when we intend to use it), we need to change the Kernel#require implementation.

Is it worth it?

Enabling the widespread availability of the remote require within the language would mean a huge effort. Not to mention that it’s not something demanded by the community or even something I imagine using myself.

But for sure it was a fun experiment!