File serving, root/remap directives, filters, preprocessors, postprocessors

This commit is contained in:
Yessiest 2023-09-08 00:44:39 +04:00
parent fccc7ea9f0
commit bbfb6f00d9
17 changed files with 320 additions and 81 deletions

30
HACKING.md Normal file
View File

@ -0,0 +1,30 @@
# Notes and things to consider on Hyde hacking
The structure of the Hyde rewrite was specifically redesigned to allow for
extensive modification and extension. So to keep things that way, you may
want to consider the methodology of writing Hyde code.
## Recommendations
To keep things beautiful, consider following recommendations:
- **USE COMMON SENSE**. None of these guidelines will ever be adequate
enough to replace common sense.
- **Code less, think with ~~portals~~ what you have**. To minimize code
overhead, try to use existing functionality to get the effect you want.
(i.e. if you want to append headers to a request when it traverses a path,
don't write a new class variable and handler for this - just create a new
DSL method that really just appends a preprocessor to the path. Or avoid
making something like this at all - after all, preprocessors exist
exactly for that reason.)
- Preferably, **extend the DSL and not the class*. Extensive class
modifications make code a lot less maintainable, if it wasn't obvious
already. If it can't be helped, then at the very least use Rubocop.
- Document classes as if the next maintainer after you has you at gunpoint.
Document thoroughly, use YARD tags and **never** skip on public method
docs and class docs. As an example, consider Hyde::PatternMatching::Glob.
- Unit tests suck for many reasons. However, if you're writing a class that
does not have any dependents and which is frequently used, consider making
a unit test for it. People that might have to fix things further along
will be very thankful.

View File

@ -1,38 +0,0 @@
# frozen_string_literal: true
$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/lib")
require_relative 'lib/hyde'
app = Hyde::Server.new do
path /^test\/\w+/ do
probe "probe"
end
path "/subdir/test" do
probe "probe"
end
path "/match/*/test/:test/" do
probe "probe"
end
path "/match/:test/" do
probe "probe"
end
path "/match2/*/" do
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
run app

View File

@ -0,0 +1,6 @@
<!DOCTYPE HTML>
<html>
<body>
<p> index page </p>
</body>
</html>

3
examples/assets/pee.css Normal file
View File

@ -0,0 +1,3 @@
.a {
color: #FF00FF;
}

18
examples/config.ru Normal file
View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/lib")
require_relative 'lib/hyde'
app = Hyde::Server.new do
preprocess do |request|
puts "New request: #{request}"
end
filter do
rand < 0.5
end
index ["index"]
root "#{ENV['PWD']}/assets"
serve "*.(html|css|js)"
end
run app

1
examples/lib Symbolic link
View File

@ -0,0 +1 @@
../lib

View File

@ -71,6 +71,10 @@ module Hyde
register(Hyde::OPTIONSHandler.new(path, parent: @origin, &setup))
end
# Create a new {Hyde::GETHandler} that serves static files
def serve(path)
register(Hyde::ServeHandler.new(path, parent: @origin))
end
end
end
end

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
module Hyde
# Shared DSL methods
module DSL
# Common path methods
module PathMethods
# Set path index
# @param index [Array,String]
def index(index)
case index
when Array
@origin.properties['index'] = index
when String
@origin.properties['index'] = [index]
else
raise StandardError, "index should be an Array or a String"
end
end
# Set root path (appends matched part of the path).
# @param path [String
def root(path)
raise StandardError, "path should be a String" unless path.is_a? String
@origin.root = path
end
# Set root path (without appending matched part).
# @param path [String
def remap(path)
root(path)
@origin.remap = true
end
# Add a preprocessor to the path.
# Does not modify path execution.
# @param block [#call]
# @yieldparam request [Hyde::Request]
def preprocess(&block)
@origin.preprocess(&block)
block
end
# Add a postprocessor to the path.
# @param block [#call]
# @yieldparam request [Hyde::Request]
# @yieldparam response [Hyde::Response]
def postprocess(&block)
@origin.postprocess(&block)
block
end
# Add a filter to the path.
# Blocks path access if a filter returns false.
# @param block [#call]
# @yieldparam request [Hyde::Request]
def filter(&block)
@origin.filter(&block)
block
end
end
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require_relative 'pattern_matching/util'
module Hyde
# Abstract class that reacts to request navigation.
# Does nothing by default, behaviour should be overriden through
@ -9,6 +11,8 @@ module Hyde
# @param path [Object]
def initialize(path)
@pattern = Pattern.new(path).freeze
@root = nil
@remap = false
end
# Try to navigate the path. Run method callback in response.
@ -19,7 +23,9 @@ module Hyde
return reject(request) unless @pattern.match?(request.path)
request.push_state
request.path, splat, param = @pattern.match(request.path)
path, splat, param = @pattern.match(request.path)
do_filepath(request, request.path.delete_suffix(path))
request.path = path
request.splat.append(*splat)
request.param.merge!(param)
value = process(request)
@ -42,5 +48,19 @@ module Hyde
def process(_request)
true
end
attr_accessor :remap, :root
private
# Process filepath for request
def do_filepath(request, path)
if @root
request.filepath = "#{@root}/#{@remap ? '' : path}/"
else
request.filepath += "/#{path}/"
end
request.filepath.gsub!(/\/+/, "/")
end
end
end

