components 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,6 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ *.log
@@ -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.
@@ -0,0 +1,148 @@
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
+ ## Troubleshooting
135
+
136
+ Q: I want to render partials from my app/views directory in a component view but it doesn't work
137
+ A: You have to add the view path of your normal views to the view paths of the components. One solution would be to overwrite the `view_paths` method in your component:
138
+
139
+ def self.view_paths
140
+ super + [Rails.root + 'app/views']
141
+ end
142
+
143
+ Q: I want to use a method from one of my helpers in app/helpers but I get a method missing error
144
+ A: Call `helper :all` in your component, just like you would in a controller
145
+
146
+ ## Copyright
147
+
148
+ Copyright (c) 2008 Lance Ivy, released under the MIT license
@@ -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 = "lightweight components for rails"
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
+
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 0
4
+ :patch: 5
@@ -0,0 +1,80 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{components}
8
+ s.version = "0.0.5"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Lance Ivy", "Alexander Lang"]
12
+ s.date = %q{2009-10-10}
13
+ s.email = %q{alex@upstream-berlin.com}
14
+ s.extra_rdoc_files = [
15
+ "README.md"
16
+ ]
17
+ s.files = [
18
+ ".document",
19
+ ".gitignore",
20
+ "MIT-LICENSE",
21
+ "README.md",
22
+ "Rakefile",
23
+ "VERSION.yml",
24
+ "components.gemspec",
25
+ "generators/component/component_generator.rb",
26
+ "generators/component/templates/component_template.rb",
27
+ "generators/component/templates/view_template.rb",
28
+ "lib/components.rb",
29
+ "lib/components/base.rb",
30
+ "lib/components/caching.rb",
31
+ "lib/components/helpers.rb",
32
+ "lib/components/view.rb",
33
+ "rails/init.rb",
34
+ "test/action_controller_test.rb",
35
+ "test/app/components/child/one.erb",
36
+ "test/app/components/child_component.rb",
37
+ "test/app/components/hello_world/bolded.erb",
38
+ "test/app/components/hello_world/say_it_with_help.erb",
39
+ "test/app/components/hello_world_component.rb",
40
+ "test/app/components/parent/one.erb",
41
+ "test/app/components/parent/two.erb",
42
+ "test/app/components/parent_component.rb",
43
+ "test/app/components/rich_view/form.erb",
44
+ "test/app/components/rich_view/linker.erb",
45
+ "test/app/components/rich_view/urler.erb",
46
+ "test/app/components/rich_view_component.rb",
47
+ "test/app/controllers/application_controller.rb",
48
+ "test/app/controllers/hello_world_controller.rb",
49
+ "test/caching_test.rb",
50
+ "test/components_test.rb",
51
+ "test/test_helper.rb"
52
+ ]
53
+ s.homepage = %q{http://github.com/langalex/components}
54
+ s.rdoc_options = ["--charset=UTF-8"]
55
+ s.require_paths = ["lib"]
56
+ s.rubygems_version = %q{1.3.5}
57
+ s.summary = %q{lightweight components for rails}
58
+ s.test_files = [
59
+ "test/action_controller_test.rb",
60
+ "test/app/components/child_component.rb",
61
+ "test/app/components/hello_world_component.rb",
62
+ "test/app/components/parent_component.rb",
63
+ "test/app/components/rich_view_component.rb",
64
+ "test/app/controllers/application_controller.rb",
65
+ "test/app/controllers/hello_world_controller.rb",
66
+ "test/caching_test.rb",
67
+ "test/components_test.rb",
68
+ "test/test_helper.rb"
69
+ ]
70
+
71
+ if s.respond_to? :specification_version then
72
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
73
+ s.specification_version = 3
74
+
75
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
76
+ else
77
+ end
78
+ else
79
+ end
80
+ 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
@@ -0,0 +1,92 @@
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
+ component._request = options[:request]
7
+ merge_standard_component_options!(component_args, options[:standard_component_options], component.method(method).arity)
8
+ component.logger.debug "Rendering component #{name}"
9
+ component.send(method, *component_args).to_s
10
+ end
11
+
12
+ def self.merge_standard_component_options!(args, standard_options, arity)
13
+ if standard_options
14
+ # when the method's arity is positive, it only accepts a fixed list of arguments, so we can't add another.
15
+ args << {} unless args.last.is_a?(Hash) or not arity < 0
16
+ args.last.reverse_merge!(standard_options) if args.last.is_a?(Hash)
17
+ end
18
+ end
19
+
20
+ module ActionController
21
+ protected
22
+
23
+ # Renders the named component with the given arguments. The component name must indicate both
24
+ # the component class and the class' action.
25
+ #
26
+ # Example:
27
+ #
28
+ # class UsersController < ApplicationController
29
+ # def show
30
+ # render :text => component("users/details", params[:id])
31
+ # end
32
+ # end
33
+ #
34
+ # would render:
35
+ #
36
+ # class UsersComponent < Components::Base
37
+ # def details(user_id)
38
+ # "all the important details about the user, nicely marked up"
39
+ # end
40
+ # end
41
+ def component(name, *args)
42
+ Components.render(name, args,
43
+ :form_authenticity_token => (form_authenticity_token if protect_against_forgery?),
44
+ :standard_component_options => standard_component_options,
45
+ :request => request
46
+ )
47
+ end
48
+
49
+ # Override this method on your controller (probably your ApplicationController)
50
+ # to define common arguments for your components. For example, you may always
51
+ # want to provide the current user, in which case you could return {:user =>
52
+ # current_user} from this method.
53
+ #
54
+ # In order to use this, your component actions should accept an options hash
55
+ # as their last argument.
56
+ #
57
+ # I feel like this is a better solution than simply making request and
58
+ # session details directly available to the components.
59
+ def standard_component_options; end
60
+ end
61
+
62
+ module ActionView
63
+ # Renders the named component with the given arguments. The component name must indicate both
64
+ # the component class and the class' action.
65
+ #
66
+ # Example:
67
+ #
68
+ # /app/views/users/show.html.erb
69
+ #
70
+ # <%= component "users/details", @user.id %>
71
+ #
72
+ # would render:
73
+ #
74
+ # class UsersComponent < Components::Base
75
+ # def details(user_id)
76
+ # "all the important details about the user, nicely marked up"
77
+ # end
78
+ # end
79
+ def component(name, *args)
80
+ Components.render(name, args,
81
+ :form_authenticity_token => (form_authenticity_token if protect_against_forgery?),
82
+ :standard_component_options => controller.send(:standard_component_options),
83
+ :controller => controller,
84
+ :request => request
85
+ )
86
+ end
87
+ end
88
+ end
89
+
90
+ ActionController::Base.class_eval do include Components::ActionController end
91
+ ActionView::Base.class_eval do include Components::ActionView end
92
+
@@ -0,0 +1,126 @@
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, :_request #:nodoc:
9
+ delegate :request_forgery_protection_token, :allow_forgery_protection, :to => "ActionController::Base"
10
+
11
+ def protect_against_forgery? #:nodoc:
12
+ allow_forgery_protection && request_forgery_protection_token
13
+ end
14
+
15
+ class << self
16
+ # The view paths to search for templates. Typically this will only be "app/components", but
17
+ # if you have a plugin that uses Components, it may add its own directory (e.g.
18
+ # "vendor/plugins/scaffolding/components/" to this array.
19
+ def view_paths
20
+ if read_inheritable_attribute(:view_paths).nil?
21
+ default_path = File.join(RAILS_ROOT, 'app', 'components')
22
+ write_inheritable_attribute(:view_paths, [default_path])
23
+ end
24
+ read_inheritable_attribute(:view_paths)
25
+ end
26
+
27
+ def path #:nodoc:
28
+ @path ||= self.to_s.sub("Component", "").underscore
29
+ end
30
+ alias_method :controller_path, :path
31
+
32
+ attr_accessor :template
33
+ end
34
+
35
+ # must be public for access from ActionView
36
+ def logger #:nodoc:
37
+ RAILS_DEFAULT_LOGGER
38
+ end
39
+
40
+ protected
41
+
42
+ # See Components::ActionController#standard_component_options
43
+ def standard_component_options; end
44
+
45
+ # When the string your component must return is complex enough to warrant a template file,
46
+ # this will render that file and return the result. Any template engine (erb, haml, etc.)
47
+ # that ActionView is capable of using can be used for templating.
48
+ #
49
+ # All instance variables that you create in the component action will be available from
50
+ # the view. There is currently no other way to provide variables to the views.
51
+ #
52
+ # === Inferred Template Name
53
+ #
54
+ # If you call render without a file name, it will:
55
+ # * assume that the name of the calling method is also the name of the template file
56
+ # * search for the named template file in the directory of this component's views, then the directories of all parent components
57
+ #
58
+ # This means that if you have:
59
+ #
60
+ # class UsersComponent < Components::Base
61
+ # def details(user_id)
62
+ # render
63
+ # end
64
+ # end
65
+ #
66
+ # Then render will essentially assume that you meant to render "users/details", which may
67
+ # be found at "app/components/users/details.erb".
68
+ def render(file = nil)
69
+ # infer the render file basename from the caller method.
70
+ unless file
71
+ caller.first =~ /`([^']*)'/
72
+ file = $1.sub("_without_caching", '')
73
+ end
74
+
75
+ # pick the closest parent component with the file
76
+ component = self.class
77
+ result = nil
78
+ if file.include?("/")
79
+ result = render_template("#{component.path}/#{file}")
80
+ else
81
+ until result
82
+ if component.superclass == Components::Base
83
+ result = render_template("#{component.path}/#{file}")
84
+ else
85
+ result = render_template("#{component.path}/#{file}") rescue nil
86
+ end
87
+ component = component.superclass
88
+ end
89
+ end
90
+ result
91
+ end
92
+
93
+ # creates and returns a view object for rendering the current action.
94
+ # note that this freezes knowledge of view_paths
95
+ def template #:nodoc:
96
+ template = self.class.template
97
+ if template.nil?
98
+ view_paths = self.class.view_paths
99
+ template = Components::View.new(view_paths, assigns_for_view, self)
100
+ template.extend self.class.master_helper_module
101
+ end
102
+ self.class.template = template
103
+ end
104
+
105
+ # should return a hash of all instance variables to assign to the view
106
+ def assigns_for_view #:nodoc:
107
+ (instance_variables - unassignable_instance_variables).inject({}) do |hash, var|
108
+ hash[var[1..-1]] = instance_variable_get(var)
109
+ hash
110
+ end
111
+ end
112
+
113
+ # should name all of the instance variables used by Components::Base that should _not_ be accessible from the view.
114
+ def unassignable_instance_variables #:nodoc:
115
+ %w(@template @assigns_for_view)
116
+ end
117
+
118
+ private
119
+
120
+ def render_template(name)
121
+ template.controller = self
122
+ template.send('_copy_ivars_from_controller')
123
+ template.render({:file => name}, assigns_for_view)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,187 @@
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('/').gsub(' ', '_')
162
+
163
+ key = Digest::MD5.hexdigest(key) if key.length > 200
164
+
165
+ ActiveSupport::Cache.expand_cache_key(key, :components)
166
+ end
167
+
168
+ # returns the versioning configuration for the given action, if any
169
+ def versioning(action) #:nodoc:
170
+ (self.send("#{action}_cache_options") || {})[:version]
171
+ end
172
+
173
+ private
174
+
175
+ def call(method, args)
176
+ case method
177
+ when Proc
178
+ method.call(*args)
179
+ when Symbol
180
+ send(method, *args)
181
+ end
182
+ end
183
+
184
+ def reserved_cache_option_keys
185
+ @reserved_cache_option_keys ||= [:if, :version]
186
+ end
187
+ end
File without changes
@@ -0,0 +1,13 @@
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
+ #
6
+
7
+ attr_accessor :controller
8
+
9
+ def request
10
+ controller._request
11
+ end
12
+
13
+ end
@@ -0,0 +1,3 @@
1
+ ActiveSupport::Dependencies.load_paths << RAILS_ROOT + '/app/components'
2
+
3
+ Components # trigger load
@@ -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
@@ -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,60 @@
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
+ assert_equal "components/834876df77918cf2bbfb42253d5977aa", @component.send(:cache_key, :say_it, [{:a => 'x' * 190}]), "hash keys are MD5ed when too long"
23
+ end
24
+
25
+ def test_conditional_caching
26
+ @component.say_it_cache_options = {:if => proc{false}}
27
+ @component.expects(:read_fragment).never
28
+ assert_equal "trimpanta", @component.say_it("trimpanta")
29
+ end
30
+
31
+ def test_cache_hit
32
+ @component.expects(:read_fragment).with("components/hello_world/say_it/loudly", nil).returns("LOUDLY!")
33
+ @component.expects(:say_it_without_caching).never
34
+ @component.say_it("loudly")
35
+ end
36
+
37
+ def test_cache_miss
38
+ @component.expects(:read_fragment).returns(nil)
39
+ @component.expects(:write_fragment).with("components/hello_world/say_it/frumpamumpa", "frumpamumpa", nil)
40
+ assert_equal "frumpamumpa", @component.say_it("frumpamumpa")
41
+ end
42
+
43
+ def test_expires_in_passthrough
44
+ @component.say_it_cache_options = {:expires_in => 15.minutes}
45
+ @component.expects(:write_fragment).with("components/hello_world/say_it/ninnanana", "ninnanana", {:expires_in => 15.minutes})
46
+ assert_equal "ninnanana", @component.say_it("ninnanana")
47
+ end
48
+
49
+ def test_versioned_keys
50
+ @component.say_it_cache_options = {:version => :some_named_method}
51
+ @component.expects(:some_named_method).with("rangleratta").returns(314)
52
+ assert_equal "components/hello_world/say_it/rangleratta/v314", @component.send(:cache_key, :say_it, ["rangleratta"])
53
+ end
54
+
55
+ def test_versioned_keys_dont_have_spaces
56
+ @component.say_it_cache_options = {:version => :some_named_method}
57
+ @component.expects(:some_named_method).with("rangleratta").returns('2009 10 31')
58
+ assert_equal "components/hello_world/say_it/rangleratta/v2009_10_31", @component.send(:cache_key, :say_it, ["rangleratta"])
59
+ end
60
+ 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__) + '/../rails/init'
23
+
24
+ ActionController::Base.cache_store = :memory_store
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: components
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Lance Ivy
8
+ - Alexander Lang
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-10-10 00:00:00 +02: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.md
25
+ files:
26
+ - .document
27
+ - .gitignore
28
+ - MIT-LICENSE
29
+ - README.md
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
+ - lib/components.rb
37
+ - lib/components/base.rb
38
+ - lib/components/caching.rb
39
+ - lib/components/helpers.rb
40
+ - lib/components/view.rb
41
+ - rails/init.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
+ licenses: []
63
+
64
+ post_install_message:
65
+ rdoc_options:
66
+ - --charset=UTF-8
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ version:
81
+ requirements: []
82
+
83
+ rubyforge_project:
84
+ rubygems_version: 1.3.5
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: lightweight components for rails
88
+ test_files:
89
+ - test/action_controller_test.rb
90
+ - test/app/components/child_component.rb
91
+ - test/app/components/hello_world_component.rb
92
+ - test/app/components/parent_component.rb
93
+ - test/app/components/rich_view_component.rb
94
+ - test/app/controllers/application_controller.rb
95
+ - test/app/controllers/hello_world_controller.rb
96
+ - test/caching_test.rb
97
+ - test/components_test.rb
98
+ - test/test_helper.rb