iron-extensions 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,7 @@
1
+ == 1.1.0 / 2012-03-06
2
+
3
+ * Added DslProxy class and specs, which enables slim and sexy DSL construction
4
+
1
5
  == 1.0.1 / 2012-03-03
2
6
 
3
7
  * Updated docs, fixed up minor packaging issues
data/README.rdoc CHANGED
@@ -57,7 +57,7 @@ Helpful extensions to core Ruby classes
57
57
 
58
58
  * Numeric#bound - bound a given number to a range
59
59
 
60
- 4.bound(5,10) # => 4
60
+ 4.bound(5,10) # => 5
61
61
 
62
62
  * Object#in? - sugar to make expressing inclusion clearer
63
63
 
@@ -84,15 +84,33 @@ Helpful extensions to core Ruby classes
84
84
  * Symbol#blank? - always false
85
85
  * Symbol#to_dashcase - same as for String
86
86
 
87
+ == ADDED CLASSES/MODULES
88
+
89
+ * DslProxy - a cool and sexy way to make powerful DSLs (domain-specific languages) look easy - see the docs for details
90
+
91
+ # DslProxy makes this code possible:
92
+ @items = ['one', 'two']
93
+ Console.out do
94
+ # No explicit receiver for DSL method calls
95
+ p 'Item List'
96
+ hr
97
+ indent do
98
+ # Even nested, local variables are still available
99
+ @items.each {|item| p item }
100
+ end
101
+ end
102
+
87
103
  == SYNOPSIS
88
104
 
89
105
  To use:
90
106
 
91
107
  require 'iron/extensions'
108
+
109
+ After that, simply write code to make use of the new extensions and helper classes.
92
110
 
93
111
  == REQUIREMENTS
94
112
 
95
- * None
113
+ * Ruby 1.9.2 or later
96
114
 
97
115
  == INSTALL
98
116
 
