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 +4 -0
- data/README.rdoc +20 -2
- data/Version.txt +1 -1
- data/lib/iron/extensions/dsl_proxy.rb +112 -0
- data/spec/extensions/dsl_proxy_spec.rb +86 -0
- metadata +6 -4
data/History.txt
CHANGED
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) # =>
|
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
|
-
*
|
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.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
|
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-
|
12
|
+
date: 2012-03-07 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
16
|
-
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: *
|
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
|