header modifiers, status modifier, proper call rollbacks

This commit is contained in:
Yessiest 2023-09-06 02:34:17 +04:00
parent a878ac58be
commit fccc7ea9f0
13 changed files with 383 additions and 8 deletions

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/lib")
require_relative 'lib/hyde' require_relative 'lib/hyde'
app = Hyde::Server.new do app = Hyde::Server.new do
@ -18,7 +21,17 @@ app = Hyde::Server.new do
end end
path "/match2/*/" do path "/match2/*/" do
probe "probe" head "probe" do
"<html><body><p>Hello world!</p></body></html>"
end
get "probe" do
code 400
header "Random-Shit", "peepee"
"<html><body><p>Hello world!</p></body></html>"
end
post "probe" do
"<html><body><p>Hello world!</p></body></html>"
end
end end
end end

View File

@ -7,7 +7,6 @@ require_relative 'hyde/request'
require_relative 'hyde/response' require_relative 'hyde/response'
# Hyde is a hideously simple ruby web framework # Hyde is a hideously simple ruby web framework
#
module Hyde module Hyde
# Hyde version # Hyde version
# @type [String] # @type [String]

View File

@ -36,3 +36,24 @@ These are module mixins that add common methods to DSL bindings.
These are self-contained classes and methods that add extra functionality to Hyde. These are self-contained classes and methods that add extra functionality to Hyde.
- Hyde::Util::Lookup [util/lookup.rb] - Hyde::Util::Lookup [util/lookup.rb]
## Probe subclasses
These are reactive request handlers with their own semantics, if needed.
- Hyde::Handler [probe/handler.rb]
- Hyde::GETHandler [probe/http\_method.rb]
- Hyde::POSTHandler [probe/http\_method.rb]
- Hyde::HEADHandler [probe/http\_method.rb]
- Hyde::PUTHandler [probe/http\_method.rb]
- Hyde::DELETEHandler [probe/http\_method.rb]
- Hyde::CONNECTHandler [probe/http\_method.rb]
- Hyde::OPTIONSHandler [probe/http\_method.rb]
- Hyde::TRACEHandler [probe/http\_method.rb]
- Hyde::PATCHHandler [probe/http\_method.rb]
## Path subclasses
These are navigation handlers with their own semantics.
(currently none)

View File

@ -25,6 +25,52 @@ module Hyde
def probe(path, &_setup) def probe(path, &_setup)
register(Hyde::Probe.new(path, parent: @origin)) register(Hyde::Probe.new(path, parent: @origin))
end end
# Create a new {Hyde::GETHandler} object
def get(path, &setup)
register(Hyde::GETHandler.new(path, parent: @origin, &setup))
end
# create a new {Hyde::POSTHandler} object
def post(path, &setup)
register(Hyde::POSTHandler.new(path, parent: @origin, &setup))
end
# Create a new {Hyde::PUTHandler} object
def put(path, &setup)
register(Hyde::PUTHandler.new(path, parent: @origin, &setup))
end
# Create a new {Hyde::HEADHandler} object
def head(path, &setup)
register(Hyde::HEADHandler.new(path, parent: @origin, &setup))
end
# Create a new {Hyde::DELETEHandler} object
def delete(path, &setup)
register(Hyde::DELETEHandler.new(path, parent: @origin, &setup))
end
# Create a new {Hyde::CONNECTHandler} object
def connect(path, &setup)
register(Hyde::CONNECTHandler.new(path, parent: @origin, &setup))
end
# Create a new {Hyde::TRACEHandler} object
def trace(path, &setup)
register(Hyde::TRACEHandler.new(path, parent: @origin, &setup))
end
# Create a new {Hyde::PATCHHandler} object
def patch(path, &setup)
register(Hyde::PATCHHandler.new(path, parent: @origin, &setup))
end
# Create a new {Hyde::OPTIONSHandler} object
def options(path, &setup)
register(Hyde::OPTIONSHandler.new(path, parent: @origin, &setup))
end
end end
end end
end end

View File

