I help maintain an open source Ruby library.

Every now and then, we get feature requests that make me scratch my head.

Feature request

Isn't this a syntax error?

class Foo < Dash
  include Persistable.new(persist_method: :commit)
end

Yes, yes it is.

module Persistable
  def initialize(*args)
  end
end

class Foo < Dash
  include Persistable.new(persist_method: :commit)
end
#=> NoMethodError: undefined method `new'

Yay, no syntax errors!

module Persistable
  def self.new(*args)
    self
  end
end

class Foo < Dash
  include Persistable.new(persist_method: :commit)
end

What can we do with this?

Dynamically Building Ruby Modules

A story about turning a feature request into a gem

Michael Herold

Full-stack Engineer, AcceptOn

Module Creation

module Persistable
  def self.new(options = {})
    adapter         = options.delete(:adapter) || :json
    persist_method  = options.delete(:persist_method) || :persist

    Builder.new(adapter, persist_method).mod
  end
end

Bare Inclusion - Use the Defaults

module Persistable
  def self.included(base)
    base.send(:include, new)
  end
end
class PersistableClass
  include Persistable
end
class SecondPersistableClass
  include Persistable.new
end

Basic Implementation

class Builder
  attr_reader :mod

  def initialize(adapter_type, persist_method, mod = Module.new)
    @adapter_type = adapter_type
    @persist_method = persist_method.to_sym
    @mod = mod

    add_adapter
    add_persist_method
    override_persist_method
  end
end

#add_adapter

class Builder
  def add_adapter
    mod.send(:include, Persistable.adapters[@adapter_type])
  end
end
module Persistable
  def self.adapters
    @adapters ||= {}
  end

  def self.register_adapter(identifier, adapter)
    adapters[identifier] = adapter
    self
  end
end

#add_persist_method

class Builder
  def add_persist_method
    mod.send(:include, Persistable::Persistence)
  end
end
module Persistable
  module Persistence
    def persist(store = nil)
      # ...
    end
  end
end

#override_persist_method

class Builder
  def override_persist_method
    return if @persist_method == :persist

    mod.send(:alias_method, @persist_method, :persist)
    mod.send(:undef_method, :persist)
  end
end

So, what is generically useful outside of this context?

Stateless Hooks - Concrete

class Builder
  attr_reader :mod

  def initialize
    # Stateless hook
    add_persist_method
  end

  def add_persist_method
    mod.send(:include, Persistable::Persistence)
  end
end

Stateless Hooks - Abstraction

class Builder
  def inclusions
    []
  end

  private

  def add_included_hook
    inclusions.each { |inclusion| mod.send(:include, inclusion) }
  end
end
class PersistableBuilder < Builder
  def inclusions
    [Persistable::Persistence]
  end
end

Stateful Hooks - Concrete

class Builder
  def initialize
    # Stateful hooks
    add_adapter
    override_persist_method
  end
end

Stateful Hooks - Abstraction

class Builder
  def hooks
    []
  end

  private

  def add_defined_hooks
    hooks.each { |hook| send(:hook) }
  end
end
class PersistableBuilder < Builder
  def hooks
    [:add_adapter, :override_persist_method]
  end

  private

  def add_adapter
    mod.send(:include, Persistable.adapters[@adapter_type])
  end

  def override_persist_method
    # ...
  end
end

Buildable DSL?

module MyBuildableModule
  include Buildable

  builder MyBuilder
end
class MyClass
  include MyBuildableModule.new(pet: :peeve)
end

Names for Built Modules?

class UnconfiguredPersistableHash < Hash
  include Hashie::Extensions::Persistable
end

hash = UnconfiguredPersistableHash.new
hash.class.ancestors.map(&:to_s)
# => [
  "UnconfiguredPersistableHash",
  "#<Module:0x005591dc2d51e0>",
  "Hashie::Extensions::Persistable::Persistence",
  "Hashie::Extensions::Persistable::Json",
  "Hashie::Extensions::Persistable",
  "Hash",
  # ...]

I'm still working out the details of what I want to include.

Is there anything you can extract into a gem?

Credits

  • GrĂ©gory Horion's syntax suggestion
  • Piotr Solnica's Virtus gem

Presentation available at:

https://michaelherold.github.io/module_presentation

Source code will (likely) be available soon at:

https://github.com/michaelherold/module_builder

Want to chat?

  • michael.j.herold@gmail.com
  • michaelherold
  • mherold