active_helper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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