@ -0,0 +1,84 @@
# frozen_string_literal: true
require_relative '../response'
module Hyde
module DSL
# Common methods for Probe objects
module ProbeMethods
# Get the current request
# @return [Hyde::Request]
def request
@request
end
# Stop execution and generate a boilerplate response with the given code
# @param errorcode [Integer]
# @param backtrace [Array(String), nil]
# @raise [UncaughtThrowError] throws :finish to return back to Server
def die(errorcode, backtrace: nil)
throw :finish, [errorcode].append(
*(@properties["handle.#{errorcode}"] or
@properties["handle.default"]).call(
errorcode,
backtrace: backtrace
)
)
end
# Bounce request to the next handler
# @raise [UncaughtThrowError] throws :break to get out of the callback
def bounce
raise :break
end
# Set response status (generate response if one doesn't exist yet)
# @param status [Integer] http status code
def status(status)
@response = (@response or Hyde::Response.new)
@response.status = status
end
alias code status
# Set response header (generate response if one doesn't exist yet)
# @param key [String] header name
# @param value [String] header value
def header(key, value)
return status(value) if key.downcase == "status"
if key.match(/(?:[(),\/:;<=>?@\[\]{}"]|[^ -~])/)
raise StandardError, "header key has invalid characters"
end
if value.match(/[^ -~]/)
raise StandardError, "value key has invalid characters"
end
@origin.response = (@origin.response or Hyde::Response.new)
key = key.downcase
@origin.response.add_header(key, value)
end
# Delete a header value from the headers hash
# If no value is provided, deletes all key entries
# @param key [String] header name
# @param value [String, nil] header value
def remove_header(key, value = nil)
return unless @origin.response
return if key.downcase == "status"
if key.match(/(?:[(),\/:;<=>?@\[\]{}"]|[^ -~])/)
raise StandardError, "header key has invalid characters"
end
if value&.match(/[^ -~]/)
raise StandardError, "value key has invalid characters"
end
@origin.response.delete_header(key, value)
end
end
end
end

View File

@ -15,12 +15,18 @@ module Hyde
# @param [Hyde::Request] # @param [Hyde::Request]
# @return [Boolean] # @return [Boolean]
def go(request) def go(request)
# rejected at pattern
return reject(request) unless @pattern.match?(request.path) return reject(request) unless @pattern.match?(request.path)
request.push_state
request.path, splat, param = @pattern.match(request.path) request.path, splat, param = @pattern.match(request.path)
request.splat.append(*splat) request.splat.append(*splat)
request.param.merge!(param) request.param.merge!(param)
process(request) value = process(request)
# rejected at callback - restore state
request.pop_state unless value
# finally, return process value
value
end end
# Method callback on failed request navigation # Method callback on failed request navigation

View File

@ -33,7 +33,7 @@ module Hyde
# Method callback on successful request navigation. # Method callback on successful request navigation.
# Finds the next appropriate path to go to. # Finds the next appropriate path to go to.
# @return [Boolean] true if further navigation is possible # @return [Boolean] true if further navigation will be done
# @raise [UncaughtThrowError] by default throws :response if no matches found. # @raise [UncaughtThrowError] by default throws :response if no matches found.
def process(request) def process(request)
@children.each do |x| @children.each do |x|
@ -41,6 +41,9 @@ module Hyde
return value return value
end end
end end
value = index(request)
return value if value
_die(404) _die(404)
rescue StandardError => e rescue StandardError => e
_die(500, backtrace: [e.to_s] + e.backtrace) _die(500, backtrace: [e.to_s] + e.backtrace)
@ -50,6 +53,23 @@ module Hyde
private private
# Try to perform indexing on the path if possible
# @param request [Hyde::Request]
# @return [Boolean] true if indexing succeeded
def index(request)
return false unless request.path.match?(/^\/?$/)
@properties["index"].each do |index|
request.path = index
@children.each do |x|
if (value = x.go(request))
return value
end
end
end
false
end
# Handle an errorcode # Handle an errorcode
# @param errorcode [Integer] # @param errorcode [Integer]
# @param backtrace [Array(String), nil] # @param backtrace [Array(String), nil]

View File

@ -4,6 +4,17 @@ require_relative 'node'
require_relative 'util/lookup' require_relative 'util/lookup'
module Hyde module Hyde
autoload :Handler, "hyde/probe/handler"
autoload :GETHandler, "hyde/probe/http_method"
autoload :POSTHandler, "hyde/probe/http_method"
autoload :HEADHandler, "hyde/probe/http_method"
autoload :PUTHandler, "hyde/probe/http_method"
autoload :DELETEHandler, "hyde/probe/http_method"
autoload :CONNECTHandler, "hyde/probe/http_method"
autoload :OPTIONSHandler, "hyde/probe/http_method"
autoload :TRACEHandler, "hyde/probe/http_method"
autoload :PATCHHandler, "hyde/probe/http_method"
# Test probe. Also base for all "reactive" nodes. # Test probe. Also base for all "reactive" nodes.
class Probe < Hyde::Node class Probe < Hyde::Node
# @param path [Object] # @param path [Object]
@ -16,6 +27,7 @@ module Hyde
# Method callback on successful request navigation. # Method callback on successful request navigation.
# Throws an error upon reaching the path. # Throws an error upon reaching the path.
# This behaviour should only be used internally. # This behaviour should only be used internally.
# @param request [Hyde::Request]
# @return [Boolean] true if further navigation is possible # @return [Boolean] true if further navigation is possible
# @raise [StandardError] # @raise [StandardError]
def process(request) def process(request)

12
lib/hyde/probe/binding.rb Normal file
View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
require_relative "../dsl/probe_methods"
module Hyde
class ProbeBinding
def initialize(origin)
@origin = origin
end
include Hyde::DSL::ProbeMethods
end
end

51
lib/hyde/probe/handler.rb Normal file
View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
require_relative '../probe'
require_relative 'binding'
module Hyde
# Probe that executes a callback on request
class Handler < Hyde::Probe
# @param path [Object]
# @param parent [Hyde::Node]
# @param exec [#call]
def initialize(path, parent:, &exec)
super(path, parent: parent)
@callback = exec
@binding = Hyde::ProbeBinding.new(self)
@response = nil
end
attr_accessor :response
# Method callback on successful request navigation.
# Runs block supplied with object initialization.
# Request's #splat and #param are passed to block.
#
# Callback's returned should be one of viable responses:
#
# - {Hyde::Response} object
# - An array that matches Rack return form
# - An array that matches old (Rack 2.x) return form
# - A string (returned as HTML with code 200)
# - false (bounces the request to next handler)
# @param request [Hyde::Request]
# @return [Boolean] true if further navigation is possible
# @raise [UncaughtThrowError] may raise if die() is called.
def process(request)
@request = request
response = catch(:break) do
@binding.instance_exec(*request.splat,
**request.param,
&@callback)
end
return false unless response
if @response and [String, File, IO].include? response.class
@response.body = response
throw :finish, @response
end
throw :finish, response
end
end
end

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
require_relative '../probe'
require_relative 'binding'
require_relative 'handler'
module Hyde
# Probe that executes callback on a GET
class GETHandler < Hyde::Handler
METHOD = "GET"
# Method callback on successful request navigation.
# Runs block supplied with object initialization.
# Request's #splat and #param are passed to block.
#
# Callback's returned should be one of viable responses:
#
# - {Hyde::Response} object
# - An array that matches Rack return form
# - An array that matches old (Rack 2.x) return form
# - A string (returned as HTML with code 200)
# - false (bounces the request to next handler)
# @param request [Hyde::Request]
# @return [Boolean] true if further navigation is possible
# @raise [UncaughtThrowError] may raise if die() is called.
def process(request)
unless request.request_method.casecmp(self.class::METHOD).zero?
return false
end
super(request)
end
end
# Probe that executes callback on a POST
class POSTHandler < GETHandler
METHOD = "POST"
end
# Probe that executes callback on a HEAD
class HEADHandler < GETHandler
METHOD = "HEAD"
end
# Probe that executes callback on a PUT
class PUTHandler < GETHandler
METHOD = "PUT"
end
# Probe that executes callback on a DELETE
class DELETEHandler < GETHandler
METHOD = "DELETE"
end
# Probe that executes callback on a CONNECT
class CONNECTHandler < GETHandler
METHOD = "CONNECT"
end
# Probe that executes callback on a OPTIONS
class OPTIONSHandler < GETHandler
METHOD = "OPTIONS"
end
# Probe that executes callback on a TRACE
class TRACEHandler < GETHandler
METHOD = "TRACE"
end
# Probe that executes callback on a PATCH
class PATCHHandler < GETHandler
METHOD = "PATCH"
end
end

View File

@ -23,6 +23,8 @@ module Hyde
@path = env["PATH_INFO"].dup @path = env["PATH_INFO"].dup
# Encapsulates all rack variables. Should not be public. # Encapsulates all rack variables. Should not be public.
@rack = init_rack_vars(env) @rack = init_rack_vars(env)
# Internal navigation states
@states = []
end end
# Returns request body (if POST data exists) # Returns request body (if POST data exists)
@ -30,6 +32,16 @@ module Hyde
@rack.input&.gets @rack.input&.gets
end end
# Push current navigation state (path, splat, param) onto state stack
def push_state
@states.push([@path, @param.dup, @splat.dup])
end
# Load last navigation state (path, splat, param) from state stack
def pop_state
@path, @param, @splat = @states.pop
end
attr_reader :request_method, :script_name, :path_info, :server_name, attr_reader :request_method, :script_name, :path_info, :server_name,
:server_port, :server_protocol, :headers, :param, :splat :server_port, :server_protocol, :headers, :param, :splat
attr_accessor :path attr_accessor :path

View File

@ -43,6 +43,33 @@ module Hyde
self self
end end
# Add a header to the headers hash
# @param key [String] header name
# @param value [String] header value
def add_header(key, value)
if @headers[key].is_a? String
@headers[key] = [@headers[key], value]
elsif @headers[key].is_a? Array
@headers[key].append(value)
else
@headers[key] = value
end
end
# Delete a header value from the headers hash
# If no value is provided, deletes all key entries
# @param key [String] header name
# @param value [String, nil] header value
def delete_header(key, value = nil)
if value and @response[key]
@response[key].delete(value)
else
@response.delete(key)
end
end
attr_accessor :status, :headers, :body
# Ensure response correctness # Ensure response correctness
# @param obj [String, Array, Hyde::Response] # @param obj [String, Array, Hyde::Response]
# @return Response # @return Response
@ -52,18 +79,16 @@ module Hyde
obj.validate obj.validate
when Array when Array
Response.new(obj).validate Response.new(obj).validate
when String when String, File, IO
Response.new([200, Response.new([200,
{ {
"content-type" => "text/html", "content-type" => "text/html",
"content-length" => obj.length "content-length" => obj.length
}, },
self.class.chunk_body(obj)]) chunk_body(obj)])
end end
end end
attr_accessor :status, :headers, :body
# Turn body into array of chunks # Turn body into array of chunks
def self.chunk_body(text) def self.chunk_body(text)
if text.is_a? String if text.is_a? String