iron-extensions 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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