rack-rsi 0.0.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/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE.markdown +21 -0
- data/README.markdown +110 -0
- data/Rakefile +2 -0
- data/TODO +3 -0
- data/examples/config.ru +13 -0
- data/examples/hello_world_app.rb +51 -0
- data/lib/rack/rsi.rb +114 -0
- data/lib/rack/rsi_version.rb +6 -0
- data/rack-rsi.gemspec +24 -0
- metadata +77 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.markdown
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2011 [Ram Singla](https://github.com/ramsingla)
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
Rack Side Include
|
2
|
+
=================
|
3
|
+
|
4
|
+
Description
|
5
|
+
-----------
|
6
|
+
|
7
|
+
Rack::RSI is an rack middleware which helps you assemble pages on
|
8
|
+
similar lines of ESI without leaving the comfort of Ruby. Rack Side
|
9
|
+
Include only support one feature of ESI i.e. <esi:include>. One of
|
10
|
+
the key differentiator from esi standard it uses ERB rather than XML
|
11
|
+
tags to assemble pages and it does not assemble pages outside the
|
12
|
+
Application.
|
13
|
+
|
14
|
+
Rationale
|
15
|
+
---------
|
16
|
+
|
17
|
+
A fair bit of what ESI offers, is in the ESI language because Akamais
|
18
|
+
customers cannot configure the Akamai edge proxies. While this is
|
19
|
+
perfectly sensible for the Akamai business model, it is of little
|
20
|
+
relevance for WebApps where the content-provider is in control of the
|
21
|
+
servers.
|
22
|
+
|
23
|
+
Also if you do not want to use a separate tier for ESI, the ESI standard
|
24
|
+
is too heavy to implement as a Rack Middleware.
|
25
|
+
|
26
|
+
ERB is much simpler to render and less CPU intensive interms of XML
|
27
|
+
parsing and generating response.
|
28
|
+
|
29
|
+
Assembling pages inside the applications is chosen deliberately because
|
30
|
+
the content for assembly is fetched from within the Rack stack without
|
31
|
+
firing any HTTP requests to the server.
|
32
|
+
|
33
|
+
Potential Scenarios
|
34
|
+
-------------------
|
35
|
+
|
36
|
+
#### Pages Decorated with Short Lived Information
|
37
|
+
|
38
|
+
Consider a case of high volume news website. Most pages on this website
|
39
|
+
contains one article decorated by ads and a "hot news" box. Without the
|
40
|
+
assembly the TTL for each of these articles need to be kept low, to keep
|
41
|
+
decorations and in particular "hot news" box fresh.
|
42
|
+
|
43
|
+
Rack Server Include assembly middleware allows you to break the page
|
44
|
+
into different fragments which can be cached differently and are
|
45
|
+
assembled just before serving the request to the client. In above case
|
46
|
+
article can be cached for an infinitely long time, with a directive to
|
47
|
+
tell the middle where what and where to include the "hot news" box from.
|
48
|
+
|
49
|
+
Each part of the section and the page containing the rack side include
|
50
|
+
directive can be cached differently.
|
51
|
+
|
52
|
+
#### Creating Dashboards
|
53
|
+
|
54
|
+
Consider an admin dashboard of an ecommerce website which shows the new
|
55
|
+
orders and new products added on the system.
|
56
|
+
|
57
|
+
If we are not using assembly middleware you would require to populate
|
58
|
+
relevant order and product object in dashboard controller which is not
|
59
|
+
well organized. Ideally orders controller should fetch order objects and
|
60
|
+
products controller should fetch the products.
|
61
|
+
|
62
|
+
Rack Server Include assembly lets you achieve this by calling two
|
63
|
+
include directives. One to new orders and one to new products which will
|
64
|
+
be served by orders and products controller respectively.
|
65
|
+
|
66
|
+
This can really help you keep you application slim and well-organized.
|
67
|
+
|
68
|
+
How do I use in Rails 3
|
69
|
+
-----------------------
|
70
|
+
|
71
|
+
in Gemfile
|
72
|
+
|
73
|
+
gem 'rack-rsi', :require => 'rack/rsi'
|
74
|
+
|
75
|
+
in config/application.rb
|
76
|
+
|
77
|
+
# Rack::RSI should be loaded high up the Rack Middleware Stack
|
78
|
+
config.middleware.insert_before ActionDispatch::Callbacks, Rack::RSI
|
79
|
+
|
80
|
+
in controller action
|
81
|
+
|
82
|
+
def foo_action
|
83
|
+
# Notify Rack::RSI to process this request
|
84
|
+
headers['rack.rsi'] = '1'
|
85
|
+
...
|
86
|
+
end
|
87
|
+
|
88
|
+
in view template foo_action.html.erb
|
89
|
+
|
90
|
+
...
|
91
|
+
<%%= rsi_include( '/path/to_include' ) %>
|
92
|
+
...
|
93
|
+
|
94
|
+
How do I use it with Bare Rack Apps
|
95
|
+
-----------------------------------
|
96
|
+
|
97
|
+
look for hello_world_app.rb in examples fold
|
98
|
+
|
99
|
+
|
100
|
+
Limitations
|
101
|
+
------------
|
102
|
+
|
103
|
+
* Only GET requests are supported as rack side includes
|
104
|
+
* Include requests should not return redirect response
|
105
|
+
* There should be no ERB tags other than ones invoking *rsi_include*.
|
106
|
+
|
107
|
+
License
|
108
|
+
-------
|
109
|
+
|
110
|
+
Copyright © 2011 Ram Singla. Released under MIT License.
|
data/Rakefile
ADDED
data/TODO
ADDED
data/examples/config.ru
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "pathname"
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(Pathname(__FILE__).expand_path.dirname)
|
4
|
+
$LOAD_PATH.unshift(Pathname(__FILE__).expand_path.dirname.parent.join("lib"))
|
5
|
+
|
6
|
+
require "rack/rsi"
|
7
|
+
require "hello_world_app"
|
8
|
+
|
9
|
+
use Rack::ShowExceptions
|
10
|
+
use Rack::Runtime
|
11
|
+
use Rack::RSI
|
12
|
+
use Rack::CommonLogger
|
13
|
+
run HelloWorldApp.new
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "rack"
|
2
|
+
|
3
|
+
class HelloWorldApp
|
4
|
+
|
5
|
+
def call(env)
|
6
|
+
request = Rack::Request.new(env)
|
7
|
+
response = Rack::Response.new
|
8
|
+
|
9
|
+
if request.path_info == "/"
|
10
|
+
action = "index"
|
11
|
+
else
|
12
|
+
action = request.path_info[1..-1].gsub(/\?.*$/, '')
|
13
|
+
end
|
14
|
+
|
15
|
+
if ACTIONS.include?(action)
|
16
|
+
send(action, request, response)
|
17
|
+
else
|
18
|
+
response.status = 404
|
19
|
+
response.write("404 Not Found")
|
20
|
+
end
|
21
|
+
|
22
|
+
response.finish
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
ACTIONS = %<index header footer>
|
28
|
+
|
29
|
+
def index(request, response)
|
30
|
+
response['rack.rsi'] = '1'
|
31
|
+
response['Cache-Control'] = 'max-age=3600'
|
32
|
+
response.write(%{
|
33
|
+
<title>HelloWorld</title>
|
34
|
+
<%= rsi_include( "/header?user=buzzmenot" ) %>
|
35
|
+
<p>Hello World!</p>
|
36
|
+
<%= rsi_include( "/footer?company=github" ) %>
|
37
|
+
}.gsub(/^\s*/, "").strip)
|
38
|
+
end
|
39
|
+
|
40
|
+
def header(request, response)
|
41
|
+
response['Cache-Control'] = 'max-age=60'
|
42
|
+
response.write( "Here comes header from Header Action exclusively for #{request.params['user']}" )
|
43
|
+
end
|
44
|
+
|
45
|
+
def footer(request, response)
|
46
|
+
response['Cache-Control'] = 'max-age=120'
|
47
|
+
response.write( "Here comes footer from Footer Action exclusively for #{request.params['company']}" )
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
data/lib/rack/rsi.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# Copyright Ram Singla (c) 2011.
|
2
|
+
# Released under MIT License
|
3
|
+
|
4
|
+
require 'rack'
|
5
|
+
require 'erb'
|
6
|
+
require 'uri'
|
7
|
+
require 'digest/md5'
|
8
|
+
require 'rack/rsi_version'
|
9
|
+
|
10
|
+
class Rack::RSI
|
11
|
+
|
12
|
+
class Error < ::RuntimeError
|
13
|
+
end
|
14
|
+
|
15
|
+
class RsiRender
|
16
|
+
|
17
|
+
def initialize( app, env, level = 0 )
|
18
|
+
@app, @env, @level = app, env, level
|
19
|
+
@headers, @body = {}, {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def rack_rsi?
|
23
|
+
@headers.values.select{ |x| x && x['rack.esi'] }.any?
|
24
|
+
end
|
25
|
+
|
26
|
+
def cache_control_headers
|
27
|
+
@headers.values.collect{ |x| ( x ? x['Cache-Control'] : nil ) || 'max-age=0' }
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_binding
|
31
|
+
return binding( )
|
32
|
+
end
|
33
|
+
|
34
|
+
def rsi_include( source )
|
35
|
+
uri = URI.parse( source )
|
36
|
+
include_env = @env.merge( "PATH_INFO" => uri.path,
|
37
|
+
"SCRIPT_NAME" => "",
|
38
|
+
"QUERY_STRING" => uri.query,
|
39
|
+
"REQUEST_METHOD" => "GET" )
|
40
|
+
begin
|
41
|
+
include_status, include_headers, include_body = @app.dup.call(include_env)
|
42
|
+
@headers[ source ] = include_headers
|
43
|
+
@body[ source ] = ( include_status == 200 ? include_body : [] )
|
44
|
+
rescue Exception => message
|
45
|
+
@body[ source ] = []
|
46
|
+
end
|
47
|
+
value = ''
|
48
|
+
@body[ source ].each{ |part| value << part }
|
49
|
+
value
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize( app )
|
55
|
+
@app = app
|
56
|
+
end
|
57
|
+
|
58
|
+
def call( env )
|
59
|
+
assemble_response( env )
|
60
|
+
end
|
61
|
+
|
62
|
+
# Assemble Response
|
63
|
+
def assemble_response( env )
|
64
|
+
vanilla_env = env.dup
|
65
|
+
vanilla_app = @app.dup
|
66
|
+
|
67
|
+
# calling app and env on orignal request
|
68
|
+
status, headers, enumerable_body = original_response = @app.call(env)
|
69
|
+
|
70
|
+
rack_rsi_flag = headers.delete('rack.rsi')
|
71
|
+
return original_response unless rack_rsi_flag
|
72
|
+
|
73
|
+
body = ""
|
74
|
+
enumerable_body.each do |part|
|
75
|
+
body << part
|
76
|
+
end
|
77
|
+
|
78
|
+
cache_control_headers = Array( headers.delete( 'Cache-Control' ) || "max-age=0" )
|
79
|
+
|
80
|
+
# Like Varnish supports upto 5 levels of ESI includes recursively
|
81
|
+
level = 0
|
82
|
+
while( rack_rsi_flag )
|
83
|
+
erb = ERB.new( body, 0 )
|
84
|
+
renderer = RsiRender.new( vanilla_app, vanilla_env, level )
|
85
|
+
body = erb.result( renderer.get_binding )
|
86
|
+
renderer.cache_control_headers.inject( cache_control_headers ){ |s,x| s.push( x ) }
|
87
|
+
rack_rsi_flag = renderer.rack_rsi?
|
88
|
+
level += 1
|
89
|
+
end
|
90
|
+
|
91
|
+
# Set ETag for the Request
|
92
|
+
headers['ETag'] = Digest::MD5.hexdigest( body )
|
93
|
+
headers.delete( 'Last-Modified' )
|
94
|
+
|
95
|
+
# For Assembled Pages Cache-Control to be set as private, with
|
96
|
+
# max-age=<minimum max-age of all the requests that are assembled>
|
97
|
+
# and should be revalidate on stale
|
98
|
+
min_max_age = cache_control_headers.collect{ |x| x.match(/max-age\s*=\s*(\d+)/).to_a[1].to_i }.min
|
99
|
+
|
100
|
+
headers['Cache-Control'] = "private, max-age=#{min_max_age}, must-revalidate"
|
101
|
+
headers.delete( 'Expires' )
|
102
|
+
headers['Expires'] = ( Time.now.utc + min_max_age ).strftime("%a, %d %m %Y %T %Z") if min_max_age > 0
|
103
|
+
|
104
|
+
# Create Correct Content-Length
|
105
|
+
headers['Content-Length'] = Rack::Utils.bytesize( body ).to_s
|
106
|
+
|
107
|
+
# For now whatever headers is set by the original action would be
|
108
|
+
# passed on. Expect for Cache-Control, ETag, Expires, Last-Modified
|
109
|
+
# Cookies from the original action are passed on.
|
110
|
+
[ status, headers, [ body ] ]
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
data/rack-rsi.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "rack/rsi_version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "rack-rsi"
|
7
|
+
s.version = Rack::RSI::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Ram Singla"]
|
10
|
+
s.email = ["ram.singla@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/ramsingla/rack-rsi"
|
12
|
+
s.summary = %q{Rack Middleware: Rack Side Include}
|
13
|
+
s.description = %q{Rack Side Include helps you assemble pages like Edge Side Include (ESI) using ERB tags.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "rack-rsi"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.required_rubygems_version = ">= 1.3.7"
|
23
|
+
s.add_dependency "rack"
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-rsi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ram Singla
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-05-13 00:00:00 +05:30
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rack
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
description: Rack Side Include helps you assemble pages like Edge Side Include (ESI) using ERB tags.
|
28
|
+
email:
|
29
|
+
- ram.singla@gmail.com
|
30
|
+
executables: []
|
31
|
+
|
32
|
+
extensions: []
|
33
|
+
|
34
|
+
extra_rdoc_files: []
|
35
|
+
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- Gemfile
|
39
|
+
- LICENSE.markdown
|
40
|
+
- README.markdown
|
41
|
+
- Rakefile
|
42
|
+
- TODO
|
43
|
+
- examples/config.ru
|
44
|
+
- examples/hello_world_app.rb
|
45
|
+
- lib/rack/rsi.rb
|
46
|
+
- lib/rack/rsi_version.rb
|
47
|
+
- rack-rsi.gemspec
|
48
|
+
has_rdoc: true
|
49
|
+
homepage: https://github.com/ramsingla/rack-rsi
|
50
|
+
licenses: []
|
51
|
+
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: "0"
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.3.7
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project: rack-rsi
|
72
|
+
rubygems_version: 1.6.2
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: "Rack Middleware: Rack Side Include"
|
76
|
+
test_files: []
|
77
|
+
|