header modifiers, status modifier, proper call rollbacks
This commit is contained in:
parent
a878ac58be
commit
fccc7ea9f0
15
config.ru
15
config.ru
|
@ -1,3 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/lib")
|
||||
require_relative 'lib/hyde'
|
||||
|
||||
app = Hyde::Server.new do
|
||||
|
@ -18,7 +21,17 @@ app = Hyde::Server.new do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ require_relative 'hyde/request'
|
|||
require_relative 'hyde/response'
|
||||
|
||||
# Hyde is a hideously simple ruby web framework
|
||||
#
|
||||
module Hyde
|
||||
# Hyde version
|
||||
# @type [String]
|
||||
|
|
|
@ -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.
|
||||
|
||||
- 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)
|
||||
|
|
|
@ -25,6 +25,52 @@ module Hyde
|
|||
def probe(path, &_setup)
|
||||
register(Hyde::Probe.new(path, parent: @origin))
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -15,12 +15,18 @@ module Hyde
|
|||
# @param [Hyde::Request]
|
||||
# @return [Boolean]
|
||||
def go(request)
|
||||
# rejected at pattern
|
||||
return reject(request) unless @pattern.match?(request.path)
|
||||
|
||||
request.push_state
|
||||
request.path, splat, param = @pattern.match(request.path)
|
||||
request.splat.append(*splat)
|
||||
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
|
||||
|
||||
# Method callback on failed request navigation
|
||||
|
|
|
@ -33,7 +33,7 @@ module Hyde
|
|||
|
||||
# Method callback on successful request navigation.
|
||||
# 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.
|
||||
def process(request)
|
||||
@children.each do |x|
|
||||
|
@ -41,6 +41,9 @@ module Hyde
|
|||
return value
|
||||
end
|
||||
end
|
||||
value = index(request)
|
||||
return value if value
|
||||
|
||||
_die(404)
|
||||
rescue StandardError => e
|
||||
_die(500, backtrace: [e.to_s] + e.backtrace)
|
||||
|
@ -50,6 +53,23 @@ module Hyde
|
|||
|
||||
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
|
||||
# @param errorcode [Integer]
|
||||
# @param backtrace [Array(String), nil]
|
||||
|
|
|
@ -4,6 +4,17 @@ require_relative 'node'
|
|||
require_relative 'util/lookup'
|
||||
|
||||
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.
|
||||
class Probe < Hyde::Node
|
||||
# @param path [Object]
|
||||
|
@ -16,6 +27,7 @@ module Hyde
|
|||
# Method callback on successful request navigation.
|
||||
# Throws an error upon reaching the path.
|
||||
# This behaviour should only be used internally.
|
||||
# @param request [Hyde::Request]
|
||||
# @return [Boolean] true if further navigation is possible
|
||||
# @raise [StandardError]
|
||||
def process(request)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -23,6 +23,8 @@ module Hyde
|
|||
@path = env["PATH_INFO"].dup
|
||||
# Encapsulates all rack variables. Should not be public.
|
||||
@rack = init_rack_vars(env)
|
||||
# Internal navigation states
|
||||
@states = []
|
||||
end
|
||||
|
||||
# Returns request body (if POST data exists)
|
||||
|
@ -30,6 +32,16 @@ module Hyde
|
|||
@rack.input&.gets
|
||||
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,
|
||||
:server_port, :server_protocol, :headers, :param, :splat
|
||||
attr_accessor :path
|
||||
|
|
|
@ -43,6 +43,33 @@ module Hyde
|
|||
self
|
||||
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
|
||||
# @param obj [String, Array, Hyde::Response]
|
||||
# @return Response
|
||||
|
@ -52,18 +79,16 @@ module Hyde
|
|||
obj.validate
|
||||
when Array
|
||||
Response.new(obj).validate
|
||||
when String
|
||||
when String, File, IO
|
||||
Response.new([200,
|
||||
{
|
||||
"content-type" => "text/html",
|
||||
"content-length" => obj.length
|
||||
},
|
||||
self.class.chunk_body(obj)])
|
||||
chunk_body(obj)])
|
||||
end
|
||||
end
|
||||
|
||||
attr_accessor :status, :headers, :body
|
||||
|
||||
# Turn body into array of chunks
|
||||
def self.chunk_body(text)
|
||||
if text.is_a? String
|
||||
|
|
Loading…
Reference in New Issue