easy_esi 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1 @@
1
+ 0.2.0 - new esi include format, expire all caches that use old esi includes
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source :rubygems
2
+
3
+ group :dev do
4
+ gem 'actionpack', '~>3.1.0.rc4'
5
+ gem 'redgreen'
6
+ gem 'rake'
7
+ gem 'rspec', '~>2'
8
+ gem 'jeweler'
9
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,67 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ actionpack (3.1.0.rc4)
5
+ activemodel (= 3.1.0.rc4)
6
+ activesupport (= 3.1.0.rc4)
7
+ builder (~> 3.0.0)
8
+ erubis (~> 2.7.0)
9
+ i18n (~> 0.6)
10
+ rack (~> 1.3.0)
11
+ rack-cache (~> 1.0.1)
12
+ rack-mount (~> 0.8.1)
13
+ rack-test (~> 0.6.0)
14
+ sprockets (~> 2.0.0.beta.10)
15
+ tzinfo (~> 0.3.27)
16
+ activemodel (3.1.0.rc4)
17
+ activesupport (= 3.1.0.rc4)
18
+ bcrypt-ruby (~> 2.1.4)
19
+ builder (~> 3.0.0)
20
+ i18n (~> 0.6)
21
+ activesupport (3.1.0.rc4)
22
+ multi_json (~> 1.0)
23
+ bcrypt-ruby (2.1.4)
24
+ builder (3.0.0)
25
+ diff-lcs (1.1.2)
26
+ erubis (2.7.0)
27
+ git (1.2.5)
28
+ hike (1.1.0)
29
+ i18n (0.6.0)
30
+ jeweler (1.6.2)
31
+ bundler (~> 1.0)
32
+ git (>= 1.2.5)
33
+ rake
34
+ multi_json (1.0.3)
35
+ rack (1.3.0)
36
+ rack-cache (1.0.2)
37
+ rack (>= 0.4)
38
+ rack-mount (0.8.1)
39
+ rack (>= 1.0.0)
40
+ rack-test (0.6.0)
41
+ rack (>= 1.0)
42
+ rake (0.9.2)
43
+ redgreen (1.2.2)
44
+ rspec (2.6.0)
45
+ rspec-core (~> 2.6.0)
46
+ rspec-expectations (~> 2.6.0)
47
+ rspec-mocks (~> 2.6.0)
48
+ rspec-core (2.6.4)
49
+ rspec-expectations (2.6.0)
50
+ diff-lcs (~> 1.1.2)
51
+ rspec-mocks (2.6.0)
52
+ sprockets (2.0.0.beta.10)
53
+ hike (~> 1.0)
54
+ rack (~> 1.0)
55
+ tilt (!= 1.3.0, ~> 1.1)
56
+ tilt (1.3.2)
57
+ tzinfo (0.3.28)
58
+
59
+ PLATFORMS
60
+ ruby
61
+
62
+ DEPENDENCIES
63
+ actionpack (~> 3.1.0.rc4)
64
+ jeweler
65
+ rake
66
+ redgreen
67
+ rspec (~> 2)
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'rake/testtask'
2
+ Rake::TestTask.new(:default) do |test|
3
+ test.libs << 'lib'
4
+ test.pattern = 'test/**/*_test.rb'
5
+ test.verbose = true
6
+ end
7
+
8
+ begin
9
+ require 'jeweler'
10
+ Jeweler::Tasks.new do |gem|
11
+ gem.name = 'easy_esi'
12
+ gem.summary = "Rails: Cached pages with updated partials"
13
+ gem.email = "michael@grosser.it"
14
+ gem.homepage = "http://github.com/grosser/#{gem.name}"
15
+ gem.authors = ["Michael Grosser"]
16
+ end
17
+
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: gem install jeweler"
21
+ end
data/Readme.md ADDED
@@ -0,0 +1,72 @@
1
+ Cached pages with updated partials.<br/>
2
+
3
+ Serve completely cached pages, but keep e.g. "Welcome <%= current_user %>" updated.<br/>
4
+ Does not influence not-cached actions.
5
+
6
+ Basics
7
+ ======
8
+ **Up and running in 23 seconds or money back!**
9
+
10
+ ### Install (5s)
11
+ [Rails 2 Version](http://github.com/grosser/easy_esi/tree/rails2)
12
+
13
+ gem install easy_esi
14
+ Or
15
+
16
+ rails plugin install git://github.com/grosser/easy_esi.git
17
+
18
+ ### Action-cache the controller (5s)
19
+ class UsersController < ApplicationController
20
+ caches_action :show
21
+ enable_esi
22
+ end
23
+
24
+ ### Refactor partials that should stay updated (3s)
25
+ Has no effect for non-esi controllers!
26
+ <%= render 'profile' %>
27
+ IS NOW
28
+ <%= esi_render 'profile' %>
29
+
30
+ <%= render 'profile', :local_variable => :foo %>
31
+ IS NOW
32
+ <%= esi_render :partial => 'profile', :locals => {:local_variable => :foo} %>
33
+
34
+ ### Realize that its that simple (10s)
35
+
36
+ Behind the scenes
37
+ =================
38
+ `esi_render` inserts an esi-include (`<esi:include .... />`).<br/>
39
+ an `after_filter` or easy-esi-rails-cache-hack replaces these by rendering the partial.<br/>
40
+ ==> if you want to share view caches between controllers,
41
+ call enable_esi in all of them (or in ApplicationController).
42
+
43
+ Comparison
44
+ ===========
45
+ Normally ESI means having an extra server that parses `<esi>` tags and then calls your app to render these partials.
46
+ Which adds a whole lot of new problems(passing arguments, login, expiration, security...).
47
+ On top of that it will slow down your application unless you do everything so perfect that it gets
48
+ faster then a action-cached request (which is really hard...).
49
+
50
+ With 'hard' Esi, each `<esi>` tag causes a new, (yet small) request which needs to load all data (instance-variables) anew.<br/>
51
+ With easy-esi, each `<esi>` tag causes a partial to be rendered, inside the current context, re-using instance-variables.
52
+
53
+ Compared to normal [ESI](http://en.wikipedia.org/wiki/Edge_Side_Includes) this means:
54
+
55
+ - Testable caching
56
+ - Enabled per-controller
57
+ - No global changes
58
+ - Deactivatable at any moment
59
+ - before-filters still work
60
+ - No extra server
61
+ - No new actions
62
+ - No duplicate server requests
63
+ - No new configuration language
64
+ - No purge requests
65
+ - No external dependencies
66
+ - ...
67
+
68
+ Author
69
+ ======
70
+ [Michael Grosser](http://grosser.it)<br/>
71
+ michael@grosser.it<br/>
72
+ Hereby placed under public domain, do what you want, just do not hold me accountable...
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.1
data/easy_esi.gemspec ADDED
@@ -0,0 +1,49 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
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{easy_esi}
8
+ s.version = "0.2.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Michael Grosser"]
12
+ s.date = %q{2011-06-19}
13
+ s.email = %q{michael@grosser.it}
14
+ s.files = [
15
+ "CHANGELOG",
16
+ "Gemfile",
17
+ "Gemfile.lock",
18
+ "Rakefile",
19
+ "Readme.md",
20
+ "VERSION",
21
+ "easy_esi.gemspec",
22
+ "init.rb",
23
+ "lib/easy_esi.rb",
24
+ "test/easy_esi_test.rb",
25
+ "test/views/esi_disabled/_a_partial.erb",
26
+ "test/views/esi_disabled/show.erb",
27
+ "test/views/esi_disabled/test_filter.erb",
28
+ "test/views/esi_disabled/with_hash.erb",
29
+ "test/views/esi_enabled/_a_partial.erb",
30
+ "test/views/esi_enabled/_unserialize.erb",
31
+ "test/views/esi_enabled/serialize.erb",
32
+ "test/views/esi_enabled/show.erb",
33
+ "test/views/esi_enabled/with_hash.erb"
34
+ ]
35
+ s.homepage = %q{http://github.com/grosser/easy_esi}
36
+ s.require_paths = ["lib"]
37
+ s.rubygems_version = %q{1.6.2}
38
+ s.summary = %q{Rails: Cached pages with updated partials}
39
+
40
+ if s.respond_to? :specification_version then
41
+ s.specification_version = 3
42
+
43
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
44
+ else
45
+ end
46
+ else
47
+ end
48
+ end
49
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'easy_esi'
data/lib/easy_esi.rb ADDED
@@ -0,0 +1,79 @@
1
+ class EasyEsi
2
+ VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
3
+
4
+ def self.include_for(data)
5
+ %{<esi:include src="#{serialize(data)}"/>}.html_safe
6
+ end
7
+
8
+ def self.replace_includes(text)
9
+ text.gsub(%r{<esi:include src="[^"]*"/>}) do |match|
10
+ match =~ /"(.*)"/
11
+ yield unserialize($1)
12
+ end
13
+ end
14
+
15
+ def self.unserialize(data)
16
+ YAML.load Base64.decode64(data)
17
+ end
18
+
19
+ def self.serialize(data)
20
+ Base64.encode64(data.to_yaml).gsub("\n",'')
21
+ end
22
+
23
+ private
24
+
25
+ def self.query_to_hash(string)
26
+ string.split('&').map{|kv| kv.split('=')}.inject({}){|hash, kv| hash[kv[0]]=kv[1];hash}
27
+ end
28
+ end
29
+
30
+ class ActionView::Base
31
+ def esi_render data
32
+ if controller.esi_enabled
33
+ EasyEsi.include_for data
34
+ else
35
+ render data
36
+ end
37
+ end
38
+ end
39
+
40
+ # replace cached includes
41
+ # cache miss:
42
+ # filter_with_esi -> filter_without_esi -> after_filter -> filter_with_esi
43
+ # do not replace <include> in after filter, but after filter_without_esi
44
+ #
45
+ # cache hit:
46
+ # filter_with_esi -> filter_without_esi -> filter_with_esi
47
+ # after_filter will not be called, but <include> needs to be replaced
48
+ #
49
+ class ActionController::Caching::Actions::ActionCacheFilter
50
+ def filter_with_esi(controller, &block)
51
+ controller.instance_variable_set "@do_not_replace_esi", true
52
+ result = filter_without_esi(controller, &block)
53
+ controller.instance_variable_set "@do_not_replace_esi", false
54
+
55
+ controller.send(:render_esi) if controller.esi_enabled
56
+
57
+ result
58
+ end
59
+ alias_method_chain :filter, :esi
60
+ end
61
+
62
+ class ActionController::Base
63
+ class_inheritable_accessor :esi_enabled
64
+
65
+ def self.enable_esi
66
+ self.esi_enabled = true
67
+ after_filter :render_esi
68
+ end
69
+
70
+ protected
71
+
72
+ def render_esi
73
+ return if @do_not_replace_esi or not response_body.kind_of?(String)
74
+ self.response_body = EasyEsi.replace_includes(response_body) do |data|
75
+ data = {:partial => data} if data.kind_of?(String)
76
+ _render_template(data)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,182 @@
1
+ require 'rubygems'
2
+ require 'rspec'
3
+ require 'active_support/all'
4
+ require 'action_pack'
5
+ require 'action_controller'
6
+ require 'action_dispatch/testing/test_process'
7
+ require 'test/unit'
8
+ require 'redgreen'
9
+ $LOAD_PATH << 'lib'
10
+ require 'init'
11
+
12
+ # fake checks
13
+ require 'ostruct'
14
+ module ActionController::UrlFor
15
+ def _routes
16
+ helpers = OpenStruct.new
17
+ helpers.url_helpers = Module.new
18
+ helpers
19
+ end
20
+ end
21
+
22
+ ActionController::Base.cache_store = :memory_store
23
+
24
+ ROUTES = ActionDispatch::Routing::RouteSet.new
25
+ ROUTES.draw do
26
+ match ':controller(/:action(/:id(.:format)))'
27
+ end
28
+ ROUTES.finalize!
29
+
30
+ class EsiDisabledController < ActionController::Base
31
+ self.view_paths = 'test/views'
32
+ caches_action :show, :random, :with_hash
33
+
34
+ def random
35
+ render :text => rand.to_s
36
+ end
37
+
38
+ def show
39
+ end
40
+
41
+ def with_hash
42
+ end
43
+
44
+ def uncached
45
+ render :action => :show
46
+ end
47
+
48
+ def self._routes
49
+ ROUTES
50
+ end
51
+
52
+ def url_for(*args)
53
+ 'xxx'
54
+ end
55
+ end
56
+
57
+ # funky patch to get @routes working, in 'setup' did not work
58
+ module ActionController::TestCase::Behavior
59
+ def process_with_routes(*args)
60
+ @routes = ROUTES
61
+ process_without_routes(*args)
62
+ end
63
+ alias_method_chain :process, :routes
64
+ end
65
+
66
+ class EsiDisabledTest < ActionController::TestCase
67
+ def setup
68
+ @controller = EsiDisabledController.new
69
+ @controller.cache_store.clear
70
+ end
71
+
72
+ test "caches actions" do
73
+ get :random
74
+ cached = @response.body
75
+ get :random
76
+ @response.body.should == cached
77
+ end
78
+
79
+ test "it has esi disabled" do
80
+ @controller.esi_enabled.should == nil
81
+ end
82
+
83
+ test "it renders correctly" do
84
+ get :show, :in_action => 'A', :in_esi => 'B'
85
+ @response.body.should == "A B"
86
+ end
87
+
88
+ test "it renders old esi-partial when action was cached" do
89
+ get :show, :in_action => 'A', :in_esi => 'B'
90
+ get :show, :in_action => 'X', :in_esi => 'C'
91
+ @response.body.should == "A B"
92
+ end
93
+
94
+ test "it renders up-to-date esi-partial when action was not cached" do
95
+ get :uncached, :in_action => 'X', :in_esi => 'C'
96
+ @response.body.should == "X C"
97
+ end
98
+
99
+ test "it does not render esi includes" do
100
+ get :test_filter
101
+ @response.body.should == '<esi:include src="no_change"/>'
102
+ end
103
+
104
+ test "it renders old esi-partial with hash" do
105
+ get :with_hash, :in_esi => 'C'
106
+ get :with_hash, :in_esi => 'D'
107
+ @response.body.should == "From hash: C"
108
+ end
109
+ end
110
+
111
+
112
+ class EsiEnabledController < EsiDisabledController
113
+ enable_esi
114
+
115
+ def test_filter
116
+ end
117
+
118
+ def send_a_file
119
+ send_file "VERSION"
120
+ end
121
+
122
+ def url_for(*args)
123
+ 'xxx'
124
+ end
125
+ end
126
+
127
+ class EsiEnabledTest < ActionController::TestCase
128
+ def setup
129
+ @controller = EsiEnabledController.new
130
+ @controller.cache_store.clear
131
+ end
132
+
133
+ test "caches actions" do
134
+ get :random
135
+ cached = @response.body
136
+ get :random
137
+ @response.body.should == cached
138
+ end
139
+
140
+ test "it has esi enabled" do
141
+ @controller.esi_enabled.should == true
142
+ end
143
+
144
+ test "it renders correctly" do
145
+ get :show, :in_action => 'A', :in_esi => 'B'
146
+ @response.body.strip.should == "A B"
147
+ end
148
+
149
+ test "it up-to-date esi partial when action was cached" do
150
+ get :show, :in_action => 'A', :in_esi => 'B'
151
+ get :show, :in_action => 'X', :in_esi => 'C'
152
+ @response.body.strip.should == "A C"
153
+ end
154
+
155
+ test "it renders up-to-date esi-partial when action was not cached" do
156
+ get :uncached, :in_action => 'X', :in_esi => 'C'
157
+ @response.body.strip.should == "X C"
158
+ end
159
+
160
+ test "it renders up-to-date esi-partial with hash" do
161
+ get :with_hash, :in_esi => 'C'
162
+ get :with_hash, :in_esi => 'D'
163
+ @response.body.should == "From hash: D"
164
+ end
165
+
166
+ test "it can serialize arbitrary data" do
167
+ data = {'src' => 'something else', 1 => :x, 2 => ['"','/','___----']}
168
+ get :serialize, :render_data => data
169
+ @response.body.strip.should == data.inspect
170
+ end
171
+
172
+ test "it can send a file" do
173
+ get :send_a_file
174
+ @response.body.should == File.read('VERSION')
175
+ end
176
+ end
177
+
178
+ class TestEasyEsi < ActionController::TestCase
179
+ test "it has a VERSION" do
180
+ EasyEsi::VERSION.should =~ /^\d+\.\d+\.\d+$/
181
+ end
182
+ end
@@ -0,0 +1 @@
1
+ <%= params[:in_esi] %>
@@ -0,0 +1 @@
1
+ <%= params[:in_action] %> <%= esi_render 'a_partial' %>
@@ -0,0 +1 @@
1
+ <esi:include src="no_change"/>
@@ -0,0 +1 @@
1
+ From hash: <%= esi_render :partial => 'a_partial' %>
@@ -0,0 +1 @@
1
+ <%= params[:in_esi] %>
@@ -0,0 +1 @@
1
+ <%= data.inspect.html_safe %>
@@ -0,0 +1 @@
1
+ <%= render 'unserialize', :data => params[:render_data]%>
@@ -0,0 +1 @@
1
+ <%= params[:in_action] %> <%= esi_render 'a_partial' %>
@@ -0,0 +1 @@
1
+ From hash: <%= esi_render :partial => 'a_partial' %>
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: easy_esi
3
+ version: !ruby/object:Gem::Version
4
+ hash: 21
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 1
10
+ version: 0.2.1
11
+ platform: ruby
12
+ authors:
13
+ - Michael Grosser
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-06-19 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description:
23
+ email: michael@grosser.it
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - CHANGELOG
32
+ - Gemfile
33
+ - Gemfile.lock
34
+ - Rakefile
35
+ - Readme.md
36
+ - VERSION
37
+ - easy_esi.gemspec
38
+ - init.rb
39
+ - lib/easy_esi.rb
40
+ - test/easy_esi_test.rb
41
+ - test/views/esi_disabled/_a_partial.erb
42
+ - test/views/esi_disabled/show.erb
43
+ - test/views/esi_disabled/test_filter.erb
44
+ - test/views/esi_disabled/with_hash.erb
45
+ - test/views/esi_enabled/_a_partial.erb
46
+ - test/views/esi_enabled/_unserialize.erb
47
+ - test/views/esi_enabled/serialize.erb
48
+ - test/views/esi_enabled/show.erb
49
+ - test/views/esi_enabled/with_hash.erb
50
+ has_rdoc: true
51
+ homepage: http://github.com/grosser/easy_esi
52
+ licenses: []
53
+
54
+ post_install_message:
55
+ rdoc_options: []
56
+
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ hash: 3
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 3
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.6.2
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: "Rails: Cached pages with updated partials"
84
+ test_files: []
85
+