iron-extensions 1.1.0 → 1.1.1

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,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