View File

@ -3,12 +3,14 @@
require_relative 'pattern_matching'
require_relative 'node'
require_relative 'dsl/path_constructors'
require_relative 'dsl/path_methods'
require_relative 'util/lookup'
module Hyde
# Protected interface that provides DSL context for setup block.
class PathBinding
include Hyde::DSL::PathConstructors
include Hyde::DSL::PathMethods
def initialize(path)
@origin = path
@ -24,8 +26,14 @@ module Hyde
# @param setup [#call] Setup block
def initialize(path, parent:, &setup)
super(path)
# Child nodes array
@children = []
# Inherited properties array
@properties = Hyde::Util::Lookup.new(parent&.properties)
# Arrays of preprocessors, postprocessors and filters
@preprocessors = []
@postprocessors = []
@filters = []
binding = Binding.new(self)
binding.instance_exec(&setup)
@ -36,6 +44,10 @@ module Hyde
# @return [Boolean] true if further navigation will be done
# @raise [UncaughtThrowError] by default throws :response if no matches found.
def process(request)
return false unless run_filters(request)
run_preprocessors(request)
enqueue_postprocessors(request)
@children.each do |x|
if (value = x.go(request))
return value
@ -49,10 +61,58 @@ module Hyde
_die(500, backtrace: [e.to_s] + e.backtrace)
end
# Add a preprocessor to the path.
# Does not modify path execution.
# @param block [#call]
# @yieldparam request [Hyde::Request]
def preprocess(&block)
@preprocessors.append(block)
end
# Add a postprocessor to the path.
# @param block [#call]
# @yieldparam request [Hyde::Request]
# @yieldparam response [Hyde::Response]
def postprocess(&block)
@postprocessors.append(block)
end
# Add a filter to the path.
# Blocks path access if a filter returns false.
# @param block [#call]
# @yieldparam request [Hyde::Request]
def filter(&block)
@filters.append(block)
end
attr_reader :children, :properties
private
# Sequentially run through all filters and drop request if one is false
# @param request [Hyde::Request]
# @return [Boolean] true if request passed all filters
def run_filters(request)
@filters.each do |filter|
return false if filter.call(request).is_a? FalseClass
end
true
end
# Sequentially run all preprocessors on a request
# @param request [Hyde::Request]
def run_preprocessors(request)
@preprocessors.each do |preproc|
preproc.call(request)
end
end
# Append postprocessors to request
# @param request [Hyde::Request]
def enqueue_postprocessors(request)
request.postprocessors.append(*@postprocessors)
end
# Try to perform indexing on the path if possible
# @param request [Hyde::Request]
# @return [Boolean] true if indexing succeeded

View File

@ -2,6 +2,7 @@
require_relative 'node'
require_relative 'util/lookup'
require 'pp'
module Hyde
autoload :Handler, "hyde/probe/handler"
@ -14,7 +15,7 @@ module Hyde
autoload :OPTIONSHandler, "hyde/probe/http_method"
autoload :TRACEHandler, "hyde/probe/http_method"
autoload :PATCHHandler, "hyde/probe/http_method"
autoload :ServeHandler, "hyde/probe/serve_handler"
# Test probe. Also base for all "reactive" nodes.
class Probe < Hyde::Node
# @param path [Object]
@ -31,8 +32,11 @@ module Hyde
# @return [Boolean] true if further navigation is possible
# @raise [StandardError]
def process(request)
return reject(request) unless request.path.match?(/^\/?$/)
raise StandardError, <<~STREND
probe reached #{request.splat.inspect}, #{request.param.inspect}
#{request.pretty_inspect}
STREND
end
end

View File

@ -33,6 +33,8 @@ module Hyde
# @return [Boolean] true if further navigation is possible
# @raise [UncaughtThrowError] may raise if die() is called.
def process(request)
return reject(request) unless request.path.match?(/^\/?$/)
@request = request
response = catch(:break) do
@binding.instance_exec(*request.splat,

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require_relative '../probe'
require_relative 'binding'
module Hyde
# Probe that sends files from a location
class ServeHandler < Hyde::Probe
# @param path [Object]
# @param parent [Hyde::Node]
# @param exec [#call]
def initialize(path, parent:)
super(path, parent: parent)
end
attr_accessor :response
# Method callback on successful request navigation.
# Tries to serve files matched by handler
# @param request [Hyde::Request]
# @return [Boolean] true if file was found
def process(request)
File.open(request.filepath.delete_suffix("/"))
rescue StandardError
false
end
end
end

View File

@ -9,7 +9,57 @@ module Hyde
def initialize(env)
# Should not be used under regular circumstances or depended upon.
@_original_env = env
# Rack environment variable bindings. Should be public and readonly.
# Rack environment variable bindings. Should be public and frozen.
init_request_params(env)
# Pattern matching parameters. Public, readable, unfrozen.
@param = {}
@splat = []
# Traversal route. Public and writable.
@path = env["PATH_INFO"].dup
# File serving path. Public and writable.
@filepath = "/"
# Encapsulates all rack variables. Should not be public.
@rack = init_rack_vars(env)
# Internal navigation states. Private.
@states = []
# Postprocessors for current request
@postprocessors = []
end
# Run postprocessors
# @param response [Hyde::Response]
def run_postprocessors(response)
@postprocessors.each do |postproc|
postproc.call(self, response)
end
end
# Returns request body (if POST data exists)
# @return [nil, String]
def body
@rack.input&.gets
end
# Push current navigation state (path, splat, param) onto state stack
def push_state
@states.push([@path, @param.dup, @splat.dup, @filepath.dup])
end
# Load last navigation state (path, splat, param) from state stack
def pop_state
@path, @param, @splat, @filepath = @states.pop
end
attr_reader :request_method, :script_name, :path_info, :server_name,
:server_port, :server_protocol, :headers, :param, :splat,
:postprocessors
attr_accessor :path, :filepath
private
# Initialize basic rack request parameters
# @param env [Hash]
def init_request_params(env)
@request_method = env["REQUEST_METHOD"]
@script_name = env["SCRIPT_NAME"]
@path_info = env["PATH_INFO"]
@ -17,37 +67,8 @@ module Hyde
@server_port = env["SERVER_PORT"]
@server_protocol = env["SERVER_PROTOCOL"]
@headers = init_headers(env)
@param = {}
@splat = []
# Traversal route. Public, writable and readable.
@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)
def body
@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
private
# Initialize rack parameters struct
# @param env [Hash]
# @return Object

View File

@ -27,15 +27,15 @@ module Hyde
end
# Make internal representation conformant
# @return [Hyde::Response]
def validate
if [204, 304].include?(@status) or (100..199).include?(@status)
@headers.delete "content-length"
@headers.delete "content-type"
@body = []
elsif @headers.empty?
length = @body.is_a?(String) ? @body.length : @body.join.length
@headers = {
"content-length" => length,
"content-length" => content_size,
"content-type" => "text/html"
}
end
@ -82,19 +82,30 @@ module Hyde
when String, File, IO
Response.new([200,
{
"content-type" => "text/html",
"content-length" => obj.length
"content-type" => "text/html"
},
chunk_body(obj)])
obj]).validate
else
Response.new([404, {}, []])
end
end
# Turn body into array of chunks
# @param text [String]
# @return [Array(String)]
def self.chunk_body(text)
if text.is_a? String
text.chars.each_slice(@chunk_size).map(&:join)
elsif text.is_a? Array
text
text.chars.each_slice(@chunk_size).map(&:join)
end
private
# Try to figure out content length
# @return [Integer, nil]
def content_size
case @body
when String then @body.length
when Array then @body.join.length
when File then @body.size
end
end
end

View File

@ -13,6 +13,8 @@ module Hyde
class Server < Hyde::Path
Binding = ServerBinding
# @param parent [Hyde::Node, nil] Parent object to inherit properties to
# @param setup [#call] Setup block
def initialize(parent: nil, &setup)
super("", parent: parent, &setup)
return if parent
@ -28,18 +30,21 @@ module Hyde
[headers, page]
end
}.each do |k, v|
@properties[k] = v
@properties[k] = v unless @properties[k]
end
end
# Rack ingress point.
# This should not be called under any circumstances twice in the same application,
# although server nesting for the purpose of creating virtual hosts is allowed.
# @param env [Hash]
# @return [Array(Integer,Hash,Array)]
def call(env)
request = Hyde::Request.new(env)
response = catch(:finish) do
request = Hyde::Request.new(env)
go(request)
end
request.run_postprocessors(response)
Response.convert(response).finalize
end
end