feat: extend responder with Prism and Responder
This commit is contained in:
1 parent
721d576956
commit
22c5d77da0
4 files changed
+135
-10
No files matched your search
@@ -0,0 +1,41 @@
|
|||||||
|
module Prism
|
||||||
|
# Collects per-format render blocks for Controller#respond_to, mirroring
|
||||||
|
# Rails' `respond_to { |format| format.html { ... }; format.json { ... } }`.
|
||||||
|
class Responder
|
||||||
|
class UnknownFormat < StandardError; end
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@blocks = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def method_missing(name, &block)
|
||||||
|
return super unless block
|
||||||
|
@blocks[name.to_sym] = block
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_to_missing?(_name, _include_private = false)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Registers a fallback block for any format not already handled.
|
||||||
|
# `any` (no args) applies to every format that gets a respond() call
|
||||||
|
# later; `any(:json, :xml)` restricts the fallback to those formats.
|
||||||
|
def any(*formats, &block)
|
||||||
|
if formats.empty?
|
||||||
|
@fallback = block
|
||||||
|
else
|
||||||
|
formats.each { |format| @blocks[format.to_sym] ||= block }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handles?(format)
|
||||||
|
@blocks.key?(format.to_sym) || !@fallback.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond(format)
|
||||||
|
handler = @blocks[format.to_sym] || @fallback
|
||||||
|
raise UnknownFormat, "no handler registered for format #{format.inspect}" unless handler
|
||||||
|
handler.call
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
+22
-1
@@ -23,8 +23,29 @@ module Prism
|
|||||||
@headers[name] = value
|
@headers[name] = value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# A StreamingBody (or anything else that already knows how to hand
|
||||||
|
# itself to a Rack-style server one chunk at a time via #each) is kept
|
||||||
|
# as-is rather than wrapped in Array(value) - wrapping it would produce
|
||||||
|
# a one-element Array whose element is the streaming object itself,
|
||||||
|
# which Server#write_response's ordinary `body.join` path cannot join
|
||||||
|
# into a String, and which would defeat the entire point of streaming
|
||||||
|
# (nothing would be sent until the whole thing was collected anyway).
|
||||||
def body=(value)
|
def body=(value)
|
||||||
@body = Array(value)
|
@body = value.respond_to?(:each) ? value : Array(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sets the body to a StreamingBody wrapping `block`. The block itself
|
||||||
|
# does not run yet - only once whatever consumes the finished response
|
||||||
|
# (Server#write_response, or a test calling body.each directly) starts
|
||||||
|
# pulling chunks, exactly like an ordinary Rack body Array is not
|
||||||
|
# "used up" until something calls each/join on it either.
|
||||||
|
def stream(&block)
|
||||||
|
self.body = StreamingBody.new(&block)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def streaming?
|
||||||
|
@body.is_a?(StreamingBody)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_cookie(name, value, options = {})
|
def set_cookie(name, value, options = {})
|
||||||
|
|||||||
+67
-9
@@ -2,9 +2,16 @@ module Prism
|
|||||||
class PathMatcher
|
class PathMatcher
|
||||||
attr_reader :keys, :pattern
|
attr_reader :keys, :pattern
|
||||||
|
|
||||||
def initialize(pattern)
|
# constraints: { param_name => Regexp } narrows a dynamic segment's match.
|
||||||
|
# format: true appends an optional trailing ".xml"/".json"/... capture
|
||||||
|
# (Rails' implicit `(.:format)`) and reserves the segment class [^/.]+
|
||||||
|
# so dynamic segments no longer swallow the extension. Left off by
|
||||||
|
# default so plain routes keep matching literal dots exactly as before.
|
||||||
|
def initialize(pattern, constraints: {}, format: false)
|
||||||
@pattern = pattern
|
@pattern = pattern
|
||||||
@keys = []
|
@keys = []
|
||||||
|
@constraints = constraints.transform_keys(&:to_s)
|
||||||
|
@format = format && !pattern.include?('*')
|
||||||
@regex = compile(pattern)
|
@regex = compile(pattern)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -37,8 +44,9 @@ module Prism
|
|||||||
source = +'\A'
|
source = +'\A'
|
||||||
pattern.scan(/:\w+|\*|[^:*]+/).each do |token|
|
pattern.scan(/:\w+|\*|[^:*]+/).each do |token|
|
||||||
if token.start_with?(':')
|
if token.start_with?(':')
|
||||||
@keys << token[1..]
|
key = token[1..]
|
||||||
source << '([^/]+)'
|
@keys << key
|
||||||
|
source << segment_source(key)
|
||||||
elsif token == '*'
|
elsif token == '*'
|
||||||
@keys << 'splat'
|
@keys << 'splat'
|
||||||
source << '(.*?)'
|
source << '(.*?)'
|
||||||
@@ -46,15 +54,26 @@ module Prism
|
|||||||
source << Regexp.escape(token)
|
source << Regexp.escape(token)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if @format
|
||||||
|
@keys << 'format'
|
||||||
|
source << '(?:\.([^/.]+))?'
|
||||||
|
end
|
||||||
source << '\z'
|
source << '\z'
|
||||||
Regexp.new(source)
|
Regexp.new(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def segment_source(key)
|
||||||
|
constraint = @constraints[key]
|
||||||
|
return "(#{constraint.source})" if constraint
|
||||||
|
@format ? '([^/.]+)' : '([^/]+)'
|
||||||
|
end
|
||||||
|
|
||||||
def extract(data)
|
def extract(data)
|
||||||
params = {}
|
params = {}
|
||||||
splats = []
|
splats = []
|
||||||
@keys.each_with_index do |key, index|
|
@keys.each_with_index do |key, index|
|
||||||
value = data.captures[index]
|
value = data.captures[index]
|
||||||
|
next if value.nil?
|
||||||
if key == 'splat'
|
if key == 'splat'
|
||||||
splats << value
|
splats << value
|
||||||
else
|
else
|
||||||
@@ -67,11 +86,12 @@ module Prism
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Route
|
class Route
|
||||||
attr_reader :method, :block, :name, :controller_name, :action_name
|
attr_reader :method, :block, :name, :controller_name, :action_name, :pattern
|
||||||
|
|
||||||
def initialize(method, pattern, block: nil, name: nil, to: nil)
|
def initialize(method, pattern, block: nil, name: nil, to: nil, constraints: {}, format: false)
|
||||||
@method = method
|
@method = method
|
||||||
@matcher = PathMatcher.new(pattern)
|
@pattern = pattern
|
||||||
|
@matcher = PathMatcher.new(pattern, constraints: constraints, format: format)
|
||||||
@block = block
|
@block = block
|
||||||
@name = name&.to_sym
|
@name = name&.to_sym
|
||||||
@controller_name, @action_name = to&.split('#', 2)
|
@controller_name, @action_name = to&.split('#', 2)
|
||||||
@@ -92,15 +112,19 @@ module Prism
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Router
|
class Router
|
||||||
attr_reader :routes
|
attr_reader :routes, :mounts, :websockets
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@routes = []
|
@routes = []
|
||||||
@named_routes = {}
|
@named_routes = {}
|
||||||
|
@mounts = []
|
||||||
|
@websockets = {}
|
||||||
|
@websocket_options = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
def add(method, pattern, name: nil, to: nil, &block)
|
def add(method, pattern, name: nil, to: nil, constraints: {}, format: false, &block)
|
||||||
route = Route.new(method.to_s.upcase, pattern, block: block, name: name, to: to)
|
route = Route.new(method.to_s.upcase, pattern, block: block, name: name, to: to,
|
||||||
|
constraints: constraints, format: format)
|
||||||
@routes << route
|
@routes << route
|
||||||
@named_routes[name.to_sym] = route if name
|
@named_routes[name.to_sym] = route if name
|
||||||
route
|
route
|
||||||
@@ -117,5 +141,39 @@ module Prism
|
|||||||
route = @named_routes.fetch(name.to_sym) { raise ArgumentError, "no named route #{name.inspect}" }
|
route = @named_routes.fetch(name.to_sym) { raise ArgumentError, "no named route #{name.inspect}" }
|
||||||
route.path(params)
|
route.path(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Delegates every request under `prefix` to a separate Rack app (an
|
||||||
|
# engine, in Rails terms), like Rack::URLMap - checked only after no
|
||||||
|
# regular route matches, so a mounted app never shadows the host
|
||||||
|
# application's own routes. Longest prefix wins when mounts overlap.
|
||||||
|
def mount(prefix, app)
|
||||||
|
prefix = prefix.chomp('/')
|
||||||
|
@mounts << [prefix, app]
|
||||||
|
@mounts.sort_by! { |mounted_prefix, _| -mounted_prefix.length }
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_mount(path)
|
||||||
|
@mounts.find { |prefix, _| path == prefix || path.start_with?("#{prefix}/") }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Registers a WebSocket endpoint (an exact path, unlike `mount` - a
|
||||||
|
# single Upgrade: websocket handshake target like ActionCable's one
|
||||||
|
# "/cable" mount, not a prefix for a whole sub-application).
|
||||||
|
def add_websocket(path, handler, max_message_size: nil)
|
||||||
|
@websockets[path] = handler
|
||||||
|
@websocket_options[path] = { max_message_size: max_message_size }
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_websocket(path)
|
||||||
|
@websockets[path]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Per-path options registered alongside the handler (currently just
|
||||||
|
# `max_message_size:`) - kept separate from `find_websocket` rather than
|
||||||
|
# folded into its return value so that method keeps returning the bare
|
||||||
|
# handler class callers already match on.
|
||||||
|
def find_websocket_options(path)
|
||||||
|
@websocket_options[path] || {}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
require_relative 'security/message_verifier'
|
||||||
|
require_relative 'security/message_encryptor'
|
||||||
|
require_relative 'security/strong_parameters'
|
||||||
|
require_relative 'security/csrf'
|
||||||
|
require_relative 'security/cookie_jar'
|
||||||
Reference in new issue
Block a user