easy_esi 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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
+