curly-templates 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,125 @@
1
+ Curly
2
+ =======
3
+
4
+ Free your views!
5
+
6
+ Curly is a template language that completely separates structure and logic.
7
+ Instead of interspersing your HTML with snippets of Ruby, all logic is moved
8
+ to a presenter class, with only simple placeholders in the HTML.
9
+
10
+ While the basic concepts are very similar to [Mustache](http://mustache.github.com/)
11
+ or [Handlebars](http://handlebarsjs.com/), Curly is different in some key ways:
12
+
13
+ - Instead of the template controlling the variable scope and looping through
14
+ data, all logic is left to the presenter object. This means that untrusted
15
+ templates can safely be executed, making Curly a possible alternative to
16
+ languages like [Liquid](http://liquidmarkup.org/).
17
+ - Instead of implementing its own template resolution mechanism, Curly hooks
18
+ directly into Rails, leveraging the existing resolvers.
19
+ - Because of the way it integrates with Rails, it is very easy to use partial
20
+ Curly templates to split out logic from a presenter. With Mustache, at least,
21
+ when integrating with Rails, it is common to return Hash objects from view
22
+ object methods that are in turn used by the template.
23
+
24
+
25
+ Examples
26
+ --------
27
+
28
+ Here is a simple Curly template -- it will be looked up by Rails automatically.
29
+
30
+ ```html
31
+ <!-- app/views/posts/show.html.curly -->
32
+ <h1>{{title}}<h1>
33
+ <p class="author">{{author}}</p>
34
+ <p>{{description}}</p>
35
+
36
+ {{comment_form}}
37
+
38
+ <div class="comments">
39
+ {{comments}}
40
+ </div>
41
+ ```
42
+
43
+ When rendering the template, a presenter is automatically instantiated with the
44
+ variables assigned in the controller or the `render` call. The presenter declares
45
+ the variables it expects with `presents`, which takes a list of variables names.
46
+
47
+ ```ruby
48
+ # app/presenters/posts/show_presenter.rb
49
+ class Posts::ShowPresenter < Curly::Presenter
50
+ presents :post
51
+
52
+ def title
53
+ @post.title
54
+ end
55
+
56
+ def author
57
+ link_to(@post.author.name, @post.author, rel: "author")
58
+ end
59
+
60
+ def description
61
+ Markdown.new(@post.description).to_html.html_safe
62
+ end
63
+
64
+ def comments
65
+ render 'comment', collection: @post.comments
66
+ end
67
+
68
+ def comment_form
69
+ if @post.comments_allowed?
70
+ render 'comment_form', post: @post
71
+ else
72
+ content_tag(:p, "Comments are disabled for this post")
73
+ end
74
+ end
75
+ end
76
+ ```
77
+
78
+
79
+ Caching
80
+ -------
81
+
82
+ Because of the way logic is contained in presenters, caching entire views or partials
83
+ becomes exceedingly straightforward. Simply define a `#cache_key` method that returns
84
+ a non-nil object, and the return value will be used to cache the template.
85
+
86
+ Whereas in ERB your would include the `cache` call in the template itself:
87
+
88
+ ```erb
89
+ <% cache([@post, signed_in?]) do %>
90
+ ...
91
+ <% end %>
92
+ ```
93
+
94
+ In Curly you would instead declare it in the presenter:
95
+
96
+ ```ruby
97
+ class Posts::ShowPresenter < Curly::Presenter
98
+ presents :post
99
+
100
+ def cache_key
101
+ [@post, signed_in?]
102
+ end
103
+ end
104
+ ```
105
+
106
+ Likewise, you can add a `#cache_duration` method if you wish to automatically expire
107
+ the fragment cache:
108
+
109
+ ```ruby
110
+ class Posts::ShowPresenter < Curly::Presenter
111
+ ...
112
+
113
+ def cache_duration
114
+ 30.minutes
115
+ end
116
+ end
117
+ ```
118
+
119
+
120
+ Copyright and License
121
+ ---------------------
122
+
123
+ Copyright (c) 2013 Daniel Schierbeck (@dasch), Zendesk Inc.
124
+
125
+ Licensed under the [Apache License Version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
data/Rakefile ADDED
@@ -0,0 +1,132 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'rake'
4
+ require 'date'
5
+
6
+ Bundler.setup
7
+
8
+ #############################################################################
9
+ #
10
+ # Helper functions
11
+ #
12
+ #############################################################################
13
+
14
+ def name
15
+ "curly"
16
+ end
17
+
18
+ def gem_name
19
+ "#{name}-templates"
20
+ end
21
+
22
+ def version
23
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
24
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
25
+ end
26
+
27
+ def date
28
+ Date.today.to_s
29
+ end
30
+
31
+ def gemspec_file
32
+ "#{gem_name}.gemspec"
33
+ end
34
+
35
+ def gem_file
36
+ "#{gem_name}-#{version}.gem"
37
+ end
38
+
39
+ def replace_header(head, header_name, value = nil)
40
+ value ||= send(header_name)
41
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{value}'"}
42
+ end
43
+
44
+ #############################################################################
45
+ #
46
+ # Standard tasks
47
+ #
48
+ #############################################################################
49
+
50
+ task :default => :spec
51
+
52
+ require 'rspec/core/rake_task'
53
+ RSpec::Core::RakeTask.new(:spec)
54
+
55
+ require 'rdoc/task'
56
+ Rake::RDocTask.new do |rdoc|
57
+ rdoc.rdoc_dir = 'rdoc'
58
+ rdoc.title = "#{name} #{version}"
59
+ rdoc.rdoc_files.include('README*')
60
+ rdoc.rdoc_files.include('lib/**/*.rb')
61
+ end
62
+
63
+ desc "Open an irb session preloaded with this library"
64
+ task :console do
65
+ sh "irb -rubygems -r ./lib/#{name}.rb"
66
+ end
67
+
68
+ #############################################################################
69
+ #
70
+ # Packaging tasks
71
+ #
72
+ #############################################################################
73
+
74
+ desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
75
+ task :release => :build do
76
+ unless `git branch` =~ /^\* master$/
77
+ puts "You must be on the master branch to release!"
78
+ exit!
79
+ end
80
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
81
+ sh "git tag v#{version}"
82
+ sh "git push origin master"
83
+ sh "git push origin v#{version}"
84
+ sh "gem push pkg/#{gem_name}-#{version}.gem"
85
+ end
86
+
87
+ desc "Build #{gem_file} into the pkg directory"
88
+ task :build => :gemspec do
89
+ sh "mkdir -p pkg"
90
+ sh "gem build #{gemspec_file}"
91
+ sh "mv #{gem_file} pkg"
92
+ end
93
+
94
+ desc "Generate #{gemspec_file}"
95
+ task :gemspec => :validate do
96
+ # read spec file and split out manifest section
97
+ spec = File.read(gemspec_file)
98
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
99
+
100
+ # replace name version and date
101
+ replace_header(head, :name, gem_name)
102
+ replace_header(head, :version)
103
+ replace_header(head, :date)
104
+
105
+ # determine file list from git ls-files
106
+ files = `git ls-files`.
107
+ split("\n").
108
+ sort.
109
+ reject { |file| file =~ /^\./ }.
110
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
111
+ map { |file| " #{file}" }.
112
+ join("\n")
113
+
114
+ # piece file back together and write
115
+ manifest = " s.files = %w[\n#{files}\n ]\n"
116
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
117
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
118
+ puts "Updated #{gemspec_file}"
119
+ end
120
+
121
+ desc "Validate #{gemspec_file}"
122
+ task :validate do
123
+ libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
124
+ unless libfiles.empty?
125
+ puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
126
+ exit!
127
+ end
128
+ unless Dir['VERSION*'].empty?
129
+ puts "A `VERSION` file at root level violates Gem best practices."
130
+ exit!
131
+ end
132
+ end
@@ -0,0 +1,44 @@
1
+ Gem::Specification.new do |s|
2
+ s.specification_version = 2 if s.respond_to? :specification_version=
3
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
+ s.rubygems_version = '1.3.5'
5
+
6
+ s.name = 'curly-templates'
7
+ s.version = '0.1.0'
8
+ s.date = '2013-01-21'
9
+
10
+ s.summary = "Free your views!"
11
+ s.description = "A view layer for your Rails apps that separates structure and logic."
12
+
13
+ s.authors = ["Daniel Schierbeck"]
14
+ s.email = 'dasch@zendesk.com'
15
+ s.homepage = 'http://github.com/zendesk/curly-templates'
16
+
17
+ s.require_paths = %w[lib]
18
+
19
+ s.rdoc_options = ["--charset=UTF-8"]
20
+
21
+ s.add_dependency("actionpack", "~> 3.2.11")
22
+
23
+ s.add_development_dependency("rake")
24
+ s.add_development_dependency("rspec", "~> 2.12.0")
25
+
26
+ # = MANIFEST =
27
+ s.files = %w[
28
+ Gemfile
29
+ README.md
30
+ Rakefile
31
+ curly-templates.gemspec
32
+ lib/curly.rb
33
+ lib/curly/presenter.rb
34
+ lib/curly/railtie.rb
35
+ lib/curly/template_handler.rb
36
+ spec/curly_spec.rb
37
+ spec/presenter_spec.rb
38
+ spec/spec_helper.rb
39
+ spec/template_handler_spec.rb
40
+ ]
41
+ # = MANIFEST =
42
+
43
+ s.test_files = s.files.select { |path| path =~ /^spec\/.*_spec\.rb/ }
44
+ end
data/lib/curly.rb ADDED
@@ -0,0 +1,67 @@
1
+ # Curly is a simple view system. Each view consists of two parts, a
2
+ # template and a presenter. The template is a simple string that can contain
3
+ # references in the format `{{refname}}`, e.g.
4
+ #
5
+ # Hello {{recipient}},
6
+ # you owe us ${{amount}}.
7
+ #
8
+ # The references will be converted into messages that are sent to the
9
+ # presenter, which is any Ruby object. Only public methods can be referenced.
10
+ # To continue the earlier example, here's the matching presenter:
11
+ #
12
+ # class BankPresenter
13
+ # def initialize(recipient, amount)
14
+ # @recipient, @amount = recipient, amount
15
+ # end
16
+ #
17
+ # def recipient
18
+ # @recipient.full_name
19
+ # end
20
+ #
21
+ # def amount
22
+ # "%.2f" % @amount
23
+ # end
24
+ # end
25
+ #
26
+ # See Curly::Presenter for more information on presenters.
27
+ #
28
+ module Curly
29
+ VERSION = "0.1.0"
30
+
31
+ REFERENCE_REGEX = %r(\{\{(\w+)\}\})
32
+
33
+ class InvalidReference < StandardError
34
+ end
35
+
36
+ def self.compile(template)
37
+ source = template.inspect
38
+ source.gsub!(REFERENCE_REGEX) { compile_reference($1) }
39
+
40
+ source
41
+ end
42
+
43
+ def self.valid?(template, presenter_class)
44
+ references = extract_references(template)
45
+ methods = presenter_class.available_methods.map(&:to_s)
46
+ references & methods == references
47
+ end
48
+
49
+ private
50
+
51
+ def self.compile_reference(reference)
52
+ %(\#{
53
+ if presenter.method_available?(:#{reference})
54
+ result = presenter.#{reference} {|*args| yield(*args) }
55
+ ERB::Util.html_escape(result)
56
+ else
57
+ raise Curly::InvalidReference, "invalid reference `{{#{reference}}}'"
58
+ end
59
+ })
60
+ end
61
+
62
+ def self.extract_references(template)
63
+ template.scan(REFERENCE_REGEX).flatten
64
+ end
65
+ end
66
+
67
+ require 'curly/railtie' if defined?(Rails)
@@ -0,0 +1,80 @@
1
+ module Curly
2
+
3
+ # A base class that can be subclassed by concrete presenters.
4
+ #
5
+ # A Curly presenter is responsible for delivering data to templates, in the
6
+ # form of simple strings. Each public instance method on the presenter class
7
+ # can be referenced in a template. When a template is evaluated with a
8
+ # presenter, the referenced methods will be called with no arguments, and
9
+ # the returned strings inserted in place of the references in the template.
10
+ #
11
+ # Note that strings that are not HTML safe will be escaped.
12
+ #
13
+ # A presenter is always instantiated with a context to which it delegates
14
+ # unknown messages, usually an instance of ActionView::Base provided by
15
+ # Rails. See Curly::Handler for a typical use.
16
+ #
17
+ # Examples
18
+ #
19
+ # class BlogPresenter < Curly::Presenter
20
+ # presents :post
21
+ #
22
+ # def title
23
+ # @post.title
24
+ # end
25
+ #
26
+ # def body
27
+ # markdown(@post.body)
28
+ # end
29
+ #
30
+ # def author
31
+ # @post.author.full_name
32
+ # end
33
+ # end
34
+ #
35
+ # presenter = BlogPresenter.new(context, post: post)
36
+ # presenter.author #=> "Jackie Chan"
37
+ #
38
+ class Presenter
39
+ # Initializes the presenter with the given context and options.
40
+ #
41
+ # context - An ActionView::Base context.
42
+ # options - A Hash of options given to the presenter.
43
+ #
44
+ def initialize(context, options = {})
45
+ @_context = context
46
+ self.class.presented_names.each do |name|
47
+ instance_variable_set("@#{name}", options.fetch(name))
48
+ end
49
+ end
50
+
51
+ def cache_key
52
+ nil
53
+ end
54
+
55
+ def cache_duration
56
+ nil
57
+ end
58
+
59
+ def method_available?(method)
60
+ self.class.available_methods.include?(method)
61
+ end
62
+
63
+ def self.available_methods
64
+ public_instance_methods - Curly::Presenter.public_instance_methods
65
+ end
66
+
67
+ private
68
+
69
+ class_attribute :presented_names
70
+ self.presented_names = [].freeze
71
+
72
+ def self.presents(*args)
73
+ self.presented_names += args
74
+ end
75
+
76
+ def method_missing(method, *args, &block)
77
+ @_context.public_send(method, *args, &block)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,8 @@
1
+ module Curly
2
+ class Railtie < Rails::Railtie
3
+ initializer 'curly.initialize_template_handler' do
4
+ require 'curly/template_handler'
5
+ ActionView::Template.register_template_handler :curly, Curly::TemplateHandler
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,51 @@
1
+ require 'active_support'
2
+ require 'action_view'
3
+ require 'curly'
4
+
5
+ class Curly::TemplateHandler
6
+ def self.presenter_name_for_path(path)
7
+ "#{path}_presenter".camelize
8
+ end
9
+
10
+ def self.call(template)
11
+ presenter_class = presenter_name_for_path(template.virtual_path)
12
+
13
+ source = Curly.compile(template.source)
14
+ template_digest = Digest::MD5.hexdigest(template.source)
15
+
16
+ # Template is empty, so there's no need to initialize a presenter.
17
+ return %("") if template.source.empty?
18
+
19
+ <<-RUBY
20
+ if local_assigns.empty?
21
+ options = assigns
22
+ else
23
+ options = local_assigns
24
+ end
25
+
26
+ presenter = #{presenter_class}.new(self, options.with_indifferent_access)
27
+
28
+ view_function = lambda do
29
+ #{source}
30
+ end
31
+
32
+ if key = presenter.cache_key
33
+ @output_buffer = ActiveSupport::SafeBuffer.new
34
+
35
+ template_digest = #{template_digest.inspect}
36
+
37
+ options = {
38
+ expires_in: presenter.cache_duration
39
+ }
40
+
41
+ cache([template_digest, key], options) do
42
+ safe_concat(view_function.call)
43
+ end
44
+
45
+ @output_buffer
46
+ else
47
+ view_function.call.html_safe
48
+ end
49
+ RUBY
50
+ end
51
+ end
@@ -0,0 +1,92 @@
1
+ require 'spec_helper'
2
+ require 'active_support/core_ext/string/output_safety'
3
+ require 'curly'
4
+
5
+ describe Curly do
6
+ let :presenter_class do
7
+ Class.new do
8
+ def foo
9
+ "FOO"
10
+ end
11
+
12
+ def high_yield
13
+ "#{yield}, motherfucker!"
14
+ end
15
+
16
+ def yield_value
17
+ "#{yield :foo}, please?"
18
+ end
19
+
20
+ def unicorns
21
+ "UNICORN"
22
+ end
23
+
24
+ def method_available?(method)
25
+ [:foo, :high_yield, :yield_value, :dirty].include?(method)
26
+ end
27
+
28
+ def self.available_methods
29
+ public_instance_methods
30
+ end
31
+
32
+ private
33
+
34
+ def method_missing(*args)
35
+ "BAR"
36
+ end
37
+ end
38
+ end
39
+
40
+ let(:presenter) { presenter_class.new }
41
+ let(:context) { double("context", presenter: presenter) }
42
+
43
+ it "compiles Curly templates to Ruby code" do
44
+ evaluate("{{foo}}").should == "FOO"
45
+ end
46
+
47
+ it "makes sure only public methods are called on the presenter object" do
48
+ expect { evaluate("{{bar}}") }.to raise_exception(Curly::InvalidReference)
49
+ end
50
+
51
+ it "propagates yields to the caller" do
52
+ evaluate("{{high_yield}}") { "$$$" }.should == "$$$, motherfucker!"
53
+ end
54
+
55
+ it "sends along arguments passed to yield" do
56
+ evaluate("{{yield_value}}") {|v| v.upcase }.should == "FOO, please?"
57
+ end
58
+
59
+ it "escapes non HTML safe strings returned from the presenter" do
60
+ presenter.stub(:dirty) { "<p>dirty</p>" }
61
+ evaluate("{{dirty}}").should == "&lt;p&gt;dirty&lt;/p&gt;"
62
+ end
63
+
64
+ it "does not escape HTML safe strings returned from the presenter" do
65
+ presenter.stub(:dirty) { "<p>dirty</p>".html_safe }
66
+ evaluate("{{dirty}}").should == "<p>dirty</p>"
67
+ end
68
+
69
+ describe ".valid?" do
70
+ it "returns true if only available methods are referenced" do
71
+ validate("Hello, {{foo}}!").should == true
72
+ end
73
+
74
+ it "returns false if a missing method is referenced" do
75
+ validate("Hello, {{i_am_missing}}").should == false
76
+ end
77
+
78
+ it "returns false if an unavailable method is referenced" do
79
+ presenter_class.stub(:available_methods) { [:foo] }
80
+ validate("Hello, {{inspect}}").should == false
81
+ end
82
+
83
+ def validate(template)
84
+ Curly.valid?(template, presenter_class)
85
+ end
86
+ end
87
+
88
+ def evaluate(template)
89
+ code = Curly.compile(template)
90
+ context.instance_eval(code)
91
+ end
92
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'active_support/all'
3
+ require 'curly/presenter'
4
+
5
+ describe Curly::Presenter do
6
+ class CircusPresenter < Curly::Presenter
7
+ module MonkeyComponents
8
+ def monkey
9
+ end
10
+ end
11
+
12
+ include MonkeyComponents
13
+
14
+ presents :midget, :clown
15
+ attr_reader :midget, :clown
16
+ end
17
+
18
+ it "sets the presented parameters as instance variables" do
19
+ context = double("context")
20
+
21
+ presenter = CircusPresenter.new(context,
22
+ midget: "Meek Harolson",
23
+ clown: "Bubbles"
24
+ )
25
+
26
+ presenter.midget.should == "Meek Harolson"
27
+ presenter.clown.should == "Bubbles"
28
+ end
29
+ end
File without changes
@@ -0,0 +1,161 @@
1
+ require 'spec_helper'
2
+ require 'active_support/core_ext/string/output_safety'
3
+ require 'active_support/core_ext/hash'
4
+ require 'curly/template_handler'
5
+
6
+ describe Curly::TemplateHandler do
7
+ let :presenter_class do
8
+ Class.new do
9
+ def initialize(context, options = {})
10
+ @context = context
11
+ @cache_key = options.fetch(:cache_key, nil)
12
+ @cache_duration = options.fetch(:cache_duration, nil)
13
+ end
14
+
15
+ def foo
16
+ "FOO"
17
+ end
18
+
19
+ def bar
20
+ @context.bar
21
+ end
22
+
23
+ def cache_key
24
+ @cache_key
25
+ end
26
+
27
+ def cache_duration
28
+ @cache_duration
29
+ end
30
+
31
+ def method_available?(method)
32
+ true
33
+ end
34
+ end
35
+ end
36
+
37
+ let :context_class do
38
+ Class.new do
39
+ attr_reader :output_buffer
40
+ attr_reader :local_assigns, :assigns
41
+
42
+ def initialize
43
+ @cache = Hash.new
44
+ @local_assigns = Hash.new
45
+ @assigns = Hash.new
46
+ @clock = 0
47
+ end
48
+
49
+ def advance_clock(duration)
50
+ @clock += duration
51
+ end
52
+
53
+ def cache(key, options = {})
54
+ fragment, expired_at = @cache[key]
55
+
56
+ if fragment.nil? || @clock >= expired_at
57
+ old_buffer = @output_buffer
58
+ @output_buffer = ActiveSupport::SafeBuffer.new
59
+
60
+ yield
61
+
62
+ fragment = @output_buffer.to_s
63
+ duration = options[:expires_in] || Float::INFINITY
64
+
65
+ @cache[key] = [fragment, @clock + duration]
66
+
67
+ @output_buffer = old_buffer
68
+ end
69
+
70
+ safe_concat(fragment)
71
+
72
+ nil
73
+ end
74
+
75
+ def safe_concat(str)
76
+ @output_buffer.safe_concat(str)
77
+ end
78
+ end
79
+ end
80
+
81
+ let(:template) { double("template", virtual_path: "test") }
82
+ let(:context) { context_class.new }
83
+
84
+ before do
85
+ stub_const("TestPresenter", presenter_class)
86
+ end
87
+
88
+ it "passes in the presenter context to the presenter class" do
89
+ context.stub(:bar) { "BAR" }
90
+ template.stub(:source) { "{{bar}}" }
91
+ output.should == "BAR"
92
+ end
93
+
94
+ it "allows calling public methods on the presenter" do
95
+ template.stub(:source) { "{{foo}}" }
96
+ output.should == "FOO"
97
+ end
98
+
99
+ it "marks its output as HTML safe" do
100
+ template.stub(:source) { "{{foo}}" }
101
+ output.should be_html_safe
102
+ end
103
+
104
+ context "caching" do
105
+ before do
106
+ template.stub(:source) { "{{bar}}" }
107
+ context.stub(:bar) { "BAR" }
108
+ end
109
+
110
+ it "caches the result with the #cache_key from the presenter" do
111
+ context.assigns[:cache_key] = "x"
112
+ output.should == "BAR"
113
+
114
+ context.stub(:bar) { "BAZ" }
115
+ output.should == "BAR"
116
+
117
+ context.assigns[:cache_key] = "y"
118
+ output.should == "BAZ"
119
+ end
120
+
121
+ it "doesn't cache when the cache key is nil" do
122
+ context.assigns[:cache_key] = nil
123
+ output.should == "BAR"
124
+
125
+ context.stub(:bar) { "BAZ" }
126
+ output.should == "BAZ"
127
+ end
128
+
129
+ it "adds a digest of the template source to the cache key" do
130
+ context.assigns[:cache_key] = "x"
131
+
132
+ template.stub(:source) { "{{bar}}" }
133
+ output.should == "BAR"
134
+
135
+ template.stub(:source) { "FOO{{bar}}" }
136
+ output.should == "FOOBAR"
137
+ end
138
+
139
+ it "expires the cache keys after #cache_duration" do
140
+ context.assigns[:cache_key] = "x"
141
+ context.assigns[:cache_duration] = 42
142
+
143
+ output.should == "BAR"
144
+
145
+ context.stub(:bar) { "FOO" }
146
+
147
+ # Cached fragment has not yet expired.
148
+ context.advance_clock(41)
149
+ output.should == "BAR"
150
+
151
+ # Now it has! Huzzah!
152
+ context.advance_clock(1)
153
+ output.should == "FOO"
154
+ end
155
+ end
156
+
157
+ def output
158
+ code = Curly::TemplateHandler.call(template)
159
+ context.instance_eval(code)
160
+ end
161
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: curly-templates
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Daniel Schierbeck
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: actionpack
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.11
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 3.2.11
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 2.12.0
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 2.12.0
62
+ description: A view layer for your Rails apps that separates structure and logic.
63
+ email: dasch@zendesk.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - Gemfile
69
+ - README.md
70
+ - Rakefile
71
+ - curly-templates.gemspec
72
+ - lib/curly.rb
73
+ - lib/curly/presenter.rb
74
+ - lib/curly/railtie.rb
75
+ - lib/curly/template_handler.rb
76
+ - spec/curly_spec.rb
77
+ - spec/presenter_spec.rb
78
+ - spec/spec_helper.rb
79
+ - spec/template_handler_spec.rb
80
+ homepage: http://github.com/zendesk/curly-templates
81
+ licenses: []
82
+ post_install_message:
83
+ rdoc_options:
84
+ - --charset=UTF-8
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ segments:
94
+ - 0
95
+ hash: 924326065759708152
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 1.8.24
105
+ signing_key:
106
+ specification_version: 2
107
+ summary: Free your views!
108
+ test_files:
109
+ - spec/curly_spec.rb
110
+ - spec/presenter_spec.rb
111
+ - spec/template_handler_spec.rb