File serving, root/remap directives, filters, preprocessors, postprocessors
This commit is contained in:
parent
fccc7ea9f0
commit
bbfb6f00d9
|
@ -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.
|
||||
|
38
config.ru
38
config.ru
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<body>
|
||||
<p> index page </p>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,3 @@
|
|||
.a {
|
||||
color: #FF00FF;
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
../lib
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
response = catch(:finish) do
|
||||
request = Hyde::Request.new(env)
|
||||
response = catch(:finish) do
|
||||
go(request)
|
||||
end
|
||||
request.run_postprocessors(response)
|
||||
Response.convert(response).finalize
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue