Skip to content

Commit

Permalink
Add part plugin, with a simpler (and better performing when using :as…
Browse files Browse the repository at this point in the history
…sume_fixed_locals) method for rendering with locals

This adds a part method, which only supports locals and no other
render options:

  # Render plugin API:
  render(:template, locals: {foo: 'bar'})

  # Same, but simpler, and potentially faster
  part(:template, foo: 'bar')
  • Loading branch information
jeremyevans committed Jan 30, 2025
1 parent 1328205 commit 67fc63f
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
=== master

* Add part plugin, with a simpler (and better performing when using :assume_fixed_locals) method for rendering with locals (jeremyevans)

* Support :assume_fixed_locals render plugin option for better caching when all templates use fixed locals (jeremyevans)

=== 3.88.0 (2025-01-14)
Expand Down
70 changes: 70 additions & 0 deletions lib/roda/plugins/part.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen-string-literal: true

#
class Roda
module RodaPlugins
# The part plugin adds a part method, which is an optimized
# render method that only supports locals.
#
# # Can replace this:
# render(:template, locals: {foo: 'bar'})
#
# # With this:
# part(:template, foo: 'bar')
#
# On Ruby 2.7+, the part method takes a keyword splat, so you
# must pass keywords and not a positional hash for the locals.
#
# If you are using the :assume_fixed_locals render plugin option,
# template caching is enabled, and you are using Ruby 3+, in
# addition to providing a simpler API, this also provides a
# significant performance improvement (more significant on Ruby
# 3.4+).
module Part
def self.load_dependencies(app)
app.plugin :render
end

def self.configure(app)
if app.render_opts[:assume_fixed_locals] && !app.render_opts[:check_template_mtime]
app.send(:include, AssumeFixedLocalsInstanceMethods)
end
end

module InstanceMethods
if RUBY_VERSION >= '2.7'
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
def part(template, **locals, &block)
render(template, :locals=>locals, &block)
end
RUBY
# :nocov:
else
def part(template, locals=OPTS, &block)
render(template, :locals=>locals, &block)
end
end
# :nocov:
end

module AssumeFixedLocalsInstanceMethods
# :nocov:
if RUBY_VERSION >= '3.0'
# :nocov:
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
def part(template, ...)
if optimized_method = _optimized_render_method_for_locals(template, OPTS)
send(optimized_method[0], ...)
else
super
end
end
RUBY
end
end
end

register_plugin(:part, Part)
end
end

160 changes: 160 additions & 0 deletions spec/plugin/part_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
require_relative "../spec_helper"

begin
require 'tilt'
require 'tilt/erb'
require 'tilt/string'
require_relative '../../lib/roda/plugins/render'
rescue LoadError
warn "tilt not installed, skipping part plugin test"
else
describe "part plugin" do
before do
app(:bare) do
plugin :render, :views=>"./spec/views", :check_paths=>true
plugin :part

route do |r|
r.on "home" do
part('layout', :title=>"Home"){part("home", :name => "Agent Smith", :title => "Home")}
end

r.on "about" do
part("about", :title => "About Roda")
end

r.on "inline" do
part({:inline=>"Hello <%= name %>"}, :name => "Agent Smith")
end

r.on "path" do
part({:path=>"./spec/views/about.erb"}, :title => "Path")
end

r.on "render-block" do
part('layout', :title=>"Home"){part("about", :title => "About Roda")}
end
end
end
end

it "default actions" do
body("/about").strip.must_equal "<h1>About Roda</h1>"
body("/home").strip.must_equal "<title>Roda: Home</title>\n<h1>Home</h1>\n<p>Hello Agent Smith</p>"
body("/inline").strip.must_equal "Hello Agent Smith"
body("/path").strip.must_equal "<h1>Path</h1>"
body("/render-block").strip.must_equal "<title>Roda: Home</title>\n<h1>About Roda</h1>"
end

it "with str as engine" do
app.plugin :render, :engine => "str"
body("/about").strip.must_equal "<h1>About Roda</h1>"
body("/home").strip.must_equal "<title>Roda: Home</title>\n<h1>Home</h1>\n<p>Hello Agent Smith</p>"
body("/inline").strip.must_equal "Hello <%= name %>"
end

if Roda::RodaPlugins::Render::FIXED_LOCALS_COMPILED_METHOD_SUPPORT
[true, false].each do |cache_plugin_option|
multiplier = cache_plugin_option ? 1 : 2

it "support fixed locals in layout templates with plugin option :cache=>#{cache_plugin_option}" do
template = "comp_test"

app(:bare) do
plugin :render, :views=>'spec/views/fixed', :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true}
plugin :part
route do
part("layout", title: "Home"){part(template)}
end
end

layout_key = [:_render_locals, "layout"]
template_key = [:_render_locals, template]
app.render_opts[:template_method_cache][template_key].must_be_nil
app.render_opts[:template_method_cache][layout_key].must_be_nil
body.strip.must_equal "<title>Roda: Home</title>\nct"
app.render_opts[:template_method_cache][template_key].must_be_kind_of(Array)
app.render_opts[:template_method_cache][layout_key].must_be_kind_of(Array)
body.strip.must_equal "<title>Roda: Home</title>\nct"
app.render_opts[:template_method_cache][template_key].must_be_kind_of(Array)
app.render_opts[:template_method_cache][layout_key].must_be_kind_of(Array)
body.strip.must_equal "<title>Roda: Home</title>\nct"
app.render_opts[:template_method_cache][template_key].must_be_kind_of(Array)
app.render_opts[:template_method_cache][layout_key].must_be_kind_of(Array)
app::RodaCompiledTemplates.private_instance_methods.length.must_equal(multiplier * 2)
end

it "support fixed locals in render templates with plugin option :cache=>#{cache_plugin_option}" do
template = "local_test"

app(:bare) do
plugin :render, :views=>'spec/views/fixed', :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true}
plugin :part
route do
part(template, title: 'ct')
end
end

key = [:_render_locals, template]
app.render_opts[:template_method_cache][key].must_be_nil
body.strip.must_equal "ct"
app.render_opts[:template_method_cache][key].must_be_kind_of(Array)
body.strip.must_equal "ct"
app.render_opts[:template_method_cache][key].must_be_kind_of(Array)
body.strip.must_equal "ct"
app.render_opts[:template_method_cache][key].must_be_kind_of(Array)
app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier
end

[true, false].each do |assume_fixed_locals_option|
it "caches expectedly for cache: #{cache_plugin_option}, assume_fixed_locals: #{assume_fixed_locals_option} options" do
template = "opt_local_test"

app(:bare) do
plugin :render, :views=>'spec/views/fixed', :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true}, :assume_fixed_locals=>assume_fixed_locals_option
plugin :part
route do |r|
r.is 'a' do
render(template)
end
part(template, title: 'ct')
end
end

cache_size = 1
key = if assume_fixed_locals_option
template
else
[:_render_locals, template]
end
cache = app.render_opts[:template_method_cache]
cache[key].must_be_nil
body.strip.must_equal "ct"
cache[key].must_be_kind_of(Array)
cache.instance_variable_get(:@hash).length.must_equal cache_size
body.strip.must_equal "ct"
cache[key].must_be_kind_of(Array)
cache.instance_variable_get(:@hash).length.must_equal cache_size
body.strip.must_equal "ct"
cache[key].must_be_kind_of(Array)
cache.instance_variable_get(:@hash).length.must_equal cache_size
app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier

cache_size = 2 unless assume_fixed_locals_option
key = template
body('/a').strip.must_equal "ct"
cache[key].must_be_kind_of(Array)
cache.instance_variable_get(:@hash).length.must_equal cache_size
body('/a').strip.must_equal "ct"
cache[key].must_be_kind_of(Array)
cache.instance_variable_get(:@hash).length.must_equal cache_size
body('/a').strip.must_equal "ct"
cache[key].must_be_kind_of(Array)
cache.instance_variable_get(:@hash).length.must_equal cache_size
app::RodaCompiledTemplates.private_instance_methods.length.must_equal(multiplier * cache_size)
end
end
end
end
end
end
1 change: 1 addition & 0 deletions www/pages/documentation.erb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
<li><a href="rdoc/classes/Roda/RodaPlugins/MultiView.html">multi_view</a>: Allows for easily setting up routing for rendering multiple views.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/NamedTemplates.html">named_templates</a>: Adds the ability to create inline templates by name, instead of storing them in the file system.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/PadrinoRender.html">padrino_render</a>: Makes render method that work similarly to Padrino's rendering, using a layout by default.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/Part.html">part</a>: Adds part method for simpler rendering of templates with locals.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/Partials.html">partials</a>: Adds partial method for rendering partials (templates prefixed with an underscore).</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/PrecompileTemplates.html">precompile_templates</a>: Adds support for precompiling templates, saving memory when using a forking webserver.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/Public.html">public</a>: Adds support for serving all files in the public directory.</li>
Expand Down

0 comments on commit 67fc63f

Please sign in to comment.