active_helper 0.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/README.textile ADDED
@@ -0,0 +1,188 @@
1
+ h1. ActiveHelper
2
+
3
+ _Finally - helpers with proper encapsulation, delegation, interfaces and inheritance!_
4
+
5
+
6
+ h2. Introduction
7
+
8
+ Helpers suck. They've always sucked, and they will suck on if we keep them in modules.
9
+
10
+ ActiveHelper is an attempt to pack helpers into *classes*. This brings us a few benefits
11
+
12
+ * *inheritance* helpers can be derived other helpers
13
+ * *delegation* helpers are no longer mixed into a target- the targets @use@ the helper, where the new
14
+ methods are _delegated_ to the helper instances
15
+ * *proper encapsulation* helpers don't rely blindly on instance variables - a helper defines its @needs@, the target has to provide readers
16
+ * *interfaces* a helper clearly @provides@ methods and might @use@ additional helpers
17
+
18
+ Note that ActiveHelper is a generic helper framework. Not coupled to anything like Rails or Merb. Not providing any concrete helpers. Feel free to use clean helpers in _any_ framework (including Rails and friends)!
19
+
20
+ h2. Example
21
+
22
+ Let's use the bloody MVC-View example as we find in Rails or Merb (Sinatra, too?).
23
+
24
+ We have a view which needs additional methods in order to render bullshit.
25
+
26
+
27
+ h3. Using helpers
28
+
29
+ The view wants to render tags using the TagHelper.
30
+
31
+ <pre>
32
+ class View
33
+ include ActiveHelper
34
+ end
35
+
36
+ > view.use TagHelper
37
+ </pre>
38
+
39
+ To pull-in ("import") a helper we invoke @use@ on the target instance.
40
+
41
+
42
+ h3. Interfaces
43
+
44
+ The exemplary _#tag_ method took me days to implement.
45
+
46
+ <pre>
47
+ class TagHelper < ActiveHelper::Base
48
+ provides :tag
49
+
50
+ def tag(name, attributes="")
51
+ "<#{name} #{attributes}>"
52
+ end
53
+ end
54
+ </pre>
55
+
56
+ The helper defines a part of its interface (what goes out) as it @provides@ methods.
57
+
58
+ <pre>
59
+ > view.tag(:form) # => "<form>"
60
+ </pre>
61
+
62
+
63
+ h3. Inheritance
64
+
65
+ The real power of OOP is inheritance, so why should we throw away that in favor of modules?
66
+
67
+ <pre>
68
+ class FormHelper < TagHelper
69
+ provides :form_tag
70
+
71
+ def form_tag(destination)
72
+ tag(:form, "action=#{destination}") # inherited from TagHelper.
73
+ end
74
+ end
75
+ </pre>
76
+
77
+ That's _a bit_ cleaner than blindly including 30 helper modules in another helper in another helper, isn't it?
78
+
79
+ <pre>
80
+ > view.use FormHelper
81
+ > view.tag(:form) # => "<form>"
82
+ > view.form('apotomo.de') # => "<form action=apotomo.de>"
83
+ </pre>
84
+
85
+ Obviously the view can invoke stuff from the _FormHelper_ itself and inherited methods that were exposed with @provides@.
86
+
87
+ h3. Delegation as Multiple Inheritance
88
+
89
+ What if the _#form_tag_ method needs to access another helper? In Rails, this would simply be
90
+
91
+ <pre>
92
+ def form_tag(destination)
93
+ destination = url_for(destination)
94
+ tag(:form, "action=#{destination}")
95
+ end
96
+ </pre>
97
+
98
+ The _#url_for_ methods comes from, na, do you know it? Me neither! It's mixed-in somewhere in the depths of the helper modules.
99
+
100
+ In ActiveHelper this is slightly different.
101
+
102
+ <pre>
103
+ class FormHelper < TagHelper
104
+ provides :form_tag
105
+ uses UrlHelper
106
+
107
+ def form_tag(destination)
108
+ destination = url_for(destination) # in UrlHelper.
109
+ tag(:form, "action=#{destination}")
110
+ end
111
+ end
112
+ </pre>
113
+
114
+ Hmm, our _FormHelper_ is already derived from _ActiveHelper_, how do we import additional methods?
115
+
116
+ Easy as well, the helper class @uses@ it.
117
+
118
+ So we have to know _#url_for_ is located in the _UrlHelper_ and we even have to declare our helper @uses@ another one.
119
+ That's a good thing for a) *code tidiness*, b) *good architecture* and c) *debugging*.
120
+
121
+ How would the _UrlHelper_ look like?
122
+
123
+
124
+ h3. Delegation as Interface
125
+
126
+ A traditional url helper would roughly look like this:
127
+
128
+ <pre>
129
+ def url_for(url)
130
+ protocol = @https_request? ? 'https' : 'http'
131
+ "#{protocol}://#{url}"
132
+ end
133
+ </pre>
134
+
135
+ Next chance, who or what did create _@https_request?_ and where does it live? That's _ugly_, boys!
136
+
137
+ Our helper bets on declaring its interface, again! This time we define what goes in (a "dependency").
138
+
139
+ <pre>
140
+ class UrlHelper < ActiveHelper::Base
141
+ provides :url_for
142
+ needs :https_request?
143
+
144
+ def url_for(url)
145
+ protocol = https_request? ? 'https' : 'http'
146
+ "#{protocol}://#{url}"
147
+ end
148
+ end
149
+ </pre>
150
+
151
+ It defines what it @needs@ and that's all for it. Any call to _#https_request?_ (that's a _method_) is strictly delegated back to the view instance, which has to care about satisfying dependencies.
152
+
153
+ Here's what happens in productive mode.
154
+
155
+ <pre>
156
+ > view.form('apotomo.de')
157
+ # => 11:in `url_for': undefined method `https_request?' for #<View:0xb749d4fc> (NoMethodError)
158
+ </pre>
159
+
160
+ That's conclusive, the view is insufficiently geared.
161
+
162
+ <pre>
163
+ class View
164
+ include ActiveHelper
165
+
166
+ def https_request?; false; end
167
+ end
168
+ </pre>
169
+
170
+ Now, does it work?
171
+
172
+ <pre>
173
+ > view.form_tag('go.and.use/active_helper')
174
+ # => <form action=http://go.and.use/active_helper>
175
+ </pre>
176
+
177
+ Yeah.
178
+
179
+ h2. Concepts
180
+ * Helpers are instances, when accessing a raw @@ivar@ it refers to their own instance variables
181
+ * Dependencies between different helpers and between the target (e.g. a _View_ instance) are modelled with OOP strategies: Inheritance and the declarative @#needs@.
182
+ * Naturally helpers can @use@ other helpers on instance level, a helper class @uses@ supporting helpers.
183
+
184
+ h2. License
185
+
186
+ Copyright (c) 2010, Nick Sutterer
187
+
188
+ Released under the MIT License.
data/Rakefile ADDED
@@ -0,0 +1,62 @@
1
+ # encoding: utf-8
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+ require File.join(File.dirname(__FILE__), 'lib', 'active_helper', 'version')
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => :test
9
+
10
+ desc 'Test the active_helper library.'
11
+ Rake::TestTask.new(:test) do |test|
12
+ test.libs << ['lib', 'test']
13
+ test.pattern = 'test/*_test.rb'
14
+ test.verbose = true
15
+ end
16
+
17
+
18
+ # Gem managment tasks.
19
+ #
20
+ # == Bump gem version (any):
21
+ #
22
+ # rake version:bump:major
23
+ # rake version:bump:minor
24
+ # rake version:bump:patch
25
+ #
26
+ # == Generate gemspec, build & install locally:
27
+ #
28
+ # rake gemspec
29
+ # rake build
30
+ # sudo rake install
31
+ #
32
+ # == Git tag & push to origin/master
33
+ #
34
+ # rake release
35
+ #
36
+ # == Release to Gemcutter.org:
37
+ #
38
+ # rake gemcutter:release
39
+ #
40
+ begin
41
+ gem 'jeweler'
42
+ require 'jeweler'
43
+
44
+ Jeweler::Tasks.new do |spec|
45
+ spec.name = "active_helper"
46
+ spec.version = ::ActiveHelper::VERSION
47
+ spec.summary = %{Finally - helpers with proper encapsulation, delegation, interfaces and inheritance!}
48
+ spec.description = spec.summary
49
+ spec.homepage = "http://github.com/apotonick/active_helper"
50
+ spec.authors = ["Nick Sutterer"]
51
+ spec.email = "apotonick@gmail.com"
52
+
53
+ spec.files = FileList["[A-Z]*", File.join(*%w[{lib,test} ** *]).to_s]
54
+
55
+ spec.add_dependency 'activesupport', '>= 2.3.0' # Dependencies and minimum versions?
56
+ end
57
+
58
+ Jeweler::GemcutterTasks.new
59
+ rescue LoadError
60
+ puts "Jeweler - or one of its dependencies - is not available. " <<
61
+ "Install it with: sudo gem install jeweler -s http://gemcutter.org"
62
+ end
@@ -0,0 +1,35 @@
1
+ require 'active_support'
2
+ require 'forwardable'
3
+
4
+
5
+ module ActiveHelper
6
+ module GenericMethods
7
+ def use_for(helper_class, target)
8
+ extend ::SingleForwardable
9
+
10
+ helper_ivar_name = ivar_name_for(helper_class)
11
+ helper_instance = helper_class.new(target)
12
+
13
+ instance_variable_set(helper_ivar_name, helper_instance)
14
+ helper_class.helper_methods.each do |meth|
15
+ def_delegator helper_ivar_name, meth
16
+ end
17
+ end
18
+
19
+ protected
20
+ # Unique ivar name for the helper class in the expanding target.
21
+ def ivar_name_for(object)
22
+ ('@__active_helper_'+("#{object.to_s}".underscore.gsub(/[\/<>@#:]/, ""))).to_sym
23
+ end
24
+ end
25
+
26
+
27
+ include GenericMethods
28
+ # Expands the target *instance* with the provided methods from +helper_class+ by delegating 'em back to a private helper
29
+ # Expands only the helped instance itself, not the class.
30
+ def use (helper_class)
31
+ use_for(helper_class, self)
32
+ end
33
+ end
34
+
35
+ require 'active_helper/base'
@@ -0,0 +1,54 @@
1
+ module ActiveHelper
2
+ class Base
3
+ class_inheritable_array :helper_methods
4
+ self.helper_methods = []
5
+
6
+ class_inheritable_array :parent_readers
7
+ self.parent_readers = []
8
+
9
+ class_inheritable_array :class_helpers
10
+ self.class_helpers = []
11
+
12
+ class << self
13
+ # Add public methods to the helper's interface. Only methods listed here will
14
+ # be used to expand the target.
15
+ def provides(*methods)
16
+ helper_methods.push(*methods)
17
+ end
18
+
19
+ def needs(*methods)
20
+ parent_readers.push(*methods).uniq!
21
+ end
22
+
23
+ def uses(*classes)
24
+ class_helpers.push(*classes).uniq!
25
+ end
26
+ end
27
+
28
+
29
+ include GenericMethods
30
+ attr_reader :parent
31
+
32
+ def initialize(parent=nil)
33
+ @parent = parent
34
+ extend SingleForwardable
35
+ add_parent_readers!
36
+ add_class_helpers!
37
+ end
38
+
39
+ def use(helper_class)
40
+ use_for(helper_class, parent) # in GenericMethods.
41
+ end
42
+
43
+ protected
44
+ # Delegates methods declared with #needs back to parent.
45
+ def add_parent_readers!
46
+ return if @parent.blank? or self.class.parent_readers.blank?
47
+ def_delegator(:@parent, self.class.parent_readers)
48
+ end
49
+
50
+ def add_class_helpers!
51
+ self.class.class_helpers.each { |helper| use helper }
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module ActiveHelper
4
+ VERSION = '0.1.0'.freeze
5
+ end
@@ -0,0 +1,267 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class ActiveHelperTest < Test::Unit::TestCase
4
+ def helper_mock(*args)
5
+ Class.new(::ActiveHelper::Base).new(*args)
6
+ end
7
+
8
+ def helper_in(helper_class, target)
9
+ target.instance_variable_get(target.send(:ivar_name_for, helper_class))
10
+ end
11
+
12
+ context "#initialize" do
13
+ setup do
14
+ @target = Object.new
15
+ @target.class.instance_eval { include ::ActiveHelper }
16
+ @helper_class = Class.new(::ActiveHelper::Base)
17
+ @greedy_class = @helper_class
18
+ end
19
+
20
+ should "receive a parent per default" do
21
+ @helper = ::ActiveHelper::Base.new @target
22
+ assert_equal @helper.parent, @target
23
+ end
24
+
25
+ should "also accept no parent" do
26
+ @helper = ::ActiveHelper::Base.new
27
+ assert_equal @helper.parent, nil
28
+ end
29
+
30
+
31
+
32
+ context "declaring with #needs" do
33
+ setup do
34
+ @target.instance_eval { def bottle; "cheers!"; end }
35
+ end
36
+
37
+ should_eventually "complain if parent doesn't provide accessors"
38
+
39
+ should "delegate the method to the parent when called" do
40
+ @helper_class.instance_eval { needs :bottle }
41
+ @helper = @helper_class.new(@target)
42
+ @target.use @helper.class
43
+
44
+ assert_equal "cheers!", @helper.bottle
45
+ end
46
+
47
+ # DiningHelper.use GreedyHelper
48
+ #
49
+ # GreedyHelper
50
+ # needs :bottle
51
+ #
52
+ # target
53
+ # use DiningHelper
54
+ # def bottle
55
+ should "always delegate to @target in helpers, for now" do
56
+ @greedy_class.instance_eval { needs :bottle }
57
+
58
+ @dining = helper_mock(@target)
59
+ @dining.use @greedy_class
60
+
61
+ assert_equal 'cheers!', helper_in(@greedy_class, @dining).bottle
62
+ end
63
+ end
64
+ end
65
+
66
+ context "With #parent_readers and #uses," do
67
+ setup do
68
+ @target = Object.new
69
+ @target.class.instance_eval { include ::ActiveHelper }
70
+ @helper_class = Class.new(::ActiveHelper::Base)
71
+ @helper = @helper_class.new
72
+ end
73
+
74
+ context "#parent_reader" do
75
+ should "yield an empty array on a fresh instance" do
76
+ assert_equal [], @helper.parent_readers
77
+ end
78
+
79
+ should "return the method names defined with #needs" do
80
+ @helper.class.instance_eval { needs :controller, :view }
81
+ assert_equal [:controller, :view], @helper.parent_readers
82
+ end
83
+
84
+ context "with inheritance" do
85
+ setup do
86
+ @helper.class.instance_eval { needs :bottle, :glass }
87
+ @dining = Class.new(@helper.class).new
88
+ end
89
+
90
+ should "return the inherited parent_readers names" do
91
+ assert_equal [:bottle, :glass], @dining.parent_readers
92
+ end
93
+
94
+ should "return also the inherited parent_readers names" do
95
+ @dining.class.instance_eval { needs :fork }
96
+ assert_equal [:bottle, :glass, :fork], @dining.parent_readers
97
+ end
98
+
99
+ should "return a unique'd parent_readers names list" do
100
+ @dining.class.instance_eval { needs :bottle }
101
+ assert_equal [:bottle, :glass], @dining.parent_readers
102
+ end
103
+ end
104
+ end
105
+
106
+
107
+ end
108
+
109
+ context "#helper_methods and #provides" do
110
+ setup do
111
+ @helper = Class.new(::ActiveHelper::Base).new
112
+ end
113
+
114
+ should "initialy yield an empty array" do
115
+ assert_equal [], @helper.class.helper_methods
116
+ end
117
+
118
+ should "grow with calls to #provide" do
119
+ assert_equal [:sleep, :drink], @helper.class.provides(:sleep, :drink)
120
+ assert_equal [:sleep, :drink], @helper.class.helper_methods
121
+ end
122
+
123
+ should "inherit provided methods from its ancestor classes" do
124
+ @helper.class.provides(:sleep, :drink)
125
+ @kid = Class.new(@helper.class).new
126
+ @kid.class.provides(:eat)
127
+
128
+ assert_equal [:sleep, :drink, :eat], @kid.class.helper_methods
129
+ end
130
+ end
131
+
132
+ context "On a Helper" do
133
+ setup do
134
+ assert_respond_to ::ActiveHelper::Base, :uses
135
+ @helper = Class.new(::ActiveHelper::Base).new
136
+ assert_respond_to @helper.class, :uses
137
+ assert ! @helper.respond_to?(:eat)
138
+
139
+ class GreedyHelper < ::ActiveHelper::Base; provides :eat; end
140
+ end
141
+
142
+ context "#use" do
143
+ should "delegate the new Helper methods" do
144
+ @helper.use GreedyHelper
145
+ assert_respond_to @helper, :eat
146
+ end
147
+
148
+ should "set @parent => @target in the used Helper" do
149
+ @target = Object.new
150
+ @helper = Class.new(::ActiveHelper::Base).new(@target)
151
+ @helper.use GreedyHelper
152
+ assert_equal @target, helper_in(GreedyHelper, @helper).parent # parent of used handler is target, not the using handler!
153
+ end
154
+ end
155
+
156
+ context "#uses" do
157
+ setup do
158
+ @greedy_class = Class.new(::ActiveHelper::Base)
159
+ @greedy_class.instance_eval do
160
+ uses GreedyHelper
161
+ end
162
+ end
163
+
164
+ context "with #class_helpers" do
165
+ should "yield an empty array on a fresh instance" do
166
+ @greedy_class = Class.new(::ActiveHelper::Base)
167
+ assert_equal [], @greedy_class.class_helpers
168
+ end
169
+
170
+ should "remember the passed helpers in #class_helpers" do
171
+ assert_equal [GreedyHelper], @greedy_class.class_helpers
172
+ end
173
+
174
+ should "inherit ancesting class_helpers" do
175
+ @dining_class = Class.new(@greedy_class)
176
+ @dining_class.instance_eval do
177
+ uses Object
178
+ end
179
+
180
+ assert_equal [GreedyHelper, Object], @dining_class.class_helpers
181
+ end
182
+ end
183
+
184
+ should "respond to the new delegated Helper methods" do
185
+ @helper.class.uses GreedyHelper
186
+ assert_respond_to @helper.class.new, :eat
187
+ end
188
+
189
+ should "inherit helper methods to ancestors" do
190
+ class DiningHelper < ::ActiveHelper::Base
191
+ provides :drink
192
+ uses GreedyHelper
193
+
194
+ def drink;end
195
+ end
196
+
197
+ @helper = Class.new(DiningHelper).new
198
+ assert_respond_to @helper, :eat # from uses GreedyHelper.
199
+ assert_respond_to @helper, :drink # from DiningHelper inheritance.
200
+ end
201
+ end
202
+ end
203
+
204
+ context "On a non-helper" do
205
+ setup do
206
+ @target_class = Class.new(Object) # don't pollute Object directly.
207
+ @target_class.instance_eval { include ::ActiveHelper }
208
+ assert_respond_to @target_class, :use
209
+
210
+ @target = @target_class.new
211
+ assert ! @target.respond_to?(:eat)
212
+
213
+ class GreedyHelper < ::ActiveHelper::Base; provides :eat; end
214
+ end
215
+
216
+ context "#use" do
217
+ should "delegate new delegated helper methods" do
218
+ @target.use GreedyHelper
219
+ assert_respond_to @target, :eat
220
+ end
221
+
222
+ should "set @parent => @target in the used Helper" do
223
+ @target.use GreedyHelper
224
+ assert_equal @target, helper_in(GreedyHelper, @target).parent
225
+ end
226
+ end
227
+
228
+ #context "#uses" do
229
+ # should "delegate new delegated helper methods" do
230
+ # @target.class.uses GreedyHelper
231
+ # assert_respond_to @target, :eat
232
+ # end
233
+ #
234
+ # should "inherit helper methods to non-helper class" do
235
+ # class DiningHelper < ::ActiveHelper::Base
236
+ # provides :drink
237
+ # uses GreedyHelper
238
+ #
239
+ # def drink;end
240
+ # end
241
+ #
242
+ # @helper = Class.new(DiningHelper).new
243
+ # assert_respond_to @helper, :eat # from uses GreedyHelper.
244
+ # assert_respond_to @helper, :drink # from DiningHelper inheritance.
245
+ # end
246
+ #end
247
+ end
248
+
249
+ context "#ivar_name_for" do
250
+ setup do
251
+ @helper = helper_mock
252
+ end
253
+
254
+ should "create a symbol for the class name" do
255
+ assert_equal '@__active_helper_object', @helper.send(:ivar_name_for, Object.new).to_s.sub(/0x.+/, "")
256
+ end
257
+
258
+ should "create a symbol for an anonym class" do
259
+ assert_equal '@__active_helper_class', @helper.send(:ivar_name_for, Class.new).to_s.sub(/0x.+/, "")
260
+ end
261
+
262
+ should "create a symbol for namespaced class" do
263
+
264
+ assert_equal '@__active_helper_active_helperbase', @helper.send(:ivar_name_for, ActiveHelper::Base).to_s.sub(/0x.+/, "")
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'shoulda'
3
+
4
+ require 'active_helper'
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_helper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nick Sutterer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-03-30 00:00:00 +02:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.3.0
24
+ version:
25
+ description: Finally - helpers with proper encapsulation, delegation, interfaces and inheritance!
26
+ email: apotonick@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README.textile
33
+ files:
34
+ - README.textile
35
+ - Rakefile
36
+ - lib/active_helper.rb
37
+ - lib/active_helper/base.rb
38
+ - lib/active_helper/version.rb
39
+ - test/active_helper_test.rb
40
+ - test/test_helper.rb
41
+ has_rdoc: true
42
+ homepage: http://github.com/apotonick/active_helper
43
+ licenses: []
44
+
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --charset=UTF-8
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.3.5
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: Finally - helpers with proper encapsulation, delegation, interfaces and inheritance!
69
+ test_files:
70
+ - test/active_helper_test.rb
71
+ - test/test_helper.rb