langalex-components 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Lance Ivy
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,134 @@
1
+ = Components
2
+
3
+ This plugin attempts to implement components in the simplest, cleanest, fastest way possible. Inspired by the Cells plugin (http://cells.rubyforge.org) by Nick Sutterer and Peter Bex.
4
+
5
+ A component can be thought of as a very lightweight controller with supporting view templates. The difference between a component "controller" and a Rails controller is that the component controller's methods are very much normal methods - they accept arguments, and they return a string. There is no magical auto-rendering, and there is no access to data that is not a) in the arguments or b) in the database. For example, there is no access to the request, the request parameters, or the session. This is designed to encourage good design and reuse of components, and to ensure that they don't take over the request/response job of your Rails controllers.
6
+
7
+ Speaking imprecisely, components prepare for and then render templates.
8
+
9
+ == Usage
10
+
11
+ Note that these examples are very simplistic and would be better implemented using Rails partials.
12
+
13
+ === Generator
14
+
15
+ Running `script/generator users details` will create a UsersComponent with a "details" view. You might then flesh out the templates like this:
16
+
17
+ class UsersComponent < Components::Base
18
+ def details(user_or_id)
19
+ @user = user_or_id.is_a?(User) ? user_or_id : User.find(user_or_id)
20
+ render
21
+ end
22
+ end
23
+
24
+ === From ActionController
25
+
26
+ class UsersController < ApplicationController
27
+ def show
28
+ return :text => component("users/detail", params[:id])
29
+ end
30
+ end
31
+
32
+ === From ActionView
33
+
34
+ <%= component "users/detail", @user %>
35
+
36
+ == More Features
37
+
38
+ === Caching
39
+
40
+ Any component action may be cached using the fragment caching you've configured on ActionController::Base. The command to cache a component action must come after the definition of the action itself. This is because the caching method wraps the action, which makes the caching work even if you call the action directly.
41
+
42
+ Example:
43
+
44
+ class UsersComponent < Components::Base
45
+ def details(user_id)
46
+ @user = User.find(user_id)
47
+ render
48
+ end
49
+ cache :details, :expires_in => 15.minutes
50
+ end
51
+
52
+ This will cache the returns from UsersComponent#details using a cache key like "users/details/5", where 5 is the user_id. The cache will only be good for fifteen minutes. See Components::Caching for more information.
53
+
54
+ === Helpers
55
+
56
+ All of the standard helper functionality exists for components. You may define a method on your component controller and use :helper_method to make it available in your views, or you may use :helper to add entire modules of extra methods to your views.
57
+
58
+ Be careful importing existing helpers, though, as some of them may try and break encapsulation by reading from the session, the request, or the params. You may need to rewrite these helpers so they accept the necessary information as arguments.
59
+
60
+ === Inherited Views
61
+
62
+ Assume two components:
63
+
64
+ class ParentComponent < Components::Base
65
+ def one
66
+ render
67
+ end
68
+
69
+ def two
70
+ render
71
+ end
72
+ end
73
+
74
+ class ChildComponent < ParentComponent
75
+ def one
76
+ render
77
+ end
78
+
79
+ def three
80
+ render "one"
81
+ end
82
+ end
83
+
84
+ Both methods on the ChildComponent class would first try and render "/app/components/child/one.erb", and if that file did not exist, would render "/app/components/parent/one.erb".
85
+
86
+ === Standard Argument Options
87
+
88
+ You may find yourself constantly needing to pass a standard set of options to each component. If so, you can define a method on your controller that returns a hash of standard options that will be merged with the component arguments and passed to every component.
89
+
90
+ Suppose a given component:
91
+
92
+ class GroupsComponent < Components::Base
93
+ def details(group_id, options = {})
94
+ @user = options[:user]
95
+ @group = Group.find(group_id)
96
+ render
97
+ end
98
+ end
99
+
100
+ Then the following setup:
101
+
102
+ class GroupsController < ApplicationController
103
+ def show
104
+ render :text => component("groups/details", params[:id])
105
+ end
106
+
107
+ protected
108
+
109
+ def standard_component_options
110
+ {:user => current_user}
111
+ end
112
+ end
113
+
114
+ Would expand to:
115
+
116
+ component("groups/details", params[:id], :user => current_user)
117
+
118
+ == Components Philosophy
119
+
120
+ I wrote this components plugin after evaluating a couple of existing ones, reflecting a bit, and either stealing or composing the following principles. I welcome all debate on the subject.
121
+
122
+ === Components <em>should not</em> simply embed existing controller actions.
123
+
124
+ Re-using existing controller actions introduces intractable performance problems related to redundant controller filters and duplicate request-cached variables.
125
+
126
+ === Components <em>should not</em> have the concept of a "request" or "current user".
127
+
128
+ Everything should be provided as an argument to the component - it should not have direct access to the session, the params, or any other aspect of the request. This means that components will never intelligently respond_to :html, :js, :xml, etc.
129
+
130
+ === Components _should_ complement RESTful controller design.
131
+
132
+ The path of least resistance in Rails includes RESTful controller design to reduce code redundancy. Components should only be designed for use cases where RESTful controller design is either awkward or impossible. This compatibility will reduce the maintenance effort for components and help them grow with Rails itself.
133
+
134
+ Copyright (c) 2008 Lance Ivy, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,42 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "components"
8
+ gem.summary = %Q{TODO}
9
+ gem.email = "alex@upstream-berlin.com"
10
+ gem.homepage = "http://github.com/langalex/components"
11
+ gem.authors = ["Lance Ivy", "Alexander Lang"]
12
+
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ require 'rake/testtask'
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/**/*_test.rb'
23
+ test.verbose = false
24
+ end
25
+
26
+ task :default => :test
27
+
28
+ require 'rake/rdoctask'
29
+ Rake::RDocTask.new do |rdoc|
30
+ if File.exist?('VERSION.yml')
31
+ config = YAML.load(File.read('VERSION.yml'))
32
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
33
+ else
34
+ version = ""
35
+ end
36
+
37
+ rdoc.rdoc_dir = 'rdoc'
38
+ rdoc.title = "components #{version}"
39
+ rdoc.rdoc_files.include('README*')
40
+ rdoc.rdoc_files.include('lib/**/*.rb')
41
+ end
42
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 0
4
+ :patch: 1
@@ -0,0 +1,78 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{components}
5
+ s.version = "0.0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Lance Ivy", "Alexander Lang"]
9
+ s.date = %q{2009-06-19}
10
+ s.email = %q{alex@upstream-berlin.com}
11
+ s.extra_rdoc_files = [
12
+ "README"
13
+ ]
14
+ s.files = [
15
+ ".document",
16
+ ".gitignore",
17
+ "MIT-LICENSE",
18
+ "README",
19
+ "Rakefile",
20
+ "VERSION.yml",
21
+ "components.gemspec",
22
+ "generators/component/component_generator.rb",
23
+ "generators/component/templates/component_template.rb",
24
+ "generators/component/templates/view_template.rb",
25
+ "init.rb",
26
+ "lib/components.rb",
27
+ "lib/components/base.rb",
28
+ "lib/components/caching.rb",
29
+ "lib/components/helpers.rb",
30
+ "lib/components/view.rb",
31
+ "test/action_controller_test.rb",
32
+ "test/app/components/child/one.erb",
33
+ "test/app/components/child_component.rb",
34
+ "test/app/components/hello_world/bolded.erb",
35
+ "test/app/components/hello_world/say_it_with_help.erb",
36
+ "test/app/components/hello_world_component.rb",
37
+ "test/app/components/parent/one.erb",
38
+ "test/app/components/parent/two.erb",
39
+ "test/app/components/parent_component.rb",
40
+ "test/app/components/rich_view/form.erb",
41
+ "test/app/components/rich_view/linker.erb",
42
+ "test/app/components/rich_view/urler.erb",
43
+ "test/app/components/rich_view_component.rb",
44
+ "test/app/controllers/application_controller.rb",
45
+ "test/app/controllers/hello_world_controller.rb",
46
+ "test/caching_test.rb",
47
+ "test/components_test.rb",
48
+ "test/test_helper.rb"
49
+ ]
50
+ s.has_rdoc = true
51
+ s.homepage = %q{http://github.com/langalex/components}
52
+ s.rdoc_options = ["--charset=UTF-8"]
53
+ s.require_paths = ["lib"]
54
+ s.rubygems_version = %q{1.3.1}
55
+ s.summary = %q{TODO}
56
+ s.test_files = [
57
+ "test/action_controller_test.rb",
58
+ "test/app/components/child_component.rb",
59
+ "test/app/components/hello_world_component.rb",
60
+ "test/app/components/parent_component.rb",
61
+ "test/app/components/rich_view_component.rb",
62
+ "test/app/controllers/application_controller.rb",
63
+ "test/app/controllers/hello_world_controller.rb",
64
+ "test/caching_test.rb",
65
+ "test/components_test.rb",
66
+ "test/test_helper.rb"
67
+ ]
68
+
69
+ if s.respond_to? :specification_version then
70
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
71
+ s.specification_version = 2
72
+
73
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
74
+ else
75
+ end
76
+ else
77
+ end
78
+ end
@@ -0,0 +1,13 @@
1
+ class ComponentGenerator < Rails::Generator::NamedBase
2
+ def manifest
3
+ record do |m|
4
+ m.class_collisions "#{class_name}Component"
5
+ m.directory "app/components/#{file_name}"
6
+ m.template "component_template.rb", "app/components/#{file_name}_component.rb"
7
+
8
+ actions.each do |action|
9
+ m.template "view_template.rb", "app/components/#{file_name}/#{action}.erb"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ class <%= class_name %>Component < Components::Base
2
+ <% actions.each do |action| %>
3
+ def <%= action %>
4
+ render
5
+ end
6
+ <% end %>
7
+ end
File without changes
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ ActiveSupport::Dependencies.load_paths << RAILS_ROOT + '/app/components'
2
+
3
+ Components # trigger load
@@ -0,0 +1,114 @@
1
+ module Components
2
+ class Base
3
+ include ::ActionController::UrlWriter
4
+ include ::ActionController::Helpers
5
+ include ::Components::Caching
6
+
7
+ # for request forgery protection compatibility
8
+ attr_accessor :form_authenticity_token #:nodoc:
9
+ delegate :request_forgery_protection_token, :allow_forgery_protection, :to => "ActionController::Base"
10
+ def protect_against_forgery? #:nodoc:
11
+ allow_forgery_protection && request_forgery_protection_token
12
+ end
13
+
14
+ class << self
15
+ # The view paths to search for templates. Typically this will only be "app/components", but
16
+ # if you have a plugin that uses Components, it may add its own directory (e.g.
17
+ # "vendor/plugins/scaffolding/components/" to this array.
18
+ def view_paths
19
+ if read_inheritable_attribute(:view_paths).nil?
20
+ default_path = File.join(RAILS_ROOT, 'app', 'components')
21
+ write_inheritable_attribute(:view_paths, [default_path])
22
+ end
23
+ read_inheritable_attribute(:view_paths)
24
+ end
25
+
26
+ def path #:nodoc:
27
+ @path ||= self.to_s.sub("Component", "").underscore
28
+ end
29
+ alias_method :controller_path, :path
30
+ end
31
+
32
+ # must be public for access from ActionView
33
+ def logger #:nodoc:
34
+ RAILS_DEFAULT_LOGGER
35
+ end
36
+
37
+ protected
38
+
39
+ # See Components::ActionController#standard_component_options
40
+ def standard_component_options; end
41
+
42
+ # When the string your component must return is complex enough to warrant a template file,
43
+ # this will render that file and return the result. Any template engine (erb, haml, etc.)
44
+ # that ActionView is capable of using can be used for templating.
45
+ #
46
+ # All instance variables that you create in the component action will be available from
47
+ # the view. There is currently no other way to provide variables to the views.
48
+ #
49
+ # === Inferred Template Name
50
+ #
51
+ # If you call render without a file name, it will:
52
+ # * assume that the name of the calling method is also the name of the template file
53
+ # * search for the named template file in the directory of this component's views, then the directories of all parent components
54
+ #
55
+ # This means that if you have:
56
+ #
57
+ # class UsersComponent < Components::Base
58
+ # def details(user_id)
59
+ # render
60
+ # end
61
+ # end
62
+ #
63
+ # Then render will essentially assume that you meant to render "users/details", which may
64
+ # be found at "app/components/users/details.erb".
65
+ def render(file = nil)
66
+ # infer the render file basename from the caller method.
67
+ unless file
68
+ caller.first =~ /`([^']*)'/
69
+ file = $1.sub("_without_caching", '')
70
+ end
71
+
72
+ # pick the closest parent component with the file
73
+ component = self.class
74
+ unless file.include?("/")
75
+ until exists?("#{component.path}/#{file}") or component.superclass == Components::Base
76
+ component = component.superclass
77
+ end
78
+ end
79
+
80
+ template.render(:file => "#{component.path}/#{file}")
81
+ end
82
+
83
+ # creates and returns a view object for rendering the current action.
84
+ # note that this freezes knowledge of view_paths and assigns.
85
+ def template #:nodoc:
86
+ if @template.nil?
87
+ @template = Components::View.new(self.class.view_paths, assigns_for_view, self)
88
+ @template.extend self.class.master_helper_module
89
+ end
90
+ @template
91
+ end
92
+
93
+ # should return a hash of all instance variables to assign to the view
94
+ def assigns_for_view #:nodoc:
95
+ @assigns_for_view ||= (instance_variables - unassignable_instance_variables).inject({}) do |hash, var|
96
+ hash[var[1..-1]] = instance_variable_get(var)
97
+ hash
98
+ end
99
+ end
100
+
101
+ # should name all of the instance variables used by Components::Base that should _not_ be accessible from the view.
102
+ def unassignable_instance_variables #:nodoc:
103
+ %w(@template @assigns_for_view)
104
+ end
105
+
106
+ private
107
+
108
+ def exists?(name)
109
+ template.render(:file => name)
110
+ rescue ::ActionView::MissingTemplate
111
+ false
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,185 @@
1
+ # Component caching is very fine-grained - a component is cached based on all
2
+ # of its arguments. Any more or less would result in inaccurate cache hits.
3
+ #
4
+ # === Howto
5
+ #
6
+ # First, configure fragment caching on ActionController. That cache store will be used
7
+ # for components as well.
8
+ #
9
+ # Second, decide which component actions should be cached. Some components cache better
10
+ # than others due to the nature of the arguments passed.
11
+ #
12
+ # For each component you wish to cache, add `cache :my_action` _after_ you define the
13
+ # action itself. This is necessary because your action will be wrapped with another
14
+ # method - :my_action_with_caching. This enables component actions to be cached no matter
15
+ # how they are called.
16
+ #
17
+ # === Example
18
+ #
19
+ # class UserComponent < Components::Base
20
+ # def show(user_id)
21
+ # @user = User.find(user_id)
22
+ # render
23
+ # end
24
+ # cache :show
25
+ # end
26
+ #
27
+ # === Expiration
28
+ #
29
+ # I know of three general methods to expire caches:
30
+ #
31
+ # * TTL: expires a cache after some number of seconds. This works well for content that
32
+ # is hit frequently but can stand to be a bit stale at times.
33
+ # * Versioning: caches are never actually expired, rather, they are eventually ignored
34
+ # when all new cache requests are for a new version. This only works with cache stores
35
+ # that have some kind of limited cache space, otherwise cache consumption will go
36
+ # through the metaphorical roof.
37
+ # * Direct Expiration: caches are expired by name. This does not work when cache keys
38
+ # have variable elements, or when the complete list of cache keys is not available
39
+ # or a brute-force regular expression approach.
40
+ #
41
+ # Of those three, direct expiration is not a viable option due to the variable nature of
42
+ # component cache keys. And since both of the remaining methods are best supported by some
43
+ # variation of memcache, that is the officially recommended cache store.
44
+ #
45
+ # ==== TTL Expiration
46
+ #
47
+ # If you are using Rails' :mem_cache_store for fragments, then you can set up TTL-style
48
+ # expiration by specifying an :expires_in option, like so:
49
+ #
50
+ # class UserComponent < Components::Base
51
+ # def show(user_id)
52
+ # @user = User.find(user_id)
53
+ # render
54
+ # end
55
+ # cache :show, :expires_in => 15.minutes
56
+ # end
57
+ #
58
+ # ==== Versioned Expiration
59
+ #
60
+ # Maintaining and incrementing version numbers may be implemented any number of ways. To
61
+ # use the version numbers, though, you can specify a :version option, which may either name
62
+ # a method (use a Symbol) or provide a proc. In either case, the method or proc should
63
+ # receive all of the same arguments as the action itself, and should return the version
64
+ # string.
65
+ #
66
+ # class UserComponent < Components::Base
67
+ # def show(user_id)
68
+ # @user = User.find(user_id)
69
+ # render
70
+ # end
71
+ # cache :show, :version => :show_cache_version
72
+ #
73
+ # protected
74
+ #
75
+ # def show_cache_version(user_id)
76
+ # # you may want to find your version from a model object, from memcache, or whereever.
77
+ # Version.for("users/show", user_id)
78
+ # end
79
+ # end
80
+ #
81
+ module Components::Caching
82
+ def self.included(base) #:nodoc:
83
+ base.class_eval do
84
+ extend ClassMethods
85
+ end
86
+ end
87
+
88
+ module ClassMethods
89
+ # Caches the named actions by wrapping them via alias_method_chain. May only
90
+ # be called on actions (methods) that have already been defined.
91
+ #
92
+ # Cache options will be passed through to the cache store's read/write methods.
93
+ def cache(action, cache_options = nil)
94
+ return unless ActionController::Base.cache_configured?
95
+
96
+ class_eval <<-EOL, __FILE__, __LINE__
97
+ cattr_accessor :#{action}_cache_options
98
+
99
+ def #{action}_with_caching(*args)
100
+ with_caching(:#{action}, args) do
101
+ #{action}_without_caching(*args)
102
+ end
103
+ end
104
+ alias_method_chain :#{action}, :caching
105
+ EOL
106
+ self.send("#{action}_cache_options=", cache_options)
107
+ end
108
+
109
+ def cache_store #:nodoc:
110
+ @cache_store ||= ActionController::Base.cache_store
111
+ end
112
+ end
113
+
114
+ protected
115
+
116
+ def with_caching(action, args, &block) #:nodoc:
117
+ key = cache_key(action, args)
118
+ cache_options = self.send("#{action}_cache_options") || {}
119
+ passthrough_cache_options = cache_options.reject{|k, v| reserved_cache_option_keys.include? k}
120
+ passthrough_cache_options = nil if cache_options.empty?
121
+
122
+ # conditional caching: the prohibited case
123
+ if cache_options[:if] and not call(cache_options[:if], args)
124
+ fragment = block.call
125
+ else
126
+ fragment = read_fragment(key, passthrough_cache_options)
127
+ unless fragment
128
+ fragment = block.call
129
+ write_fragment(key, fragment, passthrough_cache_options)
130
+ end
131
+ end
132
+
133
+ return fragment
134
+ end
135
+
136
+ def read_fragment(key, cache_options = nil) #:nodoc:
137
+ returning self.class.cache_store.read(key, cache_options) do |content|
138
+ logger.debug "Component Cache hit: #{key}" unless content.blank?
139
+ end
140
+ end
141
+
142
+ def write_fragment(key, content, cache_options = nil) #:nodoc:
143
+ logger.debug "Component Cache miss: #{key}"
144
+ self.class.cache_store.write(key, content, cache_options)
145
+ end
146
+
147
+ # generates the cache key for the given action/args
148
+ def cache_key(action, args = []) #:nodoc:
149
+ key_pieces = [self.class.path, action] + args
150
+
151
+ if v = call(versioning(action), args)
152
+ key_pieces << "v#{v}"
153
+ end
154
+ key = key_pieces.collect do |arg|
155
+ case arg
156
+ when ActiveRecord::Base
157
+ "#{arg.class.to_s.underscore}#{arg.id}" # note: doesn't apply to record sets
158
+ else
159
+ arg.to_param
160
+ end
161
+ end.join('/')
162
+
163
+ ActiveSupport::Cache.expand_cache_key(key, :components)
164
+ end
165
+
166
+ # returns the versioning configuration for the given action, if any
167
+ def versioning(action) #:nodoc:
168
+ (self.send("#{action}_cache_options") || {})[:version]
169
+ end
170
+
171
+ private
172
+
173
+ def call(method, args)
174
+ case method
175
+ when Proc
176
+ method.call(*args)
177
+ when Symbol
178
+ send(method, *args)
179
+ end
180
+ end
181
+
182
+ def reserved_cache_option_keys
183
+ @reserved_cache_option_keys ||= [:if, :version]
184
+ end
185
+ end
File without changes
@@ -0,0 +1,5 @@
1
+ class Components::View < ActionView::Base
2
+ delegate :protect_against_forgery?, :form_authenticity_token, :to => :controller
3
+
4
+ # TODO: rendering from a component view should be restricted to *just* rendering other components.
5
+ end
data/lib/components.rb ADDED
@@ -0,0 +1,88 @@
1
+ module Components #:nodoc:
2
+ def self.render(name, component_args = [], options = {})
3
+ klass, method = name.split('/')
4
+ component = (klass + "_component").camelcase.constantize.new
5
+ component.form_authenticity_token = options[:form_authenticity_token]
6
+ merge_standard_component_options!(component_args, options[:standard_component_options], component.method(method).arity)
7
+ component.logger.debug "Rendering component #{name}"
8
+ component.send(method, *component_args).to_s
9
+ end
10
+
11
+ def self.merge_standard_component_options!(args, standard_options, arity)
12
+ if standard_options
13
+ # when the method's arity is positive, it only accepts a fixed list of arguments, so we can't add another.
14
+ args << {} unless args.last.is_a?(Hash) or not arity < 0
15
+ args.last.reverse_merge!(standard_options) if args.last.is_a?(Hash)
16
+ end
17
+ end
18
+
19
+ module ActionController
20
+ protected
21
+
22
+ # Renders the named component with the given arguments. The component name must indicate both
23
+ # the component class and the class' action.
24
+ #
25
+ # Example:
26
+ #
27
+ # class UsersController < ApplicationController
28
+ # def show
29
+ # render :text => component("users/details", params[:id])
30
+ # end
31
+ # end
32
+ #
33
+ # would render:
34
+ #
35
+ # class UsersComponent < Components::Base
36
+ # def details(user_id)
37
+ # "all the important details about the user, nicely marked up"
38
+ # end
39
+ # end
40
+ def component(name, *args)
41
+ Components.render(name, args,
42
+ :form_authenticity_token => (form_authenticity_token if protect_against_forgery?),
43
+ :standard_component_options => standard_component_options
44
+ )
45
+ end
46
+
47
+ # Override this method on your controller (probably your ApplicationController)
48
+ # to define common arguments for your components. For example, you may always
49
+ # want to provide the current user, in which case you could return {:user =>
50
+ # current_user} from this method.
51
+ #
52
+ # In order to use this, your component actions should accept an options hash
53
+ # as their last argument.
54
+ #
55
+ # I feel like this is a better solution than simply making request and
56
+ # session details directly available to the components.
57
+ def standard_component_options; end
58
+ end
59
+
60
+ module ActionView
61
+ # Renders the named component with the given arguments. The component name must indicate both
62
+ # the component class and the class' action.
63
+ #
64
+ # Example:
65
+ #
66
+ # /app/views/users/show.html.erb
67
+ #
68
+ # <%= component "users/details", @user.id %>
69
+ #
70
+ # would render:
71
+ #
72
+ # class UsersComponent < Components::Base
73
+ # def details(user_id)
74
+ # "all the important details about the user, nicely marked up"
75
+ # end
76
+ # end
77
+ def component(name, *args)
78
+ Components.render(name, args,
79
+ :form_authenticity_token => (form_authenticity_token if protect_against_forgery?),
80
+ :standard_component_options => controller.send(:standard_component_options)
81
+ )
82
+ end
83
+ end
84
+ end
85
+
86
+ ActionController::Base.class_eval do include Components::ActionController end
87
+ ActionView::Base.class_eval do include Components::ActionView end
88
+
@@ -0,0 +1,24 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class ActionControllerTest < ActionController::TestCase
4
+ ::ActionController::Routing::Routes.draw do |map|
5
+ map.connect "/:controller/:action/:id"
6
+ end
7
+
8
+ def setup
9
+ @controller = HelloWorldController.new
10
+ @request = ActionController::TestRequest.new
11
+ @response = ActionController::TestResponse.new
12
+ end
13
+
14
+ def test_rendering_component_from_controller
15
+ get :say_it, :string => "onomatopoeia"
16
+ assert_equal "onomatopoeia", @response.body
17
+ end
18
+
19
+ def test_standard_component_options
20
+ HelloWorldController.any_instance.stubs(:standard_component_options).returns(:user => "snuffalumpagus")
21
+ HelloWorldComponent.any_instance.expects(:say_it).with("wawoowifnik", :user => "snuffalumpagus")
22
+ get :say_it, :string => "wawoowifnik"
23
+ end
24
+ end
@@ -0,0 +1 @@
1
+ child/one
@@ -0,0 +1,5 @@
1
+ class ChildComponent < ParentComponent
2
+ def one
3
+ render
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ <b><%= @string %></b>
@@ -0,0 +1 @@
1
+ <%= get_string %>
@@ -0,0 +1,27 @@
1
+ class HelloWorldComponent < Components::Base
2
+ helper_method :get_string
3
+
4
+ def say_it(string)
5
+ string
6
+ end
7
+
8
+ def say_it_with_style(string)
9
+ bolded(string)
10
+ end
11
+
12
+ def say_it_with_help(string)
13
+ @string = string
14
+ render
15
+ end
16
+
17
+ def bolded(string)
18
+ @string = string
19
+ render
20
+ end
21
+
22
+ protected
23
+
24
+ def get_string
25
+ @string
26
+ end
27
+ end
@@ -0,0 +1 @@
1
+ parent/one
@@ -0,0 +1 @@
1
+ parent/two
@@ -0,0 +1,9 @@
1
+ class ParentComponent < Components::Base
2
+ def one
3
+ render
4
+ end
5
+
6
+ def two
7
+ render
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ <% form_tag "foo" do %>
2
+ It's important to test request forgery protection.
3
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= link_to "linking is essential", @url %>
@@ -0,0 +1 @@
1
+ <%= url_for(:controller => "foo", :action => "bar") %>
@@ -0,0 +1,14 @@
1
+ class RichViewComponent < Components::Base
2
+ def urler
3
+ render
4
+ end
5
+
6
+ def form
7
+ render
8
+ end
9
+
10
+ def linker(url)
11
+ @url = url
12
+ render
13
+ end
14
+ end
File without changes
@@ -0,0 +1,5 @@
1
+ class HelloWorldController < ActionController::Base
2
+ def say_it
3
+ render :text => component("hello_world/say_it", params[:string])
4
+ end
5
+ end
@@ -0,0 +1,53 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class CachingTest < Test::Unit::TestCase
4
+ def setup
5
+ ActionController::Base.stubs(:cache_configured?).returns(true)
6
+ klass = HelloWorldComponent.dup
7
+ klass.stubs(:path).returns("hello_world")
8
+ klass.send(:cache, :say_it)
9
+ @component = klass.new
10
+ end
11
+
12
+ def test_cache_method_chaining
13
+ @component.expects(:with_caching).with(:say_it, ["janadumplet"]).returns("janadoomplet")
14
+ assert_equal "janadoomplet", @component.say_it("janadumplet")
15
+ end
16
+
17
+ def test_cache_key_generation
18
+ assert_equal "components/hello_world/say_it", @component.send(:cache_key, :say_it), "simplest cache key"
19
+ assert_equal "components/hello_world/say_it/trumpapum", @component.send(:cache_key, :say_it, ["trumpapum"]), "uses arguments"
20
+ assert_equal "components/hello_world/say_it/a/1/2/3/foo=bar", @component.send(:cache_key, :say_it, ["a", [1,2,3], {:foo => :bar}]), "handles mixed types"
21
+ assert_equal "components/hello_world/say_it/a=1&b=2", @component.send(:cache_key, :say_it, [{:b => 2, :a => 1}]), "hash keys are ordered"
22
+ end
23
+
24
+ def test_conditional_caching
25
+ @component.say_it_cache_options = {:if => proc{false}}
26
+ @component.expects(:read_fragment).never
27
+ assert_equal "trimpanta", @component.say_it("trimpanta")
28
+ end
29
+
30
+ def test_cache_hit
31
+ @component.expects(:read_fragment).with("components/hello_world/say_it/loudly", nil).returns("LOUDLY!")
32
+ @component.expects(:say_it_without_caching).never
33
+ @component.say_it("loudly")
34
+ end
35
+
36
+ def test_cache_miss
37
+ @component.expects(:read_fragment).returns(nil)
38
+ @component.expects(:write_fragment).with("components/hello_world/say_it/frumpamumpa", "frumpamumpa", nil)
39
+ assert_equal "frumpamumpa", @component.say_it("frumpamumpa")
40
+ end
41
+
42
+ def test_expires_in_passthrough
43
+ @component.say_it_cache_options = {:expires_in => 15.minutes}
44
+ @component.expects(:write_fragment).with("components/hello_world/say_it/ninnanana", "ninnanana", {:expires_in => 15.minutes})
45
+ assert_equal "ninnanana", @component.say_it("ninnanana")
46
+ end
47
+
48
+ def test_versioned_keys
49
+ @component.say_it_cache_options = {:version => :some_named_method}
50
+ @component.expects(:some_named_method).with("rangleratta").returns(314)
51
+ assert_equal "components/hello_world/say_it/rangleratta/v314", @component.send(:cache_key, :say_it, ["rangleratta"])
52
+ end
53
+ end
@@ -0,0 +1,52 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class ComponentsTest < ActionController::TestCase
4
+ def test_component_response
5
+ HelloWorldComponent.any_instance.expects(:say_it).returns("mukadoogle")
6
+ assert_equal "mukadoogle", Components.render("hello_world/say_it", ["gigglemuppit"])
7
+ end
8
+
9
+ def test_rendering_a_component_view
10
+ assert_equal "<b>pifferspangle</b>", Components.render("hello_world/say_it_with_style", ["pifferspangle"])
11
+ end
12
+
13
+ def test_implied_render_file
14
+ assert_equal "<b>foofididdums</b>", Components.render("hello_world/bolded", ["foofididdums"])
15
+ end
16
+
17
+ def test_inherited_views
18
+ assert_equal "parent/one", Components.render("parent/one")
19
+ assert_equal "parent/two", Components.render("parent/two")
20
+ assert_equal "child/one", Components.render("child/one")
21
+ assert_equal "parent/two", Components.render("child/two")
22
+ end
23
+
24
+ def test_links_in_views
25
+ rendered = Components.render("rich_view/linker", ["http://example.com"])
26
+ assert_select rendered, "a[href=http://example.com]"
27
+ end
28
+
29
+ def test_form_in_views
30
+ ActionController::Base.request_forgery_protection_token = :authenticity_token
31
+ rendered = Components.render("rich_view/form", [], :form_authenticity_token => "bluetlecrashit")
32
+ assert_select rendered, "form"
33
+ assert_select rendered, "input[type=hidden][name=authenticity_token][value=bluetlecrashit]"
34
+ end
35
+
36
+ def test_url_for_in_views
37
+ assert_nothing_raised do
38
+ ActionController::Routing::RouteSet.any_instance.stubs(:generate).returns("some_url")
39
+ Components.render("rich_view/urler")
40
+ end
41
+ end
42
+
43
+ def test_helper_methods
44
+ assert_equal "jingleheimer", Components.render("hello_world/say_it_with_help", ["jingleheimer"])
45
+ end
46
+
47
+ protected
48
+
49
+ def assert_select(content, *args)
50
+ super(HTML::Document.new(content).root, *args)
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ # fake the rails root
2
+ RAILS_ROOT = File.dirname(__FILE__)
3
+
4
+ # require support libraries
5
+ require 'test/unit'
6
+ require 'rubygems'
7
+ gem 'rails', '2.3.2'
8
+ require 'active_support'
9
+ require 'action_controller'
10
+ require 'action_controller/test_process' # for the assertions
11
+ require 'action_view'
12
+ require 'active_record'
13
+ require 'logger'
14
+ require 'mocha'
15
+
16
+ RAILS_DEFAULT_LOGGER = Logger.new(File.dirname(__FILE__) + '/debug.log')
17
+
18
+ %w(../lib app/controllers).each do |load_path|
19
+ ActiveSupport::Dependencies.load_paths << File.dirname(__FILE__) + "/" + load_path
20
+ end
21
+
22
+ require File.dirname(__FILE__) + '/../init'
23
+
24
+ ActionController::Base.cache_store = :memory_store
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: langalex-components
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Lance Ivy
8
+ - Alexander Lang
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-06-19 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description:
18
+ email: alex@upstream-berlin.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files:
24
+ - README
25
+ files:
26
+ - .document
27
+ - .gitignore
28
+ - MIT-LICENSE
29
+ - README
30
+ - Rakefile
31
+ - VERSION.yml
32
+ - components.gemspec
33
+ - generators/component/component_generator.rb
34
+ - generators/component/templates/component_template.rb
35
+ - generators/component/templates/view_template.rb
36
+ - init.rb
37
+ - lib/components.rb
38
+ - lib/components/base.rb
39
+ - lib/components/caching.rb
40
+ - lib/components/helpers.rb
41
+ - lib/components/view.rb
42
+ - test/action_controller_test.rb
43
+ - test/app/components/child/one.erb
44
+ - test/app/components/child_component.rb
45
+ - test/app/components/hello_world/bolded.erb
46
+ - test/app/components/hello_world/say_it_with_help.erb
47
+ - test/app/components/hello_world_component.rb
48
+ - test/app/components/parent/one.erb
49
+ - test/app/components/parent/two.erb
50
+ - test/app/components/parent_component.rb
51
+ - test/app/components/rich_view/form.erb
52
+ - test/app/components/rich_view/linker.erb
53
+ - test/app/components/rich_view/urler.erb
54
+ - test/app/components/rich_view_component.rb
55
+ - test/app/controllers/application_controller.rb
56
+ - test/app/controllers/hello_world_controller.rb
57
+ - test/caching_test.rb
58
+ - test/components_test.rb
59
+ - test/test_helper.rb
60
+ has_rdoc: true
61
+ homepage: http://github.com/langalex/components
62
+ post_install_message:
63
+ rdoc_options:
64
+ - --charset=UTF-8
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ version:
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: "0"
78
+ version:
79
+ requirements: []
80
+
81
+ rubyforge_project:
82
+ rubygems_version: 1.2.0
83
+ signing_key:
84
+ specification_version: 2
85
+ summary: TODO
86
+ test_files:
87
+ - test/action_controller_test.rb
88
+ - test/app/components/child_component.rb
89
+ - test/app/components/hello_world_component.rb
90
+ - test/app/components/parent_component.rb
91
+ - test/app/components/rich_view_component.rb
92
+ - test/app/controllers/application_controller.rb
93
+ - test/app/controllers/hello_world_controller.rb
94
+ - test/caching_test.rb
95
+ - test/components_test.rb
96
+ - test/test_helper.rb