Skip to content

Instantly share code, notes, and snippets.

@thinkerbot
Created September 6, 2009 20:12
Show Gist options
  • Save thinkerbot/181961 to your computer and use it in GitHub Desktop.
Save thinkerbot/181961 to your computer and use it in GitHub Desktop.
A DSL pattern supporting inheritance
require 'benchmark'
# A basic version of a dsl that registers key-value pairs onto a Class. When
# looking up a key-value pair, Classes that use the dsl will check the
# registry of each ancestor in order, as if looking up a method.
#
# Call cache_ancestors! to cache the ancestry and speedup lookup.
module Dsl
def self.extended(base)
base.registry ||= {}
base.cache = nil
end
def inherited(base)
base.registry ||= {}
base.cache = nil
end
attr_accessor :registry
attr_accessor :cache
def set(key, value)
registry[key] = value
end
def value(key)
each_ancestor do |ancestor|
if ancestor.registry.has_key?(key)
return ancestor.registry[key]
end
end
end
def cache_ancestors!
@cache = ancestors
end
def each_ancestor
(cache || ancestors).each {|ancestor| yield(ancestor)}
end
end
# Now the benchmarks, first without caching (ie regenerate ancestors each
# time), then with caching to model performance with an each_ancestor method.
puts "Benchmark without cache"
class A
extend Dsl
set(:one, 1)
end
class B < A
set(:two, 2)
end
class C < B
set(:three, 3)
end
Benchmark.bm(20) do |x|
n = 100000
x.report "A.value(:one)" do
n.times { A.value(:one) }
end
x.report "B.value(:two)" do
n.times { B.value(:two) }
end
x.report "C.value(:three)" do
n.times { C.value(:three) }
end
end
puts
puts "Benchmark with cache"
A.cache_ancestors!
B.cache_ancestors!
C.cache_ancestors!
Benchmark.bm(20) do |x|
n = 100000
x.report "A.value(:one)" do
n.times { A.value(:one) }
end
x.report "B.value(:two)" do
n.times { B.value(:two) }
end
x.report "C.value(:three)" do
n.times { C.value(:three) }
end
x.report "Array.new" do
n.times { Array.new }
end
end
# Models a DSL that can be included into a class or module while
# supporting inheritance where parents can dynamically modify
# children (much as when you add a method to a parent class).
module Dsl
module ClassMethods
UNDEFINED = Object.new
def self.initialize(base)
base.registry ||= {}
end
attr_accessor :registry
def set(key, value)
registry[key] = value
end
def value(key)
each_ancestor do |ancestor|
if ancestor.registry.has_key?(key)
value = ancestor.registry[key]
return value == UNDEFINED ? nil : value
end
end
end
def values
values = {}
ancestors.reverse.each do |ancestor|
next unless ancestor.kind_of?(ClassMethods)
ancestor.registry.each_pair do |key, value|
values[key] = value unless value == UNDEFINED
end
end
values
end
def remove(key)
unless registry.has_key?(key)
raise NameError.new("#{key.inspect} not set on #{self}")
end
registry.delete(key)
end
def undefine(key)
unless values.has_key?(key)
raise NameError.new("#{key.inspect} not defined in #{self}")
end
registry[key] = UNDEFINED
end
private
def inherited(base)
ClassMethods.initialize(base)
super
end
def each_ancestor
yield(self)
blank, *ancestors = self.ancestors
ancestors.each do |ancestor|
yield(ancestor) if ancestor.kind_of?(ClassMethods)
end
nil
end
end
module ModuleMethods
module_function
def included(base)
base.extend ClassMethods
base.extend ModuleMethods unless base.kind_of?(Class)
# initialize any class variables
ClassMethods.initialize(base)
end
end
extend ModuleMethods
end
require 'benchmark'
require 'dsl'
class A
include Dsl
set :def, :a
end
class B < A
set :def, :b
end
class X
include Dsl
set :def, :x
end
class Y < X
set :def, :y
end
Benchmark.bm(20) do |x|
n = 1000000
x.report "A.value(:undef)" do
n.times { A.value(:undef) }
end
x.report "B.value(:undef)" do
n.times { B.value(:undef) }
end
x.report "A.value(:def)" do
n.times { A.value(:def) }
end
x.report "B.value(:def)" do
n.times { B.value(:def) }
end
x.report "X.value(:undef)" do
n.times { X.value(:undef) }
end
x.report "Y.value(:undef)" do
n.times { Y.value(:undef) }
end
x.report "X.value(:def)" do
n.times { X.value(:def) }
end
x.report "Y.value(:def)" do
n.times { Y.value(:def) }
end
end
require 'test/unit'
require 'dsl'
# run assumption tests assuring this model is correct
require 'module_test'
require 'misc_test'
class DslModuleTest < Test::Unit::TestCase
module X
include Dsl
set :key_x, :x
end
module Y
include X
set :key_y, :y
end
class A
include Y
set :key_a, :a
end
class B < A
set :key_b, :b
end
def test_methods_from_included_module_are_available_in_module
assert_equal :x, A.value(:key_x)
assert_equal :y, A.value(:key_y)
assert_equal :a, A.value(:key_a)
assert_equal :x, B.value(:key_x)
assert_equal :y, B.value(:key_y)
assert_equal :a, B.value(:key_a)
assert_equal :b, B.value(:key_b)
end
end
class DslModifiedModuleTest < Test::Unit::TestCase
module X
include Dsl
set :key_x, :x
end
module Y
include X
set :key_y, :y
end
class A
include Y
set :key_a, :a
end
class B < A
set :key_b, :b
end
######################################################
# late include into module X, and define a new method
module LateInModule
include Dsl
set :key_late_in_module, :late_in_module
end
module X
include LateInModule
set :key_late_x, :late_x
end
######################################################
# late include into class A, and define a new method
module LateInClass
include Dsl
set :key_late_in_class, :late_in_class
end
class A
include LateInClass
set :key_late_a, :late_a
end
######################################################
# define a class after late include
class DefinedAfterLateInclude
include X
end
######################################################
# inherit a class after late include
class InheritAfterLateInclude < A
end
def test_late_inclusion_works_for_classes_but_not_modules
assert_equal :x, A.value(:key_x)
assert_equal :y, A.value(:key_y)
assert_equal :a, A.value(:key_a)
assert_equal :late_x, A.value(:key_late_x)
assert_equal :late_a, A.value(:key_late_a)
assert_equal nil, A.value(:key_method_late_in_module)
assert_equal :late_in_class, A.value(:key_late_in_class)
assert_equal :x, B.value(:key_x)
assert_equal :y, B.value(:key_y)
assert_equal :a, B.value(:key_a)
assert_equal :b, B.value(:key_b)
assert_equal :late_x, B.value(:key_late_x)
assert_equal :late_a, B.value(:key_late_a)
assert_equal nil, B.value(:key_method_late_in_module)
assert_equal :late_in_class, B.value(:key_late_in_class)
assert_equal :x, DefinedAfterLateInclude.value(:key_x)
assert_equal :late_x, DefinedAfterLateInclude.value(:key_late_x)
assert_equal :late_in_module, DefinedAfterLateInclude.value(:key_late_in_module)
assert_equal :x, InheritAfterLateInclude.value(:key_x)
assert_equal :y, InheritAfterLateInclude.value(:key_y)
assert_equal :a, InheritAfterLateInclude.value(:key_a)
assert_equal :late_x, InheritAfterLateInclude.value(:key_late_x)
assert_equal :late_a, InheritAfterLateInclude.value(:key_late_a)
assert_equal nil, InheritAfterLateInclude.value(:key_method_late_in_module)
assert_equal :late_in_class, InheritAfterLateInclude.value(:key_late_in_class)
end
end
class DslRemovalTest < Test::Unit::TestCase
module X
include Dsl
set :key_x, :x
set :key_y, :y
set :key_z, :z
remove :key_x
end
module Y
include X
end
class Z
include Y
end
class A
include Dsl
set :key_a, :a
set :key_b, :b
remove :key_a
end
class B < A
end
class C < B
set :key_a, :A
set :key_b, :B
end
def test_remove_removes_a_defined_key_in_self_and_subclasses
assert_equal nil, Z.value(:key_x)
assert_equal :y, Z.value(:key_y)
assert_equal :z, Z.value(:key_z)
assert_equal nil, A.value(:key_a)
assert_equal :b, A.value(:key_b)
assert_equal nil, B.value(:key_a)
assert_equal :b, B.value(:key_b)
end
def test_removed_keys_can_be_redefined
assert_equal :A, C.value(:key_a)
assert_equal :B, C.value(:key_b)
end
def test_remove_raises_error_for_key_not_defined_in_self
err = assert_raises(NameError) { X.send(:remove, :key_x) }
assert_equal ":key_x not set on DslRemovalTest::X", err.message
err = assert_raises(NameError) { Y.send(:remove, :key_x) }
assert_equal ":key_x not set on DslRemovalTest::Y", err.message
err = assert_raises(NameError) { Z.send(:remove, :key_z) }
assert_equal ":key_z not set on DslRemovalTest::Z", err.message
err = assert_raises(NameError) { B.send(:remove, :key_b) }
assert_equal ":key_b not set on DslRemovalTest::B", err.message
end
end
class DslUndefTest < Test::Unit::TestCase
module X
include Dsl
set :key_x, :x
set :key_y, :y
set :key_z, :z
undefine :key_x
end
module Y
include X
undefine :key_y
end
class Z
include Y
undefine :key_z
end
class A
include Dsl
set :key_a, :a
set :key_b, :b
undefine :key_a
end
class B < A
undefine :key_b
end
class C < B
set :key_a, :A
set :key_b, :B
end
def test_undef_key_removes_a_defined_key_in_self_and_subclasses
assert_equal nil, Z.value(:key_x)
assert_equal nil, Z.value(:key_y)
assert_equal nil, Z.value(:key_z)
assert_equal nil, A.value(:key_a)
assert_equal :b, A.value(:key_b)
assert_equal nil, B.value(:key_a)
assert_equal nil, B.value(:key_b)
end
def test_undef_keys_can_be_redefined
assert_equal :A, C.value(:key_a)
assert_equal :B, C.value(:key_b)
end
def test_undef_key_raises_error_for_key_not_defined_anywhere_in_ancestry
err = assert_raises(NameError) { X.send(:undefine, :key_x) }
assert_equal ":key_x not defined in DslUndefTest::X", err.message
err = assert_raises(NameError) { Y.send(:undefine, :key_unknown) }
assert_equal ":key_unknown not defined in DslUndefTest::Y", err.message
err = assert_raises(NameError) { Z.send(:undefine, :key_unknown) }
assert_equal ":key_unknown not defined in DslUndefTest::Z", err.message
err = assert_raises(NameError) { B.send(:undefine, :key_unknown) }
assert_equal ":key_unknown not defined in DslUndefTest::B", err.message
end
end
require 'test/unit'
# Tests that constants from included modules do not cause internal conflicts
class ConstantResolutionTest < Test::Unit::TestCase
module A
module ClassMethods
def m
ClassMethods
end
end
def self.included(base)
class << base; include ClassMethods; end
end
end
module B
module ClassMethods
def n
ClassMethods
end
end
def self.included(base)
class << base; include ClassMethods; end
end
end
class C
include A
include B
end
def test_class_methods_dont_conflict
assert_equal A::ClassMethods, C.m
assert_equal B::ClassMethods, C.n
end
end
require 'test/unit'
OBJECT_ANCESTORS = if RUBY_VERSION < "1.9"
[Object, Kernel]
else
[Object, PP::ObjectMixin, Kernel, BasicObject]
end
class ModuleTest < Test::Unit::TestCase
module X
def method_x; :x; end
end
module Y
include X
def method_y; :y; end
end
class A
include Y
def method_a; :a; end
end
class B < A
def method_b; :b; end
end
def test_inclusion_adds_module_to_ancestors
assert_equal [X], X.ancestors
assert_equal [Y, X], Y.ancestors
assert_equal [A, Y, X] + OBJECT_ANCESTORS, A.ancestors
assert_equal [B,A, Y, X] + OBJECT_ANCESTORS, B.ancestors
end
def test_methods_from_included_module_are_available_in_module
assert_equal :x, A.new.method_x
assert_equal :y, A.new.method_y
assert_equal :a, A.new.method_a
assert_equal :x, B.new.method_x
assert_equal :y, B.new.method_y
assert_equal :a, B.new.method_a
assert_equal :b, B.new.method_b
end
end
# Demonstrates you CAN include a module into a class after it has been
# defined; the methods propagate down the inheritance chain. You
# CANNOT include a module in a previously included module and expect
# the same.
class ModifiedModuleTest < Test::Unit::TestCase
module X
def method_x; :x; end
end
module Y
include X
def method_y; :y; end
end
class A
include Y
def method_a; :a; end
end
class B < A
def method_b; :b; end
end
######################################################
# late include into module X, and define a new method
module LateInModule
def method_late_in_module; :late_in_module; end
end
module X
include LateInModule
def method_late_x; :late_x; end
end
######################################################
# late include into class A, and define a new method
module LateInClass
def method_late_in_class; :late_in_class; end
end
class A
include LateInClass
def method_late_a; :late_a; end
end
######################################################
# define a class after late include
class DefinedAfterLateInclude
include X
end
######################################################
# inherit a class after late include
class InheritAfterLateInclude < A
end
def test_ancestors_are_assessed_on_each_call
assert A.ancestors.object_id != A.ancestors.object_id
end
def test_late_inclusion_works_for_classes_but_not_modules
# ok LateInModule
assert_equal [X, LateInModule], X.ancestors
# no LateInModule !!
assert_equal [Y, X], Y.ancestors
# no LateInModule !!
assert_equal [A, LateInClass, Y, X] + OBJECT_ANCESTORS, A.ancestors
# no LateInModule !!
assert_equal [B, A, LateInClass, Y, X] + OBJECT_ANCESTORS, B.ancestors
# ok LateInModule
assert_equal [DefinedAfterLateInclude, X, LateInModule] + OBJECT_ANCESTORS, DefinedAfterLateInclude.ancestors
# still no LateInModule !!
assert_equal [InheritAfterLateInclude, A, LateInClass, Y, X] + OBJECT_ANCESTORS, InheritAfterLateInclude.ancestors
assert_equal :x, A.new.method_x
assert_equal :y, A.new.method_y
assert_equal :a, A.new.method_a
assert_equal :late_x, A.new.method_late_x
assert_equal :late_a, A.new.method_late_a
assert_equal false, A.new.respond_to?(:method_late_in_module)
assert_equal :late_in_class, A.new.method_late_in_class
assert_equal :x, B.new.method_x
assert_equal :y, B.new.method_y
assert_equal :a, B.new.method_a
assert_equal :b, B.new.method_b
assert_equal :late_x, B.new.method_late_x
assert_equal :late_a, B.new.method_late_a
assert_equal false, B.new.respond_to?(:method_late_in_module)
assert_equal :late_in_class, B.new.method_late_in_class
assert_equal :x, DefinedAfterLateInclude.new.method_x
assert_equal :late_x, DefinedAfterLateInclude.new.method_late_x
assert_equal :late_in_module, DefinedAfterLateInclude.new.method_late_in_module
assert_equal :x, InheritAfterLateInclude.new.method_x
assert_equal :y, InheritAfterLateInclude.new.method_y
assert_equal :a, InheritAfterLateInclude.new.method_a
assert_equal :late_x, InheritAfterLateInclude.new.method_late_x
assert_equal :late_a, InheritAfterLateInclude.new.method_late_a
assert_equal false, InheritAfterLateInclude.new.respond_to?(:method_late_in_module)
assert_equal :late_in_class, InheritAfterLateInclude.new.method_late_in_class
end
end
class MethodRemovalTest < Test::Unit::TestCase
module X
def method_x; :x; end
def method_y; :y; end
def method_z; :z; end
remove_method :method_x
end
module Y
include X
end
class Z
include Y
end
class A
def method_a; :a; end
def method_b; :b; end
remove_method :method_a
end
class B < A
end
class C < B
def method_a; :A; end
def method_b; :B; end
end
def test_remove_method_removes_a_defined_method_in_self_and_subclasses
assert_equal false, Z.new.respond_to?(:method_x)
assert_equal true, Z.new.respond_to?(:method_y)
assert_equal true, Z.new.respond_to?(:method_z)
assert_equal false, A.new.respond_to?(:method_a)
assert_equal true, A.new.respond_to?(:method_b)
assert_equal false, B.new.respond_to?(:method_a)
assert_equal true, B.new.respond_to?(:method_b)
end
def test_removed_methods_can_be_redefined
assert_equal :A, C.new.method_a
assert_equal :B, C.new.method_b
end
def test_remove_method_raises_error_for_method_not_defined_in_self
err = assert_raises(NameError) { X.send(:remove_method, :method_x) }
assert_equal "method `method_x' not defined in MethodRemovalTest::X", err.message
err = assert_raises(NameError) { Y.send(:remove_method, :method_x) }
assert_equal "method `method_x' not defined in MethodRemovalTest::Y", err.message
err = assert_raises(NameError) { Z.send(:remove_method, :method_z) }
assert_equal "method `method_z' not defined in MethodRemovalTest::Z", err.message
err = assert_raises(NameError) { B.send(:remove_method, :method_b) }
assert_equal "method `method_b' not defined in MethodRemovalTest::B", err.message
end
end
class UndefMethodTest < Test::Unit::TestCase
module X
def method_x; :x; end
def method_y; :y; end
def method_z; :z; end
undef_method :method_x
end
module Y
include X
undef_method :method_y
end
class Z
include Y
undef_method :method_z
end
class A
def method_a; :a; end
def method_b; :b; end
undef_method :method_a
end
class B < A
undef_method :method_b
end
class C < B
def method_a; :A; end
def method_b; :B; end
end
def test_undef_method_removes_a_defined_method_in_self_and_subclasses
assert_equal false, Z.new.respond_to?(:method_x)
assert_equal false, Z.new.respond_to?(:method_y)
assert_equal false, Z.new.respond_to?(:method_z)
assert_equal false, A.new.respond_to?(:method_a)
assert_equal true, A.new.respond_to?(:method_b)
assert_equal false, B.new.respond_to?(:method_a)
assert_equal false, B.new.respond_to?(:method_b)
end
def test_undefined_methods_can_be_redefined
assert_equal :A, C.new.method_a
assert_equal :B, C.new.method_b
end
def test_undef_method_raises_error_for_method_not_defined_anywhere_in_ancestry
err = assert_raises(NameError) { X.send(:undef_method, :method_x) }
assert_equal "undefined method `method_x' for module `UndefMethodTest::X'", err.message
err = assert_raises(NameError) { Y.send(:undef_method, :method_unknown) }
assert_equal "undefined method `method_unknown' for module `UndefMethodTest::Y'", err.message
err = assert_raises(NameError) { Z.send(:undef_method, :method_unknown) }
assert_equal "undefined method `method_unknown' for class `UndefMethodTest::Z'", err.message
err = assert_raises(NameError) { B.send(:undef_method, :method_unknown) }
assert_equal "undefined method `method_unknown' for class `UndefMethodTest::B'", err.message
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment