feat: extend conditional_get with Prism and ConditionalGet

This commit is contained in:
dualfroz committed 2026-09-06 20:18:31 +02:00
1 parent 7478aeadf2
commit 31799fc5af
2 files changed
+147 -8

No files matched your search

+72 -8
View File
@@ -1,22 +1,48 @@
module Prism
class Controller
include Renderable
include ConditionalGet
class << self
# Combines this controller's own filters with everything declared on
# its ancestors, ancestors first, so a shared ApplicationController can
# declare `protect_from_forgery`/`before_action :require_authentication`
# once and have every subclass run it - the same way `layout`/
# `views_root` already fall back to the superclass.
def before_actions
@before_actions ||= []
own = @before_actions ||= []
superclass.respond_to?(:before_actions) ? superclass.before_actions + own : own
end
def after_actions
@after_actions ||= []
own = @after_actions ||= []
superclass.respond_to?(:after_actions) ? superclass.after_actions + own : own
end
# Own handlers first (most recently declared first, like Rails'
# reverse-declaration-order matching), then the superclass's - so an
# ApplicationController-wide `rescue_from StandardError` never shadows
# a more specific handler a subclass declares for itself.
def rescue_handlers
own = (@rescue_handlers ||= []).reverse
superclass.respond_to?(:rescue_handlers) ? own + superclass.rescue_handlers : own
end
def rescue_from(*exception_classes, with: nil, &block)
raise ArgumentError, 'rescue_from needs with: or a block' unless with || block
exception_classes.each { |klass| (@rescue_handlers ||= []) << { klass: klass, with: with, block: block } }
end
def before_action(*names, only: nil, except: nil, &block)
register_filter(before_actions, names, only, except, block)
register_filter(@before_actions ||= [], names, only, except, block)
end
def after_action(*names, only: nil, except: nil, &block)
register_filter(after_actions, names, only, except, block)
register_filter(@after_actions ||= [], names, only, except, block)
end
def protect_from_forgery(only: nil, except: nil)
before_action(:verify_authenticity_token!, only: only, except: except)
end
def layout(name = :missing)
@@ -29,8 +55,11 @@ module Prism
@views_root || superclass_views_root || 'app/views'
end
# "posts" for PostsController, "admin/posts" for Admin::PostsController
# (keeps the full module path so namespaced views live under
# app/views/admin/posts/, matching Rails' convention).
def controller_path
Inflector.underscore(name.to_s.split('::').last.sub(/Controller\z/, ''))
Inflector.underscore(name.to_s.sub(/Controller\z/, ''))
end
private
@@ -61,8 +90,12 @@ module Prism
def process
catch(:halt) do
run_before_actions
finalize_body(send(@action_name))
begin
run_before_actions
finalize_body(send(@action_name))
rescue StandardError => e
handle_exception(e)
end
end
catch(:halt) { run_after_actions }
finalize_flash
@@ -82,9 +115,40 @@ module Prism
view_resolver.render_collection(partial_path(name), collection, locals: locals, as: as)
end
# Rails-style content negotiation: `respond_to { |f| f.html { render }; f.json { json(x) } }`.
# Renders 406 when the negotiated format (see Request#format) has no matching block.
def respond_to
responder = Responder.new
yield responder
format = request.format
responder.respond(format)
rescue Responder::UnknownFormat
halt(406, "Not Acceptable: #{format}")
end
private
UnsafeMethods = %w[POST PUT PATCH DELETE].freeze
def verify_authenticity_token!
return unless UnsafeMethods.include?(@request.request_method)
token = params['authenticity_token'] || @request.headers['X-Csrf-Token']
halt(422, 'invalid authenticity token') unless csrf.valid_token?(token)
end
# Finds the most specific registered `rescue_from` handler (`is_a?`
# against the raised exception) and runs it - the handler is expected
# to finish the response itself (`halt`/`render`/`redirect`), the same
# contract Rails' rescue_from has. Re-raises when nothing matches, so
# an unhandled exception still reaches the application-level fallback.
def handle_exception(exception)
handler = self.class.rescue_handlers.find { |candidate| exception.is_a?(candidate[:klass]) }
raise exception unless handler
handler[:block] ? instance_exec(exception, &handler[:block]) : send(handler[:with], exception)
end
def finalize_body(result)
return if @response.streaming?
return unless @response.body.empty?
@response.body = [result.to_s] if result.is_a?(String)
end
@@ -94,7 +158,7 @@ module Prism
end
def view_resolver
@view_resolver ||= View.new(self.class.views_root)
@view_resolver ||= View.new(self.class.views_root, helpers: self)
end
def run_before_actions