bahuvrihi (owner)

Revisions

gist: 181961 Download_button fork
public
Description:
A DSL pattern supporting inheritance
Public Clone URL: git://gist.github.com/181961.git
Embed All Files: show embed
dsl.rb #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# 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
dsl_benchmark.rb #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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
dsl_test.rb #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
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
 
misc_test.rb #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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
 
module_test.rb #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
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