iron-extensions 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,10 @@
1
+ == 1.1.1 / 2012-03-13
2
+
3
+ * Added DslBuilder base class for additional DSL-building goodness, is basically a better blank-slate starter class than BasicObject for working with our DslProxy class
4
+ * Added dsl_accessor class method as a way to easily create DSL-style accessors
5
+ * DslProxy now only copies back instance vars when they change
6
+ * Symbol now supports #starts_with? and #ends_with?
7
+
1
8
  == 1.1.0 / 2012-03-06
2
9
 
3
10
  * Added DslProxy class and specs, which enables slim and sexy DSL construction
data/README.rdoc CHANGED
@@ -4,7 +4,7 @@ Written by Rob Morris @ Irongaze Consulting LLC (http://irongaze.com)
4
4
 
5
5
  == DESCRIPTION
6
6
 
7
- Helpful extensions to core Ruby classes
7
+ Helpful extensions to core Ruby classes, plus a little sugar for common patterns
8
8
 
9
9
  == ADDED EXTENSIONS
10
10
 
@@ -20,6 +20,19 @@ Helpful extensions to core Ruby classes
20
20
 
21
21
  [1, 2, nil, '', 'count'].list_join # => '1, 2, count'
22
22
 
23
+ * Class#dsl_accessor - helpful method for defining accessors on DSL builder-style classes
24
+
25
+ class MyBuilder
26
+ dsl_accessor :name
27
+ end
28
+ builder = MyBuilder.new
29
+ builder.name 'ProjectX'
30
+ builder.name # => 'ProjectX'
31
+ DslProxy.exec(builder) do
32
+ name 'Project Omega'
33
+ end
34
+ builder.name # => 'Project Omega'
35
+
23
36
  * Enumerable#to_hash - convert an array or other enumerable to a hash using a block or constant
24
37
 
25
38
  [:frog, :pig].to_hash {|n| n.to_s.capitalize} # => {:frog => 'Frog', :pig => 'Pig'}
@@ -117,3 +130,7 @@ After that, simply write code to make use of the new extensions and helper class
117
130
  To install, simply run:
118
131
 
119
132
  sudo gem install iron-extensions
133
+
134
+ RVM users should drop the 'sudo':
135
+
136
+ gem install iron-extensions
data/Version.txt CHANGED
@@ -1 +1 @@
1
- 1.1.0
1
+ 1.1.1
@@ -0,0 +1,10 @@
1
+ class Class
2
+
3
+ def dsl_accessor(*keys)
4
+ keys.each do |key|
5
+ class_eval "def #{key}(val = :__UNDEFINED); @#{key} = val unless val == :__UNDEFINED; @#{key}; end"
6
+ class_eval "def #{key}=(val); @#{key} = val; end"
7
+ end
8
+ end
9
+
10
+ end
@@ -0,0 +1,14 @@
1
+ # Provides a base class for building DSL (domain specific language) builder
2
+ # classes, ie classes that define a minimal subset of methods and act as aggregators
3
+ # of settings or functionality. Similar to BasicObject in the standard library, but
4
+ # has methods such as respond_to? and send that are required for any real DSL building
5
+ # effort.
6
+ class DslBuilder < Object
7
+
8
+ # Remove all methods not explicitly desired
9
+ instance_methods.each do |m|
10
+ keepers = [:inspect, :send]
11
+ undef_method m if m =~ /^[a-z]+[0-9]?$/ && !keepers.include?(m)
12
+ end
13
+
14
+ end
@@ -1,7 +1,7 @@
1
1
  # Specialty helper class for building elegant DSLs (domain-specific languages)
2
2
  # The purpose of the class is to allow seamless DSL's by allowing execution
3
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,
4
+ # all method calls proxied to a given receiver. This sounds pretty abstract,
5
5
  # so here's an example:
6
6
  #
7
7
  # class ControlBuilder
@@ -26,19 +26,19 @@
26
26
  # end
27
27
  #
28
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.
29
+ # Those calls are automatically proxied to the receiver we passed to the DslProxy.
30
30
  #
31
31
  # In quick and dirty DSLs, like Rails' migrations, you end up with a lot of
32
32
  # pointless receiver declarations for each method call, like so:
33
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? ...
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
40
41
  # end
41
- # end
42
42
  #
43
43
  # This is not a big deal if you're using a simple DSL, but when you have multiple nested
44
44
  # builders going on at once, it is ugly, pointless, and can cause bugs when
@@ -60,52 +60,116 @@ class DslProxy < BasicObject
60
60
  # block, and off you go. The passed block will be executed with all
61
61
  # block-context local and instance variables available, but with all
62
62
  # method calls sent to the receiver you pass in. The block's result will
63
- # be returned. If the receiver doesn't
63
+ # be returned.
64
+ #
65
+ # If the receiver doesn't respond_to? a method, any missing methods
66
+ # will be proxied to the enclosing context.
64
67
  def self.exec(receiver, &block) # :yields: receiver
65
- proxy = DslProxy.new(receiver, &block)
66
- return proxy._result
68
+ # Find the context within which the block was defined
69
+ context = ::Kernel.eval('self', block.binding)
70
+
71
+ # Create or re-use our proxy object
72
+ if context.respond_to?(:_to_dsl_proxy)
73
+ # If we're nested, we don't want/need a new dsl proxy, just re-use the existing one
74
+ proxy = context._to_dsl_proxy
75
+ else
76
+ # Not nested, create a new proxy for our use
77
+ proxy = DslProxy.new(context)
78
+ end
79
+
80
+ # Exec the block and return the result
81
+ proxy._proxy(receiver, &block)
67
82
  end
68
83
 
69
- # Create a new proxy and execute the passed block
70
- def initialize(builder, &block) # :yields: receiver
84
+ # Simple state setup
85
+ def initialize(context)
86
+ @_receivers = []
87
+ @_instance_original_values = {}
88
+ @_context = context
89
+ end
90
+
91
+ def _proxy(receiver, &block) # :yields: receiver
92
+ # Sanity!
93
+ raise 'Cannot proxy with a DslProxy as receiver!' if receiver.respond_to?(:_to_dsl_proxy)
94
+
95
+ if @_receivers.empty?
96
+ # On first proxy call, run each context instance variable,
97
+ # and set it to ourselves so we can proxy it
98
+ @_context.instance_variables.each do |var|
99
+ unless var.starts_with?('@_')
100
+ value = @_context.instance_variable_get(var.to_s)
101
+ @_instance_original_values[var] = value
102
+ #instance_variable_set(var, value)
103
+ instance_eval "#{var} = value"
104
+ end
105
+ end
106
+ end
107
+
71
108
  # Save the dsl target as our receiver for proxying
72
- @_receiver = builder
109
+ _push_receiver(receiver)
73
110
 
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
111
  # Run the block with ourselves as the new "self", passing the receiver in case
83
112
  # the code wants to disambiguate for some reason
84
- @_result = instance_exec(@_receiver, &block)
113
+ result = instance_exec(@_receivers.last, &block)
85
114
 
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}"))
115
+ # Pop the last receiver off the stack
116
+ _pop_receiver
117
+
118
+ if @_receivers.empty?
119
+ # Run each local instance variable and re-set it back to the context if it has changed during execution
120
+ #instance_variables.each do |var|
121
+ @_context.instance_variables.each do |var|
122
+ unless var.starts_with?('@_')
123
+ value = instance_eval("#{var}")
124
+ #value = instance_variable_get("#{var}")
125
+ if @_instance_original_values[var] != value
126
+ @_context.instance_variable_set(var.to_s, value)
127
+ end
128
+ end
129
+ end
89
130
  end
