cobranding 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,69 @@
1
+ = Cobranding
2
+
3
+ This gem allows you too pull marked up HTML from a URL and use it as a layout in a Rails view.
4
+
5
+ == Fetching the layout
6
+
7
+ To fetch the layout from a service, you can cal
8
+
9
+ Cobranding::Layout.get(url, options)
10
+
11
+ Where +url+ and +options+ are values passed to +SimpleHttpClient+. Additional options available are
12
+
13
+ * base_url: set the base url for expanding any relative URLs in the markup
14
+ * method: set to :post to perform a POST instead of a GET request
15
+
16
+ == Caching
17
+
18
+ If you specify +:ttl+ in the options, the layout will be cached for that many seconds. The cache algorithm also tries to prevent race conditions when the cache expires. Using it can greatly reduce load on the servers.
19
+
20
+ You must use a Rails.cache that supports +:expires_in+ on writes to the cache. In Rails 2 the only out of the box cache that will work properly is MemCacheStore.
21
+
22
+ == Markup
23
+
24
+ The HTML markup in the layout cannot contain any ERB code (<% %>). If any is found, it will be escaped.
25
+
26
+ You can put limited markup into the HTML using {{ }} style tags. These tags must be exactly one word and will trigger calling helper methods like *_for_cobranding.
27
+
28
+ === Example
29
+
30
+ <html>
31
+ <head>
32
+ <title>{{ page_title }}</title>
33
+ {{ stylesheets }}
34
+ </head>
35
+ <body>
36
+ {{ content }}
37
+ </body>
38
+ </html>
39
+
40
+ The tags in this layout will result in calls to
41
+
42
+ * page_title_for_cobranding
43
+ * stylesheets_for_cobranding
44
+ * content_for_cobranding
45
+
46
+ If any of these helper methods aren't defined, they will be silently ignored. If you need a different naming convention for you helper methods, you can pass in a :prefix or :suffix option to the +evaluate+ or +cobranding_layout+ helper call.
47
+
48
+ == Using in a view
49
+
50
+ The gem automatically adds a universal helper method that allows you to call the layout. It should be added to a regular old layout view like this:
51
+
52
+ <%= cobranding_layout(url, options) do %>
53
+ <html>
54
+ <head>
55
+ <title><%= page_title_for_cobranding %></title>
56
+ <%= stylesheets_for_cobranding %>
57
+ </head>
58
+ <body>
59
+ <div id="warning">Warning the layout was not available!</div>
60
+ <%= content_for_cobranding %>
61
+ </body>
62
+ </html>
63
+ <% end %>
64
+
65
+ If the tag includes a block, it will only be called if there was an error evaluating the template. This way, you can provide a default failsafe layout in case the layout service is unavailable. Providing one will keep your site up in this case and can also serve as documentation for what the layouts are expected to look like and what tags they should use.
66
+
67
+ == Persisting
68
+
69
+ If you have layouts that don't need to be real time, you can persist them to a data store and update them asynchronously via a background job. You simply need to include Cobranding::PersistentLayout in your model. To render the layout, you can then pass in model.layout to the cobranding_layout helper instead of a URL.
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ desc 'Default: run unit tests'
5
+ task :default => :test
6
+
7
+ begin
8
+ require 'rspec'
9
+ require 'rspec/core/rake_task'
10
+ desc 'Run the unit tests'
11
+ RSpec::Core::RakeTask.new(:test)
12
+ rescue LoadError
13
+ task :test do
14
+ raise "You must have rspec 2.0 installed to run the tests"
15
+ end
16
+ end
17
+
18
+ begin
19
+ require 'jeweler'
20
+ Jeweler::Tasks.new do |gem|
21
+ gem.name = "cobranding"
22
+ gem.summary = %Q{Provides Rails view layouts from an HTTP service}
23
+ gem.description = %Q{Provides Rails view layouts from an HTTP service.}
24
+ gem.authors = ["Brian Durand"]
25
+ gem.email = ["mdobrota@tribune.com", "ddpr@tribune.com"]
26
+ gem.files = FileList["lib/**/*", "spec/**/*", "README.rdoc", "Rakefile", "License.txt"].to_a
27
+ gem.has_rdoc = true
28
+ gem.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
29
+ gem.extra_rdoc_files = ["README.rdoc"]
30
+ gem.add_dependency('actionpack', '>=3.0.0')
31
+ gem.add_dependency('rest-client')
32
+ gem.add_development_dependency('rspec', '>= 2.0.0')
33
+ gem.add_development_dependency('webmock')
34
+ end
35
+ Jeweler::RubygemsDotOrgTasks.new
36
+ rescue LoadError
37
+ end
@@ -0,0 +1,11 @@
1
+ require 'active_support/all' unless defined?(ActiveSupport::HashWithIndifferentAccess)
2
+ require 'action_view'
3
+ require 'rack'
4
+
5
+ module Cobranding
6
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'cobranding', 'layout')
7
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'cobranding', 'helper')
8
+ autoload :PersistentLayout, File.join(File.expand_path(File.dirname(__FILE__)), 'cobranding', 'persistent_layout')
9
+
10
+ ActionView::Base.send(:include, Helper)
11
+ end
@@ -0,0 +1,28 @@
1
+ module Cobranding
2
+ # This module gets mixed in to ActionView::Helpers so its methods are available in all Rails views.
3
+ module Helper
4
+ # Helper method to render a layout. The +url_or_layout+ can either be a URL to a layout service or a Layout object.
5
+ # The options parameter will only be used if a URL is passed in.
6
+ #
7
+ # This method can take a block which should be a fail safe ERB version of the layout that will only be used if the
8
+ # layout service is unavailable.
9
+ #
10
+ # Note that for Rails 2.x applications you must call the tag with a block as <% cobranding_layout do %> while in Rails 3.0
11
+ # and later you must call it as <%= cobranding_layout do %>.
12
+ def cobranding_layout (url_or_layout, options = nil, &block)
13
+ options = options.dup if options
14
+ evaluate_options = {:prefix => options.delete(:prefix), :suffix => options.delete(:suffix)} if options
15
+ layout = url_or_layout.is_a?(Layout) ? url_or_layout : Layout.get(url_or_layout, options)
16
+ layout.evaluate(self, evaluate_options).html_safe
17
+ rescue SystemExit, Interrupt, NoMemoryError
18
+ raise
19
+ rescue Exception => e
20
+ if block_given?
21
+ Rails.logger.warn(e) if Rails.logger
22
+ capture(&block).html_safe
23
+ else
24
+ raise e
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,208 @@
1
+ require 'erb'
2
+ require 'digest/md5'
3
+ require 'rest-client'
4
+
5
+ module Cobranding
6
+ # This class is used to get layout HTML markup from a service and compile it into Ruby code that can be used
7
+ # as the layout in a Rails view.
8
+ class Layout
9
+ QUOTED_RELATIVE_URL = /(<\w+\s((src)|(href))=(['"]))\/(.*?)(\5[^>]*?>)/i
10
+ UNQUOTED_RELATIVE_URL = /(<\w+\s((src)|(href))=)\/(.*?)(>|(\s[^>]*?>))/i
11
+
12
+ class << self
13
+ # Get the layout HTML from a service. The options can be any of the options accepted by SimpleHttpClient
14
+ # or +:base_url+. Any relative URLs found in the HTML will be expanded to absolute URLs using either the
15
+ # +:base_url+ option or the +url+ as the base.
16
+ #
17
+ # If +:ttl+ is specified in the options, the layout will be cached for that many seconds.
18
+ #
19
+ # By default the request will be a GET request. If you need to do a POST, you can pass :method => :post in the options.
20
+ #
21
+ # If a block is passed, it will be called with the layout html before the layout is created. This can be used
22
+ # to munge the layout HTML code if necessary.
23
+ def get (url, options = {}, &block)
24
+ return nil if url.blank?
25
+ options ||= {}
26
+ options = options.is_a?(HashWithIndifferentAccess) ? options.dup : options.with_indifferent_access
27
+ ttl = options.delete(:ttl)
28
+ race_ttl = options.delete(:race_condition_ttl)
29
+ if Rails.cache && ttl
30
+ cache_options = {}
31
+ cache_options[:expires_in] = ttl if ttl
32
+ cache_options[:race_condition_ttl] = race_ttl if race_ttl
33
+ key = cache_key(url, options)
34
+ Rails.cache.fetch(key, cache_options) do
35
+ layout = fetch_layout(url, options, &block)
36
+ end
37
+ else
38
+ return fetch_layout(url, options, &block)
39
+ end
40
+ end
41
+
42
+ # Generate a unique cache key for the layout request.
43
+ def cache_key (url, options = {})
44
+ options = options.is_a?(HashWithIndifferentAccess) ? options.dup : options.with_indifferent_access
45
+ full_uri = full_uri(url, options)
46
+ params = options.delete(:params)
47
+ options.delete(:host)
48
+ options.delete(:port)
49
+ options.delete(:scheme)
50
+ options.delete(:base)
51
+ options.delete(:ttl)
52
+ options.delete(:timeout)
53
+ options.delete(:read_timeout)
54
+ options.delete(:open_timeout)
55
+ append_params_to_uri!(full_uri, params) if params
56
+
57
+ options_key, query = full_uri.to_s.split('?', 2)
58
+ if query
59
+ options_key << '?'
60
+ options_key << query.split('&').sort.join('&')
61
+ end
62
+
63
+ options.keys.sort{|a,b| a.to_s <=> b.to_s}.each do |key|
64
+ options_key << " #{key}=#{options[key]}"
65
+ end
66
+
67
+ "#{name}.#{Digest::MD5.hexdigest(options_key)}"
68
+ end
69
+
70
+ protected
71
+
72
+ # Fetch the layout HTML from the service. The block is optional and will be called with the html code.
73
+ def fetch_layout (url, options, &block)
74
+ method = options.delete(:method) || :get
75
+ full_url = full_uri(url, options).to_s
76
+ base_uri = options[:base_url] ? URI.parse(options.delete(:base_url)) : full_uri(url, options)
77
+ base_uri.userinfo = nil
78
+ base_uri.path = "/"
79
+ base_uri.query = nil
80
+ base_uri.fragment = nil
81
+ response = method.to_sym == :post ? RestClient.post(full_url, options) : RestClient.get(full_url, options)
82
+ html = expand_base_url(response, base_uri.to_s)
83
+ html = block.call(html) if block
84
+ layout = new(html)
85
+ end
86
+
87
+ # Expand any relative URL's found in HTML tags to be absolute URLs with the specified base.
88
+ def expand_base_url (html, base_url)
89
+ return html unless base_url
90
+ base_url = "#{base_url}/" unless base_url.end_with?("/")
91
+ html.gsub(QUOTED_RELATIVE_URL){|match| "#{$1}#{base_url}#{$6}#{$7}"}.gsub(UNQUOTED_RELATIVE_URL){|match| "#{$1}#{base_url}#{$5}#{$6}"}
92
+ end
93
+
94
+ private
95
+
96
+ def full_uri (url, options)
97
+ return url if url.kind_of?(URI)
98
+ uri = URI.parse(url)
99
+ base = URI.parse(options[:base]) if options[:base]
100
+
101
+ if uri.scheme == nil
102
+ host = options[:host]
103
+ port = options[:port]
104
+ scheme = options[:scheme]
105
+ if base and base.scheme
106
+ host ||= base.host
107
+ port ||= base.port
108
+ scheme ||= base.scheme
109
+ end
110
+ if host
111
+ full_url = "#{scheme ? scheme : 'http'}://#{host}"
112
+ full_url << ":#{port}" if port
113
+ if base
114
+ unless uri.to_s[0,1] == '/'
115
+ full_url << base.path
116
+ full_url << '/' unless base.path.last == '/'
117
+ end
118
+ end
119
+ full_url << uri.to_s
120
+ uri = URI.parse(full_url)
121
+ end
122
+ end
123
+
124
+ return uri
125
+ end
126
+
127
+ def append_params_to_uri! (uri, params)
128
+ unless params.blank?
129
+ if uri.query.blank?
130
+ uri.query = url_encode_parameters(params)
131
+ else
132
+ uri.query << "&"
133
+ uri.query << url_encode_parameters(params)
134
+ end
135
+ end
136
+ end
137
+
138
+ def url_encode_parameters (params)
139
+ params.collect{|name, value| url_encoded_param(name, value)}.join('&')
140
+ end
141
+
142
+ def url_encoded_param (name, value)
143
+ if value.kind_of?(Array)
144
+ return value.collect{|v| url_encoded_param(name, v)}.join('&')
145
+ else
146
+ return "#{Rack::Utils.escape(name.to_s)}=#{Rack::Utils.escape(value.to_s)}"
147
+ end
148
+ end
149
+ end
150
+
151
+ attr_accessor :src
152
+
153
+ # Create the Layout. The src will be defined from the HTML passed in.
154
+ def initialize (html = nil)
155
+ self.html = html if html
156
+ end
157
+
158
+ # Set the src by compiling HTML into RHTML and then into Ruby code.
159
+ def html= (html)
160
+ self.src = compile(html)
161
+ end
162
+
163
+ # Evaluate the RHTML source code in the specified context. Any yields will call a helper method
164
+ # corresponding to the value yielded if it exists. The options :prefix and :suffix can be set to
165
+ # determine the full method name to call. The default is to suffix values with +_for_cobranding+
166
+ # so that <tt>yield title</tt> will call +title_for_cobranding+. Setting a different prefix or
167
+ # suffix can be useful if you are pulling in templates from different sources which use the same
168
+ # variable names but need different values.
169
+ def evaluate (context, options = nil)
170
+ if src
171
+ prefix = options[:prefix] if options
172
+ suffix = options[:suffix] if options
173
+ suffix = "_for_cobranding" unless prefix or suffix
174
+ evaluator = Object.new
175
+ eval <<-EOS
176
+ def evaluator.evaluate
177
+ #{src}
178
+ end
179
+ EOS
180
+ evaluator.evaluate do |var|
181
+ method = "#{prefix}#{var}#{suffix}"
182
+ context.send(method) if context.respond_to?(method)
183
+ end
184
+ end
185
+ end
186
+
187
+ protected
188
+
189
+ # Turn markup in the html into rhtml yield statments. Markup will be in HTML comments containing
190
+ # listings:var where var is a variable name set using content_for.
191
+ def rhtml (html)
192
+ return nil unless html
193
+ # Strip blank lines 'cuz theres just so many of them
194
+ rhtml_code = html.gsub(/^\s+$/, "\n").gsub(/[\n\r]+/, "\n")
195
+ # Escape things that look like ERB since it could be a mistake or it could be malicious
196
+ rhtml_code.gsub!("<%", "&lt;%")
197
+ rhtml_code.gsub!("%>", "%&gt;")
198
+ # Replace special comments with yield tags.
199
+ rhtml_code.gsub!(/\{\{\s*(\w+)\s*\}\}/){|match| "<%=yield :#{$1}%>"}
200
+ return rhtml_code
201
+ end
202
+
203
+ # Compile RHTML into ruby code.
204
+ def compile (html)
205
+ ERB.new(rhtml(html), nil, '-').src.freeze if html
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,53 @@
1
+ module Cobranding
2
+ # This module can be mixed in to persistent objects so that layouts can be persisted to a data store. This is very
3
+ # useful when there are only a few layouts and they don't need to be updated in real time. In this case, you can run
4
+ # a background task to call fetch_layout on all your persistent layouts to update them asynchronously.
5
+ #
6
+ # By default, it will be assumed that the URL for the layout service will be stored in a field called +url+ and
7
+ # the compiled Layout ruby code will be stored in +src+. You can override these values with +layout_src_attribute+
8
+ # and +layout_url_attriubte+. In addition, if the URL takes options, you can specify the field that stores the Hash
9
+ # with +layout_url_options_attribute+.
10
+ #
11
+ # If the layout code needs to be munged, set the +:layout_preprocessor+ class attribute to either a symbol that
12
+ # matches a method name or to a +Proc+.
13
+ module PersistentLayout
14
+ def self.included (base)
15
+ base.class_attribute :layout_src_attribute, :layout_url_attribute, :layout_url_options_attribute, :layout_preprocessor
16
+ end
17
+
18
+ # Fetch a loyout from the service and store the ruby src code in the src attribute.
19
+ def fetch_layout
20
+ layout_url = send(layout_url_attribute || :url)
21
+ unless layout_url.blank?
22
+ options = send(layout_url_options_attribute) unless layout_url_options_attribute.blank?
23
+ options ||= {}
24
+ preprocessor = self.class.layout_preprocessor
25
+ if preprocessor && !preprocessor.is_a?(Proc)
26
+ preprocessor = method(preprocessor)
27
+ end
28
+ @layout = preprocessor ? Layout.get(layout_url, options, &preprocessor) : Layout.get(layout_url, options)
29
+ send("#{self.class.layout_src_attribute || :src}=", @layout.src)
30
+ end
31
+ end
32
+
33
+ # Get the layout defined by the src attribute.
34
+ def layout
35
+ unless @layout
36
+ layout_src = send(layout_src_attribute || :src)
37
+ unless layout_src.blank?
38
+ @layout = Layout.new
39
+ @layout.src = layout_src
40
+ end
41
+ end
42
+ @layout
43
+ end
44
+
45
+ def layout_html= (html)
46
+ preprocessor = self.class.layout_preprocessor
47
+ preprocessor = method(preprocessor) if preprocessor && !preprocessor.is_a?(Proc)
48
+ html = preprocessor.call(html) if preprocessor
49
+ @layout = Layout.new(html)
50
+ send("#{self.class.layout_src_attribute || :src}=", @layout.src)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,98 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Cobranding::Helper do
4
+
5
+ module Cobranding::Helper::TestMethods
6
+ def content_for_cobranding
7
+ "Content!"
8
+ end
9
+ end
10
+
11
+ let(:view) do
12
+ view = ActionView::Base.new
13
+ view.extend(Cobranding::Helper::TestMethods)
14
+ view
15
+ end
16
+
17
+ def template(rhtml)
18
+ handler = ActionView::Template.handler_class_for_extension("erb")
19
+ ActionView::Template.new(rhtml, "test template", handler, {})
20
+ end
21
+
22
+ def test_template(rhtml)
23
+ handler = Template.handler_for_extension("erb")
24
+ template = Template.new(rhtml, "test template", handler, {})
25
+ template.extend(Cobranding::Helper)
26
+ def template.content_for_cobranding
27
+ "Content!"
28
+ end
29
+ end
30
+
31
+ before :each do
32
+ cache = ActiveSupport::Cache::MemoryStore.new
33
+ Rails.stub!(:cache).and_return(cache)
34
+ end
35
+
36
+ it "should render a layout in a view with a Layout" do
37
+ rhtml = '<%= cobranding_layout(layout) %>'
38
+ layout = Cobranding::Layout.new("<html><title>Success</title><body>{{content}}</body></html>")
39
+ view.stub(:layout => layout)
40
+ view.render(:inline => rhtml).should == "<html><title>Success</title><body>Content!</body></html>"
41
+ end
42
+
43
+ it "should render a layout in a view with a Layout using custom prefix and suffix on helper methods" do
44
+ def view._content!
45
+ "Content with custom prefix/suffix"
46
+ end
47
+ url = "http://test.host/layout"
48
+ rhtml = "<%= cobranding_layout('#{url}', :params => {:x => 1}, :prefix => '_', :suffix => '!') %>"
49
+ layout = Cobranding::Layout.new("<html><title>Success</title><body>{{content}}</body></html>")
50
+ Cobranding::Layout.should_receive(:get).with(url, :params => {:x => 1}).and_return(layout)
51
+ view.stub(:layout => layout)
52
+ view.render(:inline => rhtml).should == "<html><title>Success</title><body>Content with custom prefix/suffix</body></html>"
53
+ end
54
+
55
+ it "should render a layout in a view with a URL" do
56
+ rhtml = '<%= cobranding_layout("http://localhost/layout", :params => {:v => 1}, :ttl => 300) %>'
57
+ layout = Cobranding::Layout.new("<html><title>Success</title><body>{{content}}</body></html>")
58
+ key = Cobranding::Layout.cache_key("http://localhost/layout?v=1")
59
+ Rails.cache.write(key, layout)
60
+ view.render(:inline => rhtml).should == "<html><title>Success</title><body>Content!</body></html>"
61
+ end
62
+
63
+ it "should render a layout in a view with an alternate failsafe layout in the body of the cobranding_layout tag" do
64
+ rhtml = '<%= cobranding_layout("http://localhost/layout", :params => {:v => 1}, :ttl => 300) do -%><html><title>FAIL</title><body><%= content_for_cobranding %></body></html><% end -%>'
65
+ layout = Cobranding::Layout.new("<html><title>Success</title><body>{{content}}</body></html>")
66
+ key = Cobranding::Layout.cache_key("http://localhost/layout?v=1")
67
+ Rails.cache.write(key, layout)
68
+ view.render(:inline => rhtml).should == "<html><title>Success</title><body>Content!</body></html>"
69
+ end
70
+
71
+ it "should render a default layout in the body of the cobranding_layout tag in a view" do
72
+ rhtml = '<%= cobranding_layout("invalid url", :params => {:v => 1}, :ttl => 300) do -%><html><title>FAIL</title><body><%= content_for_cobranding %></body></html><% end -%>'
73
+ view.render(:inline => rhtml).should == "<html><title>FAIL</title><body>Content!</body></html>"
74
+ end
75
+
76
+ it "should raise an error if no failsafe layout is specified" do
77
+ rhtml = '<%= cobranding_layout("http://localhost/layout", :params => {:v => 1}, :ttl => 300) %>'
78
+ lambda{view.render(:inline => rhtml)}.should raise_error
79
+ end
80
+
81
+ context "when rendering a failsafe layout" do
82
+ let(:action_view) { ActionView::Base.new }
83
+
84
+ def block_helper(block, content)
85
+ "<%= #{block} do %>#{content}<% end %>"
86
+ end
87
+
88
+ it "should render the default block only once when an exception is raised" do
89
+ url = "http://p2p.cobranding.bad"
90
+ expected_output = "Tribune Tower"
91
+ Cobranding::Layout.should_receive(:get).with(url, nil).and_raise(Exception)
92
+
93
+ output = action_view.render(:inline => block_helper(%(cobranding_layout("#{url}")), expected_output))
94
+ output.should == expected_output
95
+ end
96
+ end
97
+
98
+ end