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)) register(Hyde::OPTIONSHandler.new(path, parent: @origin, &setup))
end end
# Create a new {Hyde::GETHandler} that serves static files
def serve(path)
register(Hyde::ServeHandler.new(path, parent: @origin))
end
end end
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 # frozen_string_literal: true
require_relative 'pattern_matching/util'
module Hyde module Hyde
# Abstract class that reacts to request navigation. # Abstract class that reacts to request navigation.
# Does nothing by default, behaviour should be overriden through # Does nothing by default, behaviour should be overriden through
@ -9,6 +11,8 @@ module Hyde
# @param path [Object] # @param path [Object]
def initialize(path) def initialize(path)
@pattern = Pattern.new(path).freeze @pattern = Pattern.new(path).freeze
@root = nil
@remap = false
end end
# Try to navigate the path. Run method callback in response. # Try to navigate the path. Run method callback in response.
@ -19,7 +23,9 @@ module Hyde
return reject(request) unless @pattern.match?(request.path) return reject(request) unless @pattern.match?(request.path)
request.push_state 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.splat.append(*splat)
request.param.merge!(param) request.param.merge!(param)
value = process(request) value = process(request)
@ -42,5 +48,19 @@ module Hyde
def process(_request) def process(_request)
true true
end 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
end end

View File

@ -3,12 +3,14 @@
require_relative 'pattern_matching' require_relative 'pattern_matching'
require_relative 'node' require_relative 'node'
require_relative 'dsl/path_constructors' require_relative 'dsl/path_constructors'
require_relative 'dsl/path_methods'
require_relative 'util/lookup' require_relative 'util/lookup'
module Hyde module Hyde
# Protected interface that provides DSL context for setup block. # Protected interface that provides DSL context for setup block.
class PathBinding class PathBinding
include Hyde::DSL::PathConstructors include Hyde::DSL::PathConstructors
include Hyde::DSL::PathMethods
def initialize(path) def initialize(path)
@origin = path @origin = path
@ -24,8 +26,14 @@ module Hyde
# @param setup [#call] Setup block # @param setup [#call] Setup block
def initialize(path, parent:, &setup) def initialize(path, parent:, &setup)
super(path) super(path)
# Child nodes array
@children = [] @children = []
# Inherited properties array
@properties = Hyde::Util::Lookup.new(parent&.properties) @properties = Hyde::Util::Lookup.new(parent&.properties)
# Arrays of preprocessors, postprocessors and filters
@preprocessors = []
@postprocessors = []
@filters = []
binding = Binding.new(self) binding = Binding.new(self)
binding.instance_exec(&setup) binding.instance_exec(&setup)
@ -36,6 +44,10 @@ module Hyde
# @return [Boolean] true if further navigation will be done # @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)
return false unless run_filters(request)
run_preprocessors(request)
enqueue_postprocessors(request)
@children.each do |x| @children.each do |x|
if (value = x.go(request)) if (value = x.go(request))
return value return value
@ -49,10 +61,58 @@ module Hyde
_die(500, backtrace: [e.to_s] + e.backtrace) _die(500, backtrace: [e.to_s] + e.backtrace)
end 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 attr_reader :children, :properties
private 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 # Try to perform indexing on the path if possible
# @param request [Hyde::Request] # @param request [Hyde::Request]
# @return [Boolean] true if indexing succeeded # @return [Boolean] true if indexing succeeded

View File

@ -2,6 +2,7 @@
require_relative 'node' require_relative 'node'
require_relative 'util/lookup' require_relative 'util/lookup'
require 'pp'
module Hyde module Hyde
autoload :Handler, "hyde/probe/handler" autoload :Handler, "hyde/probe/handler"
@ -14,7 +15,7 @@ module Hyde
autoload :OPTIONSHandler, "hyde/probe/http_method" autoload :OPTIONSHandler, "hyde/probe/http_method"
autoload :TRACEHandler, "hyde/probe/http_method" autoload :TRACEHandler, "hyde/probe/http_method"
autoload :PATCHHandler, "hyde/probe/http_method" autoload :PATCHHandler, "hyde/probe/http_method"
autoload :ServeHandler, "hyde/probe/serve_handler"
# 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]
@ -31,8 +32,11 @@ module Hyde
# @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)
return reject(request) unless request.path.match?(/^\/?$/)
raise StandardError, <<~STREND raise StandardError, <<~STREND
probe reached #{request.splat.inspect}, #{request.param.inspect} probe reached #{request.splat.inspect}, #{request.param.inspect}
#{request.pretty_inspect}
STREND STREND
end end
end end

View File

@ -33,6 +33,8 @@ module Hyde
# @return [Boolean] true if further navigation is possible # @return [Boolean] true if further navigation is possible
# @raise [UncaughtThrowError] may raise if die() is called. # @raise [UncaughtThrowError] may raise if die() is called.
def process(request) def process(request)
return reject(request) unless request.path.match?(/^\/?$/)
@request = request @request = request
response = catch(:break) do response = catch(:break) do
@binding.instance_exec(*request.splat, @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) def initialize(env)
# Should not be used under regular circumstances or depended upon. # Should not be used under regular circumstances or depended upon.
@_original_env = env @_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"] @request_method = env["REQUEST_METHOD"]
@script_name = env["SCRIPT_NAME"] @script_name = env["SCRIPT_NAME"]
@path_info = env["PATH_INFO"] @path_info = env["PATH_INFO"]
@ -17,37 +67,8 @@ module Hyde
@server_port = env["SERVER_PORT"] @server_port = env["SERVER_PORT"]
@server_protocol = env["SERVER_PROTOCOL"] @server_protocol = env["SERVER_PROTOCOL"]
@headers = init_headers(env) @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 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 # Initialize rack parameters struct
# @param env [Hash] # @param env [Hash]
# @return Object # @return Object

View File

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

View File

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