131
+
132
+ return result
90
133
  end
91
134
 
92
- # Returns value of the exec'd block
93
- def _result
94
- @_result
135
+ # For nesting multiple proxies
136
+ def _to_dsl_proxy
137
+ self
138
+ end
139
+
140
+ # Set the currently active receiver
141
+ def _push_receiver(receiver)
142
+ @_receivers.push receiver
143
+ end
144
+
145
+ # Remove the currently active receiver, restore old receiver if nested
146
+ def _pop_receiver
147
+ @_receivers.pop
95
148
  end
96
149
 
97
150
  # Proxies all calls to our receiver, or to the block's context
98
151
  # if the receiver doesn't respond_to? it.
99
152
  def method_missing(method, *args, &block)
100
- if @_receiver.respond_to?(method)
101
- @_receiver.send(method, *args, &block)
153
+ #$stderr.puts "Method missing: #{method}"
154
+ if @_receivers.last.respond_to?(method)
155
+ #$stderr.puts "Proxy [#{method}] to receiver"
156
+ @_receivers.last.__send__(method, *args, &block)
102
157
  else
103
- @_context.send(method, *args, &block)
158
+ #$stderr.puts "Proxy [#{method}] to context"
159
+ @_context.__send__(method, *args, &block)
104
160
  end
105
161
  end
106
162
 
107
- # Proxies searching for constants to the context
163
+ # Let anyone who's interested know what our proxied objects will accept
164
+ def respond_to?(method, include_private = false)
165
+ return true if method == :_to_dsl_proxy
166
+ @_receivers.last.respond_to?(method, include_private) || @_context.respond_to?(method, include_private)
167
+ end
168
+
169
+ # Proxies searching for constants to the context, so that eg Kernel::foo can actually
170
+ # find Kernel - BasicObject does not partake in the global scope!
108
171
  def self.const_missing(name)
172
+ #$stderr.puts "Constant missing: #{name} - proxy to context"
109
173
  @_context.class.const_get(name)
110
174
  end
111
175
 
@@ -9,4 +9,14 @@ class Symbol
9
9
  self.to_s.to_dashcase
10
10
  end
11
11
 
12
+ unless :test.respond_to?(:starts_with?)
13
+ def starts_with?(str)
14
+ self.to_s.starts_with?(str)
15
+ end
16
+
17
+ def ends_with?(str)
18
+ self.to_s.ends_with?(str)
19
+ end
20
+ end
21
+
12
22
  end
@@ -0,0 +1,20 @@
1
+ describe DslBuilder do
2
+
3
+ it 'should allow creating DSL-style accessors' do
4
+ class MyBuilder < DslBuilder
5
+ dsl_accessor :name
6
+ end
7
+ builder = MyBuilder.new
8
+
9
+ # Test standalone
10
+ builder.name 'ProjectX'
11
+ builder.name.should == 'ProjectX'
12
+
13
+ # Test as part of DslProxy usage (common case)
14
+ DslProxy.exec(builder) do
15
+ name 'Project Omega'
16
+ end
17
+ builder.name.should == 'Project Omega'
18
+ end
19
+
20
+ end
@@ -74,6 +74,25 @@ describe DslProxy do
74
74
  end
75
75
  end
76
76
 
77
+ it 'should proxy correctly even when nested' do
78
+ def outerfunc
79
+ 5
80
+ end
81
+ @instance_var = nil
82
+ local_var = nil
83
+ DslProxy.exec(self) do
84
+ DslProxy.exec(Object.new) do
85
+ outerfunc.should == 5
86
+ @instance_var.should be_nil
87
+ local_var.should be_nil
88
+ @instance_var = 10
89
+ local_var = 11
90
+ end
91
+ end
92
+ @instance_var.should == 10
93
+ local_var.should == 11
94
+ end
95
+
77
96
  it 'should put it all together' do
78
97
  @knob_count = 5
79
98
  controls = ControlBuilder.define do
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.1.0
4
+ version: 1.1.1
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-07 00:00:00.000000000Z
12
+ date: 2012-03-13 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &2160170500 !ruby/object:Gem::Requirement
16
+ requirement: &2164378800 !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: *2160170500
24
+ version_requirements: *2164378800
25
25
  description: Adds common extensions to core Ruby classes
26
26
  email:
27
27
  - rob@irongaze.com
@@ -30,6 +30,8 @@ extensions: []
30
30
  extra_rdoc_files: []
31
31
  files:
32
32
  - lib/iron/extensions/array.rb
33
+ - lib/iron/extensions/class.rb
34
+ - lib/iron/extensions/dsl_builder.rb
33
35
  - lib/iron/extensions/dsl_proxy.rb
34
36
  - lib/iron/extensions/enumerable.rb
35
37
  - lib/iron/extensions/file.rb
@@ -44,6 +46,7 @@ files:
44
46
  - lib/iron/extensions/string.rb
45
47
  - lib/iron/extensions/symbol.rb
46
48
  - lib/iron/extensions.rb
49
+ - spec/extensions/dsl_builder_spec.rb
47
50
  - spec/extensions/dsl_proxy_spec.rb
48
51
  - spec/extensions/enumerable_spec.rb
49
52
  - spec/extensions/kernel_spec.rb