From 9015c290effe5c935f7501e4d8886ac25d85e125 Mon Sep 17 00:00:00 2001 From: Jared White Date: Sat, 4 May 2024 08:17:44 -0700 Subject: [PATCH] Make it easy for Roda routes to render "callable" objects (#892) * Make it easy for Roda routes to render "callable" objects * Test additional code paths --- bridgetown-core/lib/bridgetown-core.rb | 9 ++++++-- .../lib/bridgetown-core/component.rb | 2 +- .../lib/bridgetown-core/model/base.rb | 6 +++++ .../lib/bridgetown-core/resource/base.rb | 6 +++++ .../lib/roda/plugins/bridgetown_server.rb | 11 ---------- .../lib/roda/plugins/bridgetown_ssr.rb | 11 ++++++++++ bridgetown-core/test/ssr/config.ru | 2 ++ .../test/ssr/server/routes/hello.rb | 2 +- .../test/ssr/server/routes/render_resource.rb | 6 ++++- .../test/ssr/src/_components/UseRoda.rb | 22 +++++++++++++++++++ bridgetown-core/test/test_ssr.rb | 8 +++++++ 11 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 bridgetown-core/test/ssr/src/_components/UseRoda.rb diff --git a/bridgetown-core/lib/bridgetown-core.rb b/bridgetown-core/lib/bridgetown-core.rb index 6e0db5c69..9dde66dc7 100644 --- a/bridgetown-core/lib/bridgetown-core.rb +++ b/bridgetown-core/lib/bridgetown-core.rb @@ -379,9 +379,7 @@ def build_errors_path ) end end -end -module Bridgetown module CoreExt; end module Model; end @@ -395,6 +393,13 @@ def self.register_extension(mod) end end end + + # mixin for identity so Roda knows to call renderable objects + module RodaCallable + def self.===(other) + other.class < self + end + end end Zeitwerk::Loader.new.tap do |loader| diff --git a/bridgetown-core/lib/bridgetown-core/component.rb b/bridgetown-core/lib/bridgetown-core/component.rb index 1120db878..4dc4f2eb2 100644 --- a/bridgetown-core/lib/bridgetown-core/component.rb +++ b/bridgetown-core/lib/bridgetown-core/component.rb @@ -201,7 +201,7 @@ def render_in(view_context, &block) # Subclasses can override this method to return a string from their own # template handling. def template - call || _renderer.render(self) + (method(:call).arity.zero? ? call : nil) || _renderer.render(self) end # Typically not used but here as a compatibility nod toward ViewComponent. diff --git a/bridgetown-core/lib/bridgetown-core/model/base.rb b/bridgetown-core/lib/bridgetown-core/model/base.rb index fa3a164d0..909b8c3de 100644 --- a/bridgetown-core/lib/bridgetown-core/model/base.rb +++ b/bridgetown-core/lib/bridgetown-core/model/base.rb @@ -5,6 +5,7 @@ module Bridgetown module Model class Base + include Bridgetown::RodaCallable include ActiveModel::Model extend ActiveModel::Callbacks define_model_callbacks :load, :save, :destroy @@ -102,6 +103,11 @@ def render_as_resource to_resource.read!.transform! end + # Converts this model to a resource and returns the full output + # + # @return [String] + def call(*) = render_as_resource.output + # override if need be # @return [Bridgetown::Site] def site diff --git a/bridgetown-core/lib/bridgetown-core/resource/base.rb b/bridgetown-core/lib/bridgetown-core/resource/base.rb index 5344aa285..26e3d75b2 100644 --- a/bridgetown-core/lib/bridgetown-core/resource/base.rb +++ b/bridgetown-core/lib/bridgetown-core/resource/base.rb @@ -4,6 +4,7 @@ module Bridgetown module Resource class Base # rubocop:todo Metrics/ClassLength include Comparable + include Bridgetown::RodaCallable include Bridgetown::Publishable include Bridgetown::LayoutPlaceable include Bridgetown::LiquidRenderable @@ -171,6 +172,11 @@ def transform! # rubocop:todo Metrics/CyclomaticComplexity self end + # Transforms the resource and returns the full output + # + # @return [String] + def call(*) = transform!.output + def trigger_hooks(hook_name, *args) Bridgetown::Hooks.trigger collection.label.to_sym, hook_name, self, *args if collection Bridgetown::Hooks.trigger :resources, hook_name, self, *args diff --git a/bridgetown-core/lib/roda/plugins/bridgetown_server.rb b/bridgetown-core/lib/roda/plugins/bridgetown_server.rb index 900f89a60..986f0ea54 100644 --- a/bridgetown-core/lib/roda/plugins/bridgetown_server.rb +++ b/bridgetown-core/lib/roda/plugins/bridgetown_server.rb @@ -41,17 +41,6 @@ def self.load_dependencies(app) # rubocop:disable Metrics "500 Internal Server Error" end - # This lets us return models or resources directly in Roda response blocks - app.plugin :custom_block_results - - app.handle_block_result Bridgetown::Model::Base do |result| - result.render_as_resource.output - end - - app.handle_block_result Bridgetown::Resource::Base do |result| - result.transform!.output - end - # TODO: there may be a better way to do this, see `exception_page_css` instance method ExceptionPage.class_eval do # rubocop:disable Metrics/BlockLength def self.css diff --git a/bridgetown-core/lib/roda/plugins/bridgetown_ssr.rb b/bridgetown-core/lib/roda/plugins/bridgetown_ssr.rb index 3bf5a5779..97141c48a 100644 --- a/bridgetown-core/lib/roda/plugins/bridgetown_ssr.rb +++ b/bridgetown-core/lib/roda/plugins/bridgetown_ssr.rb @@ -9,6 +9,17 @@ module InstanceMethods def bridgetown_site self.class.opts[:bridgetown_site] end + + alias_method :site, :bridgetown_site + end + + def self.load_dependencies(app) + app.plugin :custom_block_results + + # This lets us return callable objects directly in Roda response blocks + app.handle_block_result(Bridgetown::RodaCallable) do |callable| + callable.(self) + end end def self.configure(app, _opts = {}, &) diff --git a/bridgetown-core/test/ssr/config.ru b/bridgetown-core/test/ssr/config.ru index 957991226..aaee05741 100644 --- a/bridgetown-core/test/ssr/config.ru +++ b/bridgetown-core/test/ssr/config.ru @@ -10,4 +10,6 @@ require "bridgetown-core/rack/boot" Bridgetown::Rack.boot +require_relative "src/_components/UseRoda" # normally Zeitwerk would take care of this for us + run RodaApp.freeze.app # see server/roda_app.rb diff --git a/bridgetown-core/test/ssr/server/routes/hello.rb b/bridgetown-core/test/ssr/server/routes/hello.rb index b737e655b..edebce028 100644 --- a/bridgetown-core/test/ssr/server/routes/hello.rb +++ b/bridgetown-core/test/ssr/server/routes/hello.rb @@ -4,7 +4,7 @@ class Routes::Hello < Bridgetown::Rack::Routes priority :lowest route do |r| - saved_value = bridgetown_site.data.save_value + saved_value = site.data.save_value # route: GET /hello/:name r.get "hello", String do |name| diff --git a/bridgetown-core/test/ssr/server/routes/render_resource.rb b/bridgetown-core/test/ssr/server/routes/render_resource.rb index 07798b00e..4a49e27a2 100644 --- a/bridgetown-core/test/ssr/server/routes/render_resource.rb +++ b/bridgetown-core/test/ssr/server/routes/render_resource.rb @@ -5,12 +5,16 @@ class Routes::RenderResource < Bridgetown::Rack::Routes # route: GET /render_resource r.get "render_resource" do # Roda should know how to autorender the resource - bridgetown_site.collections.pages.resources.find { _1.id == "repo://pages.collection/index.md" } + site.collections.pages.find { _1.id == "repo://pages.collection/index.md" } end r.get "render_model" do # Roda should know how to autorender the model as a resource Bridgetown::Model::Base.find("repo://pages.collection/test_doc.md") end + + r.get "render_component", String do |title| + UseRoda.new(title:) + end end end diff --git a/bridgetown-core/test/ssr/src/_components/UseRoda.rb b/bridgetown-core/test/ssr/src/_components/UseRoda.rb new file mode 100644 index 000000000..14dd7fb84 --- /dev/null +++ b/bridgetown-core/test/ssr/src/_components/UseRoda.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class UseRoda < Bridgetown::Component + include Bridgetown::RodaCallable + + def initialize(title:) # rubocop:disable Lint/MissingSuper + @title = title.upcase + end + + def template + "#{@title} #{@testing}" # not real RSS =) + end + + def call(app) + app.request => r + @testing = r.env["rack.test"] + + app.response["Content-Type"] = "application/rss+xml" + + render_in(app) + end +end diff --git a/bridgetown-core/test/test_ssr.rb b/bridgetown-core/test/test_ssr.rb index 36fd247e3..69fe97447 100644 --- a/bridgetown-core/test/test_ssr.rb +++ b/bridgetown-core/test/test_ssr.rb @@ -85,5 +85,13 @@ def site assert last_response.ok? assert_includes last_response.body, "

THIS IS A TEST.

" end + + should "return rendered component" do + get "/render_component/wow" + + assert last_response.ok? + assert_equal "application/rss+xml", last_response["Content-Type"] + assert_equal "WOW true", last_response.body + end end end