data/Version.txt CHANGED
@@ -1 +1 @@
1
- 1.0.1
1
+ 1.1.0
@@ -0,0 +1,112 @@
1
+ # Specialty helper class for building elegant DSLs (domain-specific languages)
2
+ # The purpose of the class is to allow seamless DSL's by allowing execution
3
+ # of blocks with the instance variables of the calling context preserved, but
4
+ # all method calls be proxied to a given receiver. This sounds pretty abstract,
5
+ # so here's an example:
6
+ #
7
+ # class ControlBuilder
8
+ # def initialize; @controls = []; end
9
+ # def control_list; @controls; end
10
+ # def knob; @controls << :knob; end
11
+ # def button; @controls << :button; end
12
+ # def switch; @controls << :switch; end
13
+ # def self.define(&block)
14
+ # @builder = self.new
15
+ # DslProxy.exec(@builder, &block)
16
+ # # Do something here with the builder's list of controls
17
+ # @builder.control_list
18
+ # end
19
+ # end
20
+ #
21
+ # @knob_count = 5
22
+ # new_list = ControlBuilder.define do
23
+ # switch
24
+ # @knob_count.times { knob }
25
+ # button
26
+ # end
27
+ #
28
+ # Notice the lack of explicit builder receiver to the calls to #switch, #knob and #button.
29
+ # Those calls are automatically proxied to the @builder we passed to the DslProxy.
30
+ #
31
+ # In quick and dirty DSLs, like Rails' migrations, you end up with a lot of
32
+ # pointless receiver declarations for each method call, like so:
33
+ #
34
+ # def change
35
+ # create_table do |t|
36
+ # t.integer :counter
37
+ # t.text :title
38
+ # t.text :desc
39
+ # # ... tired of typing "t." yet? ...
40
+ # end
41
+ # end
42
+ #
43
+ # This is not a big deal if you're using a simple DSL, but when you have multiple nested
44
+ # builders going on at once, it is ugly, pointless, and can cause bugs when
45
+ # the throwaway arg names you choose (eg 't' above) overlap in scope.
46
+ #
47
+ # In addition, simply using a yield statment loses the instance variables set in the calling
48
+ # context. This is a major pain in eg Rails views, where most of the interesting
49
+ # data resides in instance variables. You can get around this when #yield-ing by
50
+ # explicitly creating a local variable to be picked up by the closure created in the
51
+ # block, but it kind of sucks.
52
+ #
53
+ # In summary, DslProxy allows you to keep all the local and instance variable context
54
+ # from your block declarations, while proxying all method calls to a given
55
+ # receiver. If you're not building DSLs, this class is not for you, but if you are,
56
+ # I hope it helps!
57
+ class DslProxy < BasicObject
58
+
59
+ # Pass in a builder-style class, or other receiver you want set as "self" within the
60
+ # block, and off you go. The passed block will be executed with all
61
+ # block-context local and instance variables available, but with all
62
+ # method calls sent to the receiver you pass in. The block's result will
63
+ # be returned. If the receiver doesn't
64
+ def self.exec(receiver, &block) # :yields: receiver
65
+ proxy = DslProxy.new(receiver, &block)
66
+ return proxy._result
67
+ end
68
+
69
+ # Create a new proxy and execute the passed block
70
+ def initialize(builder, &block) # :yields: receiver
71
+ # Save the dsl target as our receiver for proxying
72
+ @_receiver = builder
73
+
74
+ # Find the context within which the block was defined
75
+ @_context = ::Kernel.eval('self', block.binding)
76
+ # Run each instance variable, and set it to ourselves so we can proxy it
77
+ @_context.instance_variables.each do |var|
78
+ value = @_context.instance_variable_get(var.to_s)
79
+ instance_eval "#{var} = value"
80
+ end
81
+
82
+ # Run the block with ourselves as the new "self", passing the receiver in case
83
+ # the code wants to disambiguate for some reason
84
+ @_result = instance_exec(@_receiver, &block)
85
+
86
+ # Run each instance variable, and set it to ourselves so we can proxy it
87
+ @_context.instance_variables.each do |var|
88
+ @_context.instance_variable_set(var.to_s, instance_eval("#{var}"))
89
+ end
90
+ end
91
+
92
+ # Returns value of the exec'd block
93
+ def _result
94
+ @_result
95
+ end
96
+
97
+ # Proxies all calls to our receiver, or to the block's context
98
+ # if the receiver doesn't respond_to? it.
99
+ def method_missing(method, *args, &block)
100
+ if @_receiver.respond_to?(method)
101
+ @_receiver.send(method, *args, &block)
102
+ else
103
+ @_context.send(method, *args, &block)
104
+ end
105
+ end
106
+
107
+ # Proxies searching for constants to the context
108
+ def self.const_missing(name)
109
+ @_context.class.const_get(name)
110
+ end
111
+
112
+ end
@@ -0,0 +1,86 @@
1
+ describe DslProxy do
2
+
3
+ # Sample DSL builder class for use in testing
4
+ class ControlBuilder
5
+ def initialize; @controls = []; end
6
+ def controls; @controls; end
7
+ def knob; @controls << :knob; end
8
+ def button; @controls << :button; end
9
+ def switch; @controls << :switch; end
10
+
11
+ def self.define(&block)
12
+ @builder = self.new
13
+ DslProxy.exec(@builder, &block)
14
+ @builder.controls
15
+ end
16
+ end
17
+
18
+ it 'should proxy calls to the receiver' do
19
+ receiver = Object.new
20
+ DslProxy.exec(receiver) do
21
+ self.class.name.should == 'Object'
22
+ end
23
+ end
24
+
25
+ it 'should proxy respond_to? to the receiver' do
26
+ receiver = Object.new
27
+ DslProxy.exec(receiver) do
28
+ respond_to?(:garbaz).should == false
29
+ respond_to?(:dup).should == true
30
+ end
31
+ end
32
+
33
+ it 'should proxy local variables from the binding context' do
34
+ @foo = 'bar'
35
+ DslProxy.exec(Object.new) do
36
+ @foo.should == 'bar'
37
+ end
38
+ end
39
+
40
+ it 'should propagate local variable changes back to the binding context' do
41
+ @foo = 'bar'
42
+ DslProxy.exec(Object.new) do
43
+ @foo = 'no bar!'
44
+ end
45
+ @foo.should == 'no bar!'
46
+ end
47
+
48
+ it 'should proxy missing methods on the receiver to the context' do
49
+ class TestContext
50
+ def bar
51
+ 'something'
52
+ end
53
+
54
+ def test
55
+ DslProxy.exec(Object.new) do
56
+ bar
57
+ end
58
+ end
59
+ end
60
+
61
+ TestContext.new.test.should == 'something'
62
+ end
63
+
64
+ it 'should return the result of the block' do
65
+ res = DslProxy.exec(Object.new) do
66
+ 'foo'
67
+ end
68
+ res.should == 'foo'
69
+ end
70
+
71
+ it 'should allow access to global constants' do
72
+ DslProxy.exec(self) do # Use self here, so #be_a is defined. :-)
73
+ Object.new.should be_a(Object)
74
+ end
75
+ end
76
+
77
+ it 'should put it all together' do
78
+ @knob_count = 5
79
+ controls = ControlBuilder.define do
80
+ switch
81
+ @knob_count.times { knob }
82
+ end
83
+ controls.count.should == 6
84
+ end
85
+
86
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iron-extensions
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-03 00:00:00.000000000Z
12
+ date: 2012-03-07 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &2156310200 !ruby/object:Gem::Requirement
16
+ requirement: &2160170500 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: '2.6'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *2156310200
24
+ version_requirements: *2160170500
25
25
  description: Adds common extensions to core Ruby classes
26
26
  email:
27
27
  - rob@irongaze.com
@@ -30,6 +30,7 @@ extensions: []
30
30
  extra_rdoc_files: []
31
31
  files:
32
32
  - lib/iron/extensions/array.rb
33
+ - lib/iron/extensions/dsl_proxy.rb
33
34
  - lib/iron/extensions/enumerable.rb
34
35
  - lib/iron/extensions/file.rb
35
36
  - lib/iron/extensions/fixnum.rb
@@ -43,6 +44,7 @@ files:
43
44
  - lib/iron/extensions/string.rb
44
45
  - lib/iron/extensions/symbol.rb
45
46
  - lib/iron/extensions.rb
47
+ - spec/extensions/dsl_proxy_spec.rb
46
48
  - spec/extensions/enumerable_spec.rb
47
49
  - spec/extensions/kernel_spec.rb
48
50
  - spec/extensions/numeric_spec.rb