feat: extend renderable with csrf and csrf_token

This commit is contained in:
dualfroz committed 2026-09-06 21:56:26 +02:00
1 parent b9fc8a09d2
commit 721d576956
3 files changed
+181 -4

No files matched your search

+70
View File
@@ -13,8 +13,23 @@ module Prism
end
def cookies
@cookie_jar ||= begin
secret = @env['prism.secret']
if secret
CookieJar.new(@request, @response, secret: secret, previous_secrets: @env['prism.previous_secrets'] || [])
else
@request.cookies
end
end
end
def csrf
@csrf ||= Csrf.new(session)
end
def csrf_token
csrf.masked_token
end
def status(code = nil)
@response.status = code if code
@@ -34,6 +49,21 @@ module Prism
payload
end
# JSON:API-lite error response for a failed-validation record: `{
# "errors": [...] }` with each entry built by Orm::Errors#to_jsonapi.
# Mirrors `render_errors(post)` -> `format.json` the same way `render`
# covers the HTML branch of a respond_to block.
def render_errors(record, status: 422)
json({ errors: record.errors.to_jsonapi(status: status) }, status)
end
# JSON:API-lite "not found" error, the 404 counterpart to render_errors.
# No `source.pointer` (unlike a validation error, a missing resource
# does not point at one attribute of it).
def render_not_found(detail, status: 404)
json({ errors: [{ status: status.to_s, title: 'Resource Not Found', detail: detail }] }, status)
end
def html(markup, code = nil)
@response.status = code if code
@response['Content-Type'] = 'text/html;charset=utf-8'
@@ -41,6 +71,15 @@ module Prism
markup
end
# ActionController::Live-lite: `stream { |out| out.write(row) }` sends
# the response as HTTP/1.1 chunked Transfer-Encoding instead of
# buffering the whole body first - see Prism::StreamingBody and
# Server#write_response. Nothing inside the block runs here; it only
# runs once the server starts sending the response (see Response#stream).
def stream(&block)
@response.stream(&block)
end
def redirect(location, code = 302)
@response.status = code
@response['Location'] = location
@@ -73,5 +112,36 @@ module Prism
def finalize_flash
@flash.sweep if @flash
end
# Locale for this request: an explicit ?locale= param wins, then a
# sticky value stashed in the session, then the client's
# Accept-Language header, then Prism::I18n.default_locale.
def locale
@locale ||= detect_locale
end
def translate(key, **options)
Prism::I18n.t(key, locale: options.delete(:locale) || locale, **options)
end
alias_method :t, :translate
def localize(value, **options)
Prism::I18n.l(value, locale: options.delete(:locale) || locale, **options)
end
alias_method :l, :localize
private
def detect_locale
requested = params['locale'] || session[:locale]
return requested.to_sym if requested && available_locale?(requested)
negotiated = Prism::I18n.best_locale_from_accept(@request.headers['Accept-Language'])
negotiated || Prism::I18n.default_locale
end
def available_locale?(candidate)
Prism::I18n.available_locales.map(&:to_s).include?(candidate.to_s)
end
end
end
+34 -4
View File
@@ -6,8 +6,10 @@ module Prism
@env = env
end
OverridableMethods = %w[PUT PATCH DELETE].freeze
def request_method
@env['REQUEST_METHOD']
@request_method ||= compute_request_method
end
def path
@@ -22,10 +24,20 @@ module Prism
@env['CONTENT_TYPE']
end
def user_agent
@env['HTTP_USER_AGENT']
end
def remote_addr
@env['REMOTE_ADDR']
end
def body
@body ||= begin
input = @env['rack.input']
input ? input.read : ''
content = input ? input.read : ''
input.rewind if input.respond_to?(:rewind)
content
end
end
@@ -67,11 +79,27 @@ module Prism
end
end
# Negotiated response format: an explicit `.json` path/query extension
# wins, otherwise it falls back to the Accept header, defaulting to
# :html when nothing else matches (mirrors Rails' request.format).
def format
explicit = path_params['format'] || query_params['format']
return explicit.to_s.to_sym unless explicit.nil? || explicit.to_s.empty?
Mime.best_from_accept(headers['Accept']) || :html
end
private
def compute_request_method
actual = @env['REQUEST_METHOD']
return actual unless actual == 'POST'
override = (@env['HTTP_X_HTTP_METHOD_OVERRIDE'] || body_params['_method']).to_s.upcase
OverridableMethods.include?(override) ? override : actual
end
def parse_query(string)
return Params.new if string.empty?
Params.from(URI.decode_www_form(string).to_h)
Params.nested_from(URI.decode_www_form(string))
rescue ArgumentError
Params.new
end
@@ -82,7 +110,9 @@ module Prism
parsed = JSON.parse(body)
parsed.is_a?(Hash) ? Params.from(parsed) : Params.from('_json' => parsed)
elsif content_type&.include?('application/x-www-form-urlencoded')
Params.from(URI.decode_www_form(body).to_h)
Params.nested_from(URI.decode_www_form(body))
elsif Multipart.multipart?(content_type)
Multipart.parse(body, Multipart.boundary_from(content_type))
else
Params.new
end
+77
View File
@@ -0,0 +1,77 @@
module Prism
# DSL context yielded (via instance_eval) inside `resources ... do ... end`
# and `resource ... do ... end` blocks. Provides member/collection routes
# and nesting further resources under the current one.
class ResourceScope
def initialize(app, name, singular, param)
@app = app
@name = name
@singular = singular
@param = param
end
# Extra actions scoped to one member: /name/:id/action -> name#action,
# named "action_singular" (e.g. `publish_post`).
def member(&block)
require_member_param!
@app.group("/#{@name}/:#{@param}") do
MemberCollectionScope.new(@app, @singular, @name, :member).instance_eval(&block)
end
end
# Extra actions scoped to the whole collection: /name/action -> name#action,
# named "action_name" (e.g. `search_posts`).
def collection(&block)
@app.group("/#{@name}") do
MemberCollectionScope.new(@app, @singular, @name, :collection).instance_eval(&block)
end
end
# Nested resources, scoped under this resource: for a plural parent
# that's /name/:singular_id/nested_name/... (Rails' classic nested
# routing); for a singular parent (no id at all) it's just
# /name/nested_name/....
def resources(nested_name, **options, &block)
@app.group(nested_prefix) { @app.resources(nested_name, **options, &block) }
end
def resource(nested_name, **options, &block)
@app.group(nested_prefix) { @app.resource(nested_name, **options, &block) }
end
private
def nested_prefix
@param ? "/#{@name}/:#{@singular}_id" : "/#{@name}"
end
def require_member_param!
return if @param
raise ArgumentError, 'member/collection routes need an id segment; not available on a singular resource'
end
end
# DSL context for the block passed to ResourceScope#member/#collection.
class MemberCollectionScope
def initialize(app, singular, plural, kind)
@app = app
@singular = singular
@plural = plural
@kind = kind
end
%w[get post put patch delete].each do |verb|
define_method(verb) do |action, to: nil, as: nil|
controller_action = to || "#{@plural}##{action}"
name = as || default_name(action)
@app.public_send(verb, "/#{action}", to: controller_action, as: name)
end
end
private
def default_name(action)
@kind == :member ? "#{action}_#{@singular}" : "#{action}_#{@plural}"
end
end
end