Impromptu

A lazy/auto-loading component manager for Ruby.

Motivation

Large libraries such as web frameworks define a lot of code that may never be used in a production running app. It would be nice if you could pick and choose which parts of a library are actually loaded and used within an application, and automatically reload files when they are changed. It would be even nicer if you didn’t have to manually configure which components to include at all.

Overview

Impromptu allows you to define components of a system, the gem/external requirements of a component, and the files and folders which implement the resources provided. const_missing is used to load modules and classes the first time they are used, along with any gems/requirements the component as a whole has. By doing this, there’s no need to define which components of a library are actually used; the fewest components possible are loaded simply as a result of the code you write.

A simple component DSL is used to define components:

component 'framework' do
  namespace :Framework
  requires  'rack'
  folder    'lib', nested_namespaces: false
end

component 'private' do
  namespace :Framework
  folder 'private'
end

component 'other' do
  folder 'other' do
    file 'load.rb'
    file 'mods.rb', provides: [:ModOne, :ModTwo]
  end
end

Components may have a namespace, may have requirements, and must have at least one folder containing source files that implement the resources provided by the component. Imagine the ‘lib’ folder used by the ‘framework’ component contained the files:

[lib]
  \ klass.rb
  \ [group]
      \ klass2.rb

These files would provide the resources Framework::Klass, and Framework::Klass2. If the ‘nested_namespaces’ flag was true, klass2.rb would instead provide Framework::Group::Klass2. When either of these resources are requested anywhere, Impromptu first loads the requirements of the component (in this case rack) before loading any files implementing the requested resource. Sometimes, a resource is defined by more than one file. Imagine the ‘private’ folder contained:

[private]
  \ klass.rb
  \ klass2.rb

Because the ‘private’ component shares the same namespace as the ‘framework’ component, these two files would also provide Framework::Klass and Framework::Klass2. Referencing Klass or Klass2 would load the file from ‘lib’ first before the file from ‘private’ (since the ‘private’ component appears after the ‘framework’ component).

In both these components no file provides the Framework module. In this case, Impromptu will automatically create a blank module. If after an update a file appears which implements the namespace, the blank module will be removed, and the new implementation loaded. Removing this file will cause the blank module to be created again.

Just as a resource may be implemented by multiple files, a file may implement multiple resources. Because Impromptu doesn’t scan files before loading them you must manually tell Impromptu which resources a file provides if it provides more than one, or if the name of the resource cannot be inferred from the name of the file. Folder definitions may take a block (as with the ‘other’ folder from the ‘other’ component). Calling file within the block informs Impromptu of a file to load, and specifying the :provides option allows you to tell Impromptu the names of the resources implemented in the file. You can set the implicitly_loaded option to false if you would like to restrict the set of files loaded for a folder to the set you define within the passed block.

Extending the Standard Library

Impromptu deals with resources that implement extensions to the standard library a little differently. When a resource is declared, if the implementation of the resource already exists, it assumed the resource extends a standard Ruby class or module. For instance, a resource extending String falls in to this category.

These components are automatically loaded at the start of your program since it’s impossible for Impromptu to detect when you use them for the first time. If the folder containing their source files is reloadable, any changes to the files will cause the files to be re-required without undefining the module or class. Normally Impromptu undefines a resource before re-requiring the files which define it to ensure the definition doesn’t become ‘stale’. This isn’t possible with preexisting resources, so a side effect of reloading these files is that methods or constants you remove from your extension will remain defined. You will need to restart your program to ensure a clean definition.

Defining Components

Before any resources from a component can be loaded, you must define the components of your system in a block provided to define_components. There are two options: you can either define components directly in this block, or you can call the ‘parse_file’ method to load a file which defines the components instead.

Impromptu.define_components do
  # direct
  component do
    ...
  end

  # from a file
  parse_file 'framework/framework.components'
end

The present working directory, or the directory containing a file being parsed, is used as the current ‘base’ directory. Folder references are assumed to be relative to this directory, unless they are an absolute path. Once the set of components is defined it cannot be changed. The list of folders implementing a component also cannot be changed, although the files within a folder may be changed and reloaded. These restrictions may be lifted in a later release.

Reloading Components

Impromptu supports the reloading of resource implementation files, including adding new files to a folder and removing existing files. By default folders are assumed to be reloadable, though setting the reloadable option to false prevents folders being scanned during an update:

folder 'lib', :reloadable => false

To force a reload of already loaded resources, and an update of the resource tree, call Impromptu.update. Only resources which have their files changed and have previously been loaded will be reloaded. Any new files will either insert their resources in to the resource tree or add themselves to the list of files implementing an existing resource. Removing a file may simply remove it from the list of files implementing a resource, or it may unload and remove the resource entirely (if it was the only or last file implementing a resource). Any folders which were defined using a block and subsequent calls to ‘file’ will not be scanned for new or missing files since their file list is explicitly defined. Only files with a reloadable extension (currently ‘rb’) will be reloaded if they are changed - object files such as bundles and shared objects can only be loaded once.

Known Issues

Reloading resources is currently not guarded so if you have one thread reloading a resource, and another accessing it, the second thread may get a stale reference.

Thanks

To Matt (twitter.com/mattrobs) for the name