Rewrite: Most of the structure is done, several pattern matching bugs fixed, config.ru is now working
This commit is contained in:
parent
9d483aa163
commit
a878ac58be
29
config.ru
29
config.ru
|
@ -1,12 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
require_relative 'lib/hyde'
|
||||
|
||||
require 'rack'
|
||||
app = Rack::Builder.new do |builder|
|
||||
builder.use Rack::Lint
|
||||
builder.run (proc do |env|
|
||||
pp env
|
||||
[200, {"content-type" => "text/html"}, ["p","i","s","s"]]
|
||||
end)
|
||||
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
|
||||
probe "probe"
|
||||
end
|
||||
end
|
||||
|
||||
run app
|
||||
|
|
423
hyde.old.rb
423
hyde.old.rb
|
@ -1,423 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'mime-types'
|
||||
require 'webrick'
|
||||
require 'uri'
|
||||
require 'pp'
|
||||
|
||||
# Primary module
|
||||
module Hyde
|
||||
# Hyde version
|
||||
# @type [String]
|
||||
VERSION = '0.5 (alpha)'
|
||||
attr_reader :VERSION
|
||||
|
||||
# Hyde branding and version (for error templates)
|
||||
# @type [String]
|
||||
VLINE = "Hyde/#{Hyde::VERSION} on WEBrick/#{WEBrick::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})\n"
|
||||
attr_reader :VLINE
|
||||
|
||||
# Generate HTML error template
|
||||
# @param errortext [String] Error explanation
|
||||
# @param backtrace [String] Ruby backtrace
|
||||
def error_template(errortext, backtrace)
|
||||
<<~HTMLEOF
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>#{WEBrick::HTMLUtils.escape(errortext)}</title>
|
||||
<style> .header {background-color: #CC7878; padding: 0.5rem; border-bottom-width: 0.2rem; border-bottom-style: solid; border-bottom-color: #202222; overflow: auto;} .title { font-weight: bolder; font-size: 36px; margin: 0 0; text-shadow: 1px 1px 1px #202222, 2px 2px 2px #404444; float: left } body { margin: 0;
|
||||
} .text { font-size 1rem; } .small { color: #7D7D7D; font-size: 12px;} .code { font-family: monospace; font-size: 0.7rem; } </style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<p class="title">HYDE</p>
|
||||
<p style="float: right"><a href="https://adastra7.net/git/yessiest/hyde">Source code</a></p>
|
||||
</div>
|
||||
<div style="padding: 0.5rem">
|
||||
<p class="text">#{WEBrick::HTMLUtils.escape(errortext)}</p>
|
||||
<pre><code class="text code">
|
||||
#{WEBrick::HTMLUtils.escape(backtrace) or "\n\n\n"}
|
||||
</code></pre>
|
||||
<hr/>
|
||||
<p class="small">#{WEBrick::HTMLUtils.escape(Hyde::VLINE)}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTMLEOF
|
||||
end
|
||||
module_function :error_template
|
||||
|
||||
WEBrick::HTTPResponse.class_exec do
|
||||
public
|
||||
|
||||
attr_accessor :recent_backtrace
|
||||
|
||||
def create_error_page
|
||||
@body = Hyde.error_template(@reason_phrase, @recent_backtrace)
|
||||
end
|
||||
end
|
||||
|
||||
# Interchangeable glob/regex/string pattern matching
|
||||
module PatternMatching
|
||||
def _prep_path(path, safe_regexp: true)
|
||||
@safe_regexp = safe_regexp
|
||||
@path = _normalize(path) if path.is_a? String
|
||||
@path = path if path.is_a? Regexp
|
||||
end
|
||||
|
||||
# @return [Boolean]
|
||||
def _match?(path, _ctx)
|
||||
# behvaiour used by "index" method
|
||||
return true if @path == ''
|
||||
split_path = path.split('/').filter { |x| x != '' }
|
||||
if @path.is_a? Regexp
|
||||
# this chunk of fuck is needed to force regexp into 3 rules:
|
||||
# 1) unsafe regexp means match the whole (remaining) line.
|
||||
# 3) safe regexp means match only the part before the next slash
|
||||
# 2) a ._match? only returns when there are no leftovers
|
||||
# this forces the matching to work somewhat consistently
|
||||
test = @path.match _normalize_input(path) unless @safe_regexp
|
||||
test = @path.match split_path[0] if @safe_regexp
|
||||
if test and (test.pre_match == '') and (test.post_match == '')
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
else
|
||||
# algorithm to match path segments until no more left in @path
|
||||
@path.split('/').filter { |x| x != '' }
|
||||
.each_with_index do |x, i|
|
||||
return false if x != split_path[i]
|
||||
end
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def _normalize_input(path)
|
||||
# remove duplicate slashes and trim edge slashes
|
||||
(path.split '/').filter { |x| x != '' }.join('/')
|
||||
end
|
||||
|
||||
def _normalize(path)
|
||||
# remove duplicate slashe s and trim edge slashes
|
||||
path = _normalize_input(path)
|
||||
# globbing behaviour simulated with regexp
|
||||
if path.match /(?<!\\)[\*\?\[]/
|
||||
path = Regexp.new(path
|
||||
.gsub(/[\^\$\.\|\+\(\)\{\}]/, '\\\\\\0')
|
||||
.gsub(/(?<!\\)\*\*/, '.*')
|
||||
.gsub(/(?<![.\\])\*/, '[^/]*')
|
||||
.gsub(/(?<!\\)\?/, '.')
|
||||
.gsub(/(?<!\\)\[\!/, '[^')
|
||||
)
|
||||
end
|
||||
path
|
||||
end
|
||||
end
|
||||
|
||||
# Methods to control requests, accessible from within blocks
|
||||
module PublicContextControlMethods
|
||||
def redirect(url)
|
||||
res = @current_context.response
|
||||
res.status = 301
|
||||
res.body = "<HTML><A HREF=\"#{url}\">#{url}</A>.</HTML>\n"
|
||||
res.header['location'] = URI(url).to_s
|
||||
throw :controlled_exit, @current_context
|
||||
end
|
||||
|
||||
def rewrite(url)
|
||||
new_context = Context.rewrite(@current_context, url)
|
||||
new_context.exit_loop = false
|
||||
throw :controlled_exit, new_context
|
||||
end
|
||||
|
||||
def die(code, message = nil, backtrace = '')
|
||||
@current_context.response.status = code
|
||||
@current_context.response.backtrace = backtrace
|
||||
message ||= WEBrick::HTTPStatus::StatusMessage[code]
|
||||
if @current_context.codehandlers[code]
|
||||
@current_context.codehandlers[code].call(@current_context, message, backtrace)
|
||||
else
|
||||
@current_context.response.body = Hyde.error_template(message, backtrace)
|
||||
end
|
||||
throw :controlled_exit, @current_context
|
||||
end
|
||||
end
|
||||
|
||||
# Request wrapper class
|
||||
class Context
|
||||
def initialize(path, request, response)
|
||||
@path = path
|
||||
@filepath = ''
|
||||
@request = request
|
||||
@response = response
|
||||
@indexlist = []
|
||||
@vars = {}
|
||||
@codehandlers = {}
|
||||
@queue_postprocess = []
|
||||
@queue_finalize = []
|
||||
@exit_loop = false
|
||||
end
|
||||
|
||||
def self.rewrite(pctx, newpath)
|
||||
newctx = Context.new(newpath, pctx.request, pctx.response)
|
||||
newctx.vars = pctx.vars
|
||||
newctx.queue_finalize = pctx.queue_finalize.clone
|
||||
newctx.queue_postprocess = pctx.queue_postprocess.clone
|
||||
newctx
|
||||
end
|
||||
|
||||
def enqueue_finalizer(dup: false, &block)
|
||||
return unless block_given?
|
||||
@queue_finalize.append(block) if dup or !@queue_finalize.include? block
|
||||
end
|
||||
|
||||
def enqueue_postprocessor(&block)
|
||||
@queue_postprocess.append(block) if block_given?
|
||||
end
|
||||
attr_reader :request
|
||||
attr_reader :response
|
||||
attr_accessor :queue_finalize
|
||||
attr_accessor :queue_postprocess
|
||||
attr_accessor :path
|
||||
attr_accessor :indexlist
|
||||
attr_accessor :filepath
|
||||
attr_accessor :exit_loop
|
||||
attr_accessor :vars
|
||||
attr_accessor :codehandlers
|
||||
end
|
||||
|
||||
# Context object with safe path encapsulation
|
||||
class ProtectedContext < Context
|
||||
def initialize(request)
|
||||
@path = request.path
|
||||
@filepath = request.filepath
|
||||
@request = request.request
|
||||
@response = request.response
|
||||
@indexlist = request.indexlist
|
||||
@exit_loop = request.exit_loop
|
||||
@vars = request.vars
|
||||
@codehandlers = request.codehandlers
|
||||
@queue_postprocess = request.queue_postprocess
|
||||
@queue_finalize = request.queue_finalize
|
||||
end
|
||||
undef :path=
|
||||
undef :filepath=
|
||||
undef :indexlist=
|
||||
undef :queue_postprocess=
|
||||
undef :queue_finalize=
|
||||
end
|
||||
|
||||
# Handler classes
|
||||
class Probe
|
||||
include Hyde::PatternMatching
|
||||
include Hyde::PublicContextControlMethods
|
||||
def initialize(path, safe_regexp: true, &block_optional)
|
||||
_prep_path path, safe_regexp: safe_regexp
|
||||
@block = block_optional
|
||||
end
|
||||
|
||||
def _match(request)
|
||||
return unless @block and (_match? request.path, request)
|
||||
@current_context = Hyde::ProtectedContext.new(request)
|
||||
return_later = instance_exec @current_context, &@block
|
||||
return_later
|
||||
end
|
||||
|
||||
# @sg-ignore
|
||||
def _match?(path, request)
|
||||
# End node - nothing must be after it
|
||||
return unless super(path, request)
|
||||
match_path = _normalize_input(path).match(@path)
|
||||
match_path.post_match == ''
|
||||
end
|
||||
end
|
||||
|
||||
class Serve < Hyde::Probe
|
||||
def _match(request)
|
||||
return super if @block
|
||||
return unless _match? request.path, request
|
||||
@current_context = request
|
||||
match_path = _normalize_input(request.path).match(@path)[0]
|
||||
filepath = request.filepath + match_path
|
||||
begin
|
||||
mimetype = MIME::Types.type_for(filepath)
|
||||
file = File.new filepath, 'r'
|
||||
data = file.read
|
||||
request.response.body = data
|
||||
request.response['Content-Type'] = mimetype
|
||||
rescue Errno::ENOENT
|
||||
die(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class GetMatch < Hyde::Probe
|
||||
@match_method = 'get'
|
||||
def initialize(*a, **b, &block)
|
||||
@match_method = (self.class.instance_variable_get :@match_method)
|
||||
raise Exception, 'block required!' unless block
|
||||
super(*a, **b, &block)
|
||||
end
|
||||
|
||||
def _match?(path, ctx)
|
||||
if ctx.request.request_method == @match_method.upcase
|
||||
super(path, ctx)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class PostMatch < GetMatch
|
||||
@match_method = 'post'
|
||||
end
|
||||
|
||||
class PutMatch < GetMatch
|
||||
@match_method = 'put'
|
||||
end
|
||||
|
||||
class PatchMatch < GetMatch
|
||||
@match_method = 'patch'
|
||||
end
|
||||
|
||||
class DeleteMatch < GetMatch
|
||||
@match_method = 'delete'
|
||||
end
|
||||
|
||||
class OptionsMatch < GetMatch
|
||||
@match_method = 'options'
|
||||
end
|
||||
|
||||
class LinkMatch < GetMatch
|
||||
@match_method = 'link'
|
||||
end
|
||||
|
||||
class UnlinkMatch < GetMatch
|
||||
@match_method = 'unlink'
|
||||
end
|
||||
|
||||
class PrintProbe < Hyde::Probe
|
||||
def _match(request)
|
||||
puts "#{request.path} matched!" if _match? request.path, request
|
||||
end
|
||||
end
|
||||
|
||||
# Handler invocation functions
|
||||
module Handlers
|
||||
{
|
||||
probe: Hyde::Probe,
|
||||
printProbe: Hyde::PrintProbe,
|
||||
serve: Hyde::Serve,
|
||||
post: Hyde::PostMatch,
|
||||
get: Hyde::GetMatch,
|
||||
put: Hyde::PutMatch,
|
||||
patch: Hyde::PatchMatch,
|
||||
delete: Hyde::DeleteMatch,
|
||||
options: Hyde::OptionsMatch,
|
||||
link: Hyde::LinkMatch,
|
||||
unlink: Hyde::UnlinkMatch
|
||||
}.each_pair do |name, newclass|
|
||||
define_method name do |path, *a, **b, &block|
|
||||
if path.is_a? Array
|
||||
path.each do |x|
|
||||
@chain.append newclass.new x, *a, **b, &block
|
||||
end
|
||||
else
|
||||
@chain.append newclass.new path, *a, **b, &block
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Pathspec
|
||||
include Hyde::PatternMatching
|
||||
include Hyde::Handlers
|
||||
include Hyde::PublicContextControlMethods
|
||||
def initialize(path, root_path: nil, safe_regexp: true, &block)
|
||||
_prep_path path, safe_regexp: safe_regexp
|
||||
@chain = []
|
||||
@root_override = root_path
|
||||
@remap = false
|
||||
instance_exec(&block)
|
||||
end
|
||||
|
||||
def path(path, *a, **b, &block)
|
||||
if path.is_a? Array
|
||||
path.each do |x|
|
||||
@chain.append Hyde::Pathspec.new x, *a, **b, &block
|
||||
end
|
||||
else
|
||||
@chain.append Hyde::Pathspec.new path, *a, **b, &block
|
||||
end
|
||||
end
|
||||
|
||||
def root(path)
|
||||
@root_override = '/' + _normalize_input(path)
|
||||
end
|
||||
|
||||
def remap(path)
|
||||
@root_override = '/' + _normalize_input(path)
|
||||
@remap = true
|
||||
end
|
||||
|
||||
def index(list)
|
||||
@indexlist = list if list.is_a? Array
|
||||
@indexlist = [list] if list.is_a? String
|
||||
end
|
||||
|
||||
def preprocess(&block)
|
||||
@preprocessor = block
|
||||
end
|
||||
|
||||
def postprocess(&block)
|
||||
@postprocessor = block
|
||||
end
|
||||
|
||||
def finalize(dup: false, &block)
|
||||
@finalizer = block
|
||||
@finalizer_dup = dup
|
||||
end
|
||||
|
||||
def _match(request)
|
||||
@current_context = request
|
||||
instance_exec request, &@preprocessor if @preprocessor
|
||||
request.enqueue_postprocessor(&@postprocessor) if @preprocessor
|
||||
request.enqueue_finalizer dup: @finalizer_dup, &@finalizer if @finalizer
|
||||
if _match? request.path, request
|
||||
match_path = _normalize_input(request.path).match(@path)
|
||||
next_path = match_path[0]
|
||||
request.path = cut_path = match_path.post_match
|
||||
# remap/root method handling
|
||||
if @root_override
|
||||
request.filepath = if @remap
|
||||
@root_override + '/'
|
||||
else @root_override + '/' + next_path + '/' end
|
||||
else
|
||||
request.filepath = request.filepath + next_path + '/'
|
||||
end
|
||||
# redefine indexing parameters if they are defined for a pathspec
|
||||
request.indexlist = @indexlist if @indexlist
|
||||
# do directory indexing
|
||||
if cut_path.match %r{^/?$}
|
||||
request.indexlist.each do |x|
|
||||
try_index = @chain.find { |y| y._match? x, request }
|
||||
if try_index
|
||||
request.path = x
|
||||
return try_index._match request
|
||||
end
|
||||
end
|
||||
end
|
||||
# passthrough to the next path object
|
||||
next_pathspec = @chain.find { |x| x._match? cut_path, request }
|
||||
next_pathspec._match request if next_pathspec
|
||||
unless next_pathspec
|
||||
# die and throw up if nowhere to go
|
||||
die(404)
|
||||
end
|
||||
end
|
||||
@current_context = nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'pattern_matching'
|
||||
require_relative 'hyde/server'
|
||||
require_relative 'hyde/path'
|
||||
require_relative 'hyde/probe'
|
||||
require_relative 'hyde/request'
|
||||
require_relative 'hyde/response'
|
||||
|
||||
# Hyde is a hideously simple ruby web framework
|
||||
#
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
# Internal structure of Hyde lib
|
||||
|
||||
Note: If you want to start hacking on Hyde and extending it, follow this
|
||||
layout as closely as possible.
|
||||
|
||||
## Core classes
|
||||
|
||||
These are core classes of Hyde and they are loaded as soon as the library is loaded.
|
||||
|
||||
- Hyde::Path [path.rb]
|
||||
- Hyde::PathBinding [path.rb]
|
||||
- Hyde::Probe [probe.rb]
|
||||
- Hyde::ProbeBinding [probe.rb]
|
||||
- Hyde::Node (parent of Path and Probe) [node.rb]
|
||||
- Hyde::Server (Rack application interface) [server.rb]
|
||||
- Hyde::ServerBinding [server.rb]
|
||||
- Hyde::Request (Rack request wrapper) [request.rb]
|
||||
- Hyde::Response (Rack response wrapper) [response.rb]
|
||||
- Hyde::Pattern [pattern\_matching.rb]
|
||||
|
||||
## Patterns
|
||||
|
||||
These are classes that Hyde::Pattern can interface with to create Patterns.
|
||||
|
||||
- Hyde::PatternMatching::ReMatch [pattern\_matching/rematch.rb]
|
||||
- Hyde::PatternMatching::Glob [pattern\_matching/glob.rb]
|
||||
|
||||
## DSL Method mixins
|
||||
|
||||
These are module mixins that add common methods to DSL bindings.
|
||||
|
||||
- Hyde::DSL::PathConstructors [dsl/path\_constructors.rb]
|
||||
|
||||
## Utilities
|
||||
|
||||
These are self-contained classes and methods that add extra functionality to Hyde.
|
||||
|
||||
- Hyde::Util::Lookup [util/lookup.rb]
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Hyde
|
||||
# Shared DSL methods
|
||||
module DSL
|
||||
# Path (and subclasses) DSL constructors
|
||||
module PathConstructors
|
||||
# Append a Node child object to the list of children
|
||||
def register(obj)
|
||||
unless obj.is_a? Hyde::Node
|
||||
raise StandardError, "register accepts node children only"
|
||||
end
|
||||
|
||||
@origin.children.append(obj)
|
||||
end
|
||||
|
||||
# Create a new {Hyde::Path} object
|
||||
def path(path, &setup)
|
||||
# i don't know WHAT is wrong with this thing. it just is wrong.
|
||||
# @sg-ignore
|
||||
register(Hyde::Path.new(path, parent: @origin, &setup))
|
||||
end
|
||||
|
||||
# Create a new {Hyde::Probe} object
|
||||
def probe(path, &_setup)
|
||||
register(Hyde::Probe.new(path, parent: @origin))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Hyde
|
||||
# Abstract class that reacts to request navigation.
|
||||
# Does nothing by default, behaviour should be overriden through
|
||||
# #reject and #process
|
||||
# @abstract
|
||||
class Node
|
||||
# @param path [Object]
|
||||
def initialize(path)
|
||||
@pattern = Pattern.new(path).freeze
|
||||
end
|
||||
|
||||
# Try to navigate the path. Run method callback in response.
|
||||
# @param [Hyde::Request]
|
||||
# @return [Boolean]
|
||||
def go(request)
|
||||
return reject(request) unless @pattern.match?(request.path)
|
||||
|
||||
request.path, splat, param = @pattern.match(request.path)
|
||||
request.splat.append(*splat)
|
||||
request.param.merge!(param)
|
||||
process(request)
|
||||
end
|
||||
|
||||
# Method callback on failed request navigation
|
||||
# @param _request [Hyde::Request]
|
||||
# @return false
|
||||
def reject(_request)
|
||||
false
|
||||
end
|
||||
|
||||
# Method callback on successful request navigation
|
||||
# @param _request [Hyde::Request]
|
||||
# @return true
|
||||
def process(_request)
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,27 +1,67 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'pattern_matching'
|
||||
require_relative 'node'
|
||||
require_relative 'dsl/path_constructors'
|
||||
require_relative 'util/lookup'
|
||||
|
||||
module Hyde
|
||||
# Primary building block of request navigation.
|
||||
class Path
|
||||
def initialize(path, &setup)
|
||||
@pattern = Pattern.new(path).freeze
|
||||
@children = []
|
||||
|
||||
binding = PathBinding.new(self)
|
||||
binding.instance_exec setup
|
||||
end
|
||||
end
|
||||
|
||||
# Protected interface that provides DSL context for setup block
|
||||
# Protected interface that provides DSL context for setup block.
|
||||
class PathBinding
|
||||
include Hyde::DSL::PathConstructors
|
||||
|
||||
def initialize(path)
|
||||
@origin = path
|
||||
end
|
||||
end
|
||||
|
||||
def params
|
||||
@origin.params.freeze
|
||||
# Primary building block of request navigation.
|
||||
class Path < Hyde::Node
|
||||
Binding = Hyde::PathBinding
|
||||
|
||||
# @param path [Object] Object to generate {Hyde::Pattern} from
|
||||
# @param parent [Hyde::Node] Parent object to inherit properties to
|
||||
# @param setup [#call] Setup block
|
||||
def initialize(path, parent:, &setup)
|
||||
super(path)
|
||||
@children = []
|
||||
@properties = Hyde::Util::Lookup.new(parent&.properties)
|
||||
|
||||
binding = Binding.new(self)
|
||||
binding.instance_exec(&setup)
|
||||
end
|
||||
|
||||
# Method callback on successful request navigation.
|
||||
# Finds the next appropriate path to go to.
|
||||
# @return [Boolean] true if further navigation is possible
|
||||
# @raise [UncaughtThrowError] by default throws :response if no matches found.
|
||||
def process(request)
|
||||
@children.each do |x|
|
||||
if (value = x.go(request))
|
||||
return value
|
||||
end
|
||||
end
|
||||
_die(404)
|
||||
rescue StandardError => e
|
||||
_die(500, backtrace: [e.to_s] + e.backtrace)
|
||||
end
|
||||
|
||||
attr_reader :children, :properties
|
||||
|
||||
private
|
||||
|
||||
# Handle an errorcode
|
||||
# @param errorcode [Integer]
|
||||
# @param backtrace [Array(String), nil]
|
||||
# @raise [UncaughtThrowError] throws :finish to stop processing
|
||||
def _die(errorcode, backtrace: nil)
|
||||
throw :finish, [errorcode].append(
|
||||
*(@properties["handle.#{errorcode}"] or
|
||||
@properties["handle.default"]).call(
|
||||
errorcode,
|
||||
backtrace: backtrace
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,6 @@ require_relative 'pattern_matching/glob'
|
|||
require_relative 'pattern_matching/rematch'
|
||||
|
||||
module Hyde
|
||||
|
||||
# Utility functions and pattern-generator classes.
|
||||
# Used primarily to create patterns for path definitions.
|
||||
module PatternMatching end
|
||||
|
@ -52,7 +51,7 @@ module Hyde
|
|||
# @return [Boolean]
|
||||
def match?(input)
|
||||
if @pattern.is_a? String
|
||||
Hyde::PatternMatching.canonicalize(input).start_with? pattern
|
||||
Hyde::PatternMatching.canonicalize(input).start_with? @pattern
|
||||
else
|
||||
@pattern.match?(input)
|
||||
end
|
||||
|
@ -66,7 +65,9 @@ module Hyde
|
|||
.filter { |x| classdomain.const_get(x).is_a? Class }
|
||||
.map { |x| classdomain.const_get(x) }
|
||||
.each do |pattern_generator|
|
||||
return pattern_generator.new(pattern) if pattern_generator.can_convert? pattern
|
||||
if pattern_generator.can_convert? pattern
|
||||
return pattern_generator.new(pattern)
|
||||
end
|
||||
end
|
||||
Hyde::PatternMatching.canonicalize(pattern)
|
||||
end
|
||||
|
|
|
@ -107,7 +107,7 @@ module Hyde
|
|||
\*\* | # Attached globstar
|
||||
\* | # Regular glob
|
||||
\[!?\w-\w\]\+ | # Character group
|
||||
(?<=\/):[\w_]+(?=\/) | # Named glob
|
||||
(?<=\/):[\w_]+(?=(?:\/|$)) | # Named glob
|
||||
\([\w\/|_-]+\) # Alternator group
|
||||
)
|
||||
/x.freeze
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'node'
|
||||
require_relative 'util/lookup'
|
||||
|
||||
module Hyde
|
||||
# Test probe. Also base for all "reactive" nodes.
|
||||
class Probe < Hyde::Node
|
||||
# @param path [Object]
|
||||
# @param parent [Hyde::Node]
|
||||
def initialize(path, parent:)
|
||||
super(path)
|
||||
@properties = Hyde::Util::Lookup.new(parent&.properties)
|
||||
end
|
||||
|
||||
# Method callback on successful request navigation.
|
||||
# Throws an error upon reaching the path.
|
||||
# This behaviour should only be used internally.
|
||||
# @return [Boolean] true if further navigation is possible
|
||||
# @raise [StandardError]
|
||||
def process(request)
|
||||
raise StandardError, <<~STREND
|
||||
probe reached #{request.splat.inspect}, #{request.param.inspect}
|
||||
STREND
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,7 +9,7 @@ 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.
|
||||
# Rack environment variable bindings. Should be public and readonly.
|
||||
@request_method = env["REQUEST_METHOD"]
|
||||
@script_name = env["SCRIPT_NAME"]
|
||||
@path_info = env["PATH_INFO"]
|
||||
|
@ -17,6 +17,10 @@ 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)
|
||||
end
|
||||
|
@ -26,7 +30,9 @@ module Hyde
|
|||
@rack.input&.gets
|
||||
end
|
||||
|
||||
attr_reader :request_method, :script_name, :path_info, :server_name, :server_port, :server_protocol, :headers
|
||||
attr_reader :request_method, :script_name, :path_info, :server_name,
|
||||
:server_port, :server_protocol, :headers, :param, :splat
|
||||
attr_accessor :path
|
||||
|
||||
private
|
||||
|
||||
|
@ -34,26 +40,34 @@ module Hyde
|
|||
# @param env [Hash]
|
||||
# @return Object
|
||||
def init_rack_vars(env)
|
||||
rack_vars = env.filter_map do |k|
|
||||
k.delete_prefix "rack." if k.start_with? "rack."
|
||||
end
|
||||
rack_vars["multipart"] = init_multipart_vars
|
||||
rack_keys = rack_vars.keys.map(&:to_sym)
|
||||
Struct.new(*rack_keys)
|
||||
.new(*rack_vars.values_at(rack_keys))
|
||||
rack_vars = env.filter_map do |k, v|
|
||||
[k.delete_prefix("rack."), v] if k.start_with? "rack."
|
||||
end.to_h
|
||||
return if rack_vars.empty?
|
||||
|
||||
rack_vars["multipart"] = init_multipart_vars(env)
|
||||
rack_keys = rack_vars.keys
|
||||
rack_keys_sym = rack_keys.map(&:to_sym)
|
||||
Struct.new(*rack_keys_sym)
|
||||
.new(*rack_vars.values_at(*rack_keys))
|
||||
.freeze
|
||||
end
|
||||
|
||||
# Initialize multipart parameters struct
|
||||
# @param env [Hash]
|
||||
# @return Object
|
||||
def init_multipart_vars
|
||||
multipart_vars = env.filter_map do |k|
|
||||
k.delete_prefix "rack.multipart." if k.start_with? "rack.multipart"
|
||||
def init_multipart_vars(env)
|
||||
multipart_vars = env.filter_map do |k, v|
|
||||
if k.start_with? "rack.multipart"
|
||||
[k.delete_prefix("rack.multipart."), v]
|
||||
end
|
||||
multipart_keys = multipart_vars.keys.map(&:to_sym)
|
||||
Struct.new(*multipart_keys)
|
||||
.new(*multipart_vars.values_at(multipart_keys))
|
||||
end.to_h
|
||||
return if multipart_vars.empty?
|
||||
|
||||
multipart_keys = multipart_vars.keys
|
||||
multipart_keys_sym = multipart_keys.map(&:to_sym)
|
||||
Struct.new(*multipart_keys_sym)
|
||||
.new(*multipart_vars.values_at(*multipart_keys))
|
||||
.freeze
|
||||
end
|
||||
|
||||
|
@ -69,19 +83,4 @@ module Hyde
|
|||
headers.freeze
|
||||
end
|
||||
end
|
||||
|
||||
# Rack protocol response wrapper.
|
||||
class Response
|
||||
def initialize
|
||||
@status = 404
|
||||
@headers = {}
|
||||
@body = []
|
||||
end
|
||||
|
||||
# Finish constructing Rack protocol response.
|
||||
# @return [Array(Integer,Hash,Array)]
|
||||
def finalize
|
||||
[@status, @headers, @body]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Hyde
|
||||
# Rack protocol response wrapper.
|
||||
class Response
|
||||
@chunk_size = 1024
|
||||
|
||||
self.class.attr_accessor :chunk_size
|
||||
|
||||
# @param response [Array(Integer, Hash, Array), nil]
|
||||
def initialize(response = nil)
|
||||
if response
|
||||
@status = response[0]
|
||||
@headers = response[1]
|
||||
@body = response[2]
|
||||
else
|
||||
@status = 404
|
||||
@headers = {}
|
||||
@body = []
|
||||
end
|
||||
end
|
||||
|
||||
# Return internal representation of Rack response
|
||||
# @return [Array(Integer,Hash,Array)]
|
||||
def finalize
|
||||
[@status, @headers, @body]
|
||||
end
|
||||
|
||||
# Make internal representation conformant
|
||||
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-type" => "text/html"
|
||||
}
|
||||
end
|
||||
@body = self.class.chunk_body(@body) if @body.is_a? String
|
||||
self
|
||||
end
|
||||
|
||||
# Ensure response correctness
|
||||
# @param obj [String, Array, Hyde::Response]
|
||||
# @return Response
|
||||
def self.convert(obj)
|
||||
case obj
|
||||
when Response
|
||||
obj.validate
|
||||
when Array
|
||||
Response.new(obj).validate
|
||||
when String
|
||||
Response.new([200,
|
||||
{
|
||||
"content-type" => "text/html",
|
||||
"content-length" => obj.length
|
||||
},
|
||||
self.class.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
|
||||
text.chars.each_slice(@chunk_size).map(&:join)
|
||||
elsif text.is_a? Array
|
||||
text
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'path'
|
||||
require_relative 'request'
|
||||
require_relative 'dsl/path_constructors'
|
||||
require_relative 'util/html'
|
||||
|
||||
module Hyde
|
||||
class ServerBinding < Hyde::PathBinding
|
||||
end
|
||||
|
||||
# A specialized path that can be used directly as a Rack application.
|
||||
class Server < Hyde::Path
|
||||
Binding = ServerBinding
|
||||
|
||||
def initialize(parent: nil, &setup)
|
||||
super("", parent: parent, &setup)
|
||||
return if parent
|
||||
|
||||
{
|
||||
"index" => [],
|
||||
"handle.default" => proc do |code, backtrace: nil|
|
||||
page = Hyde::Util.default_error_page(code, backtrace)
|
||||
headers = {
|
||||
"content-length": page.length,
|
||||
"content-type": "text/html"
|
||||
}
|
||||
[headers, page]
|
||||
end
|
||||
}.each do |k, v|
|
||||
@properties[k] = v
|
||||
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.
|
||||
def call(env)
|
||||
response = catch(:finish) do
|
||||
request = Hyde::Request.new(env)
|
||||
go(request)
|
||||
end
|
||||
Response.convert(response).finalize
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,107 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Hyde
|
||||
module Util
|
||||
# HTTP status codes and descriptions
|
||||
# Taken from WEBrick {https://github.com/ruby/webrick/blob/master/lib/webrick/httpstatus.rb}
|
||||
HTTP_STATUS = {
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
200 => 'OK',
|
||||
201 => 'Created',
|
||||
202 => 'Accepted',
|
||||
203 => 'Non-Authoritative Information',
|
||||
204 => 'No Content',
|
||||
205 => 'Reset Content',
|
||||
206 => 'Partial Content',
|
||||
207 => 'Multi-Status',
|
||||
300 => 'Multiple Choices',
|
||||
301 => 'Moved Permanently',
|
||||
302 => 'Found',
|
||||
303 => 'See Other',
|
||||
304 => 'Not Modified',
|
||||
305 => 'Use Proxy',
|
||||
307 => 'Temporary Redirect',
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
402 => 'Payment Required',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
406 => 'Not Acceptable',
|
||||
407 => 'Proxy Authentication Required',
|
||||
408 => 'Request Timeout',
|
||||
409 => 'Conflict',
|
||||
410 => 'Gone',
|
||||
411 => 'Length Required',
|
||||
412 => 'Precondition Failed',
|
||||
413 => 'Request Entity Too Large',
|
||||
414 => 'Request-URI Too Large',
|
||||
415 => 'Unsupported Media Type',
|
||||
416 => 'Request Range Not Satisfiable',
|
||||
417 => 'Expectation Failed',
|
||||
422 => 'Unprocessable Entity',
|
||||
423 => 'Locked',
|
||||
424 => 'Failed Dependency',
|
||||
426 => 'Upgrade Required',
|
||||
428 => 'Precondition Required',
|
||||
429 => 'Too Many Requests',
|
||||
431 => 'Request Header Fields Too Large',
|
||||
451 => 'Unavailable For Legal Reasons',
|
||||
500 => 'Internal Server Error',
|
||||
501 => 'Not Implemented',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
504 => 'Gateway Timeout',
|
||||
505 => 'HTTP Version Not Supported',
|
||||
507 => 'Insufficient Storage',
|
||||
511 => 'Network Authentication Required'
|
||||
}.freeze
|
||||
|
||||
# Return string with escaped HTML entities
|
||||
# @param str [String]
|
||||
# @return [String]
|
||||
def self.escape_html(str)
|
||||
str.gsub("&", "&")
|
||||
.gsub("<", "<")
|
||||
.gsub(">", ">")
|
||||
.gsub("\"", """)
|
||||
.gsub("'", "'")
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
|
||||
# Default error page for Hyde
|
||||
# @param code [Integer] HTTP Status code
|
||||
# @param backtrace [Array(String), nil] Message to show in backtrace window
|
||||
# @return [String]
|
||||
def self.default_error_page(code, backtrace)
|
||||
backtrace ||= []
|
||||
errortext = HTTP_STATUS[code]
|
||||
<<~HTMLEOF
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>#{Util.escape_html(errortext)}</title>
|
||||
<style> .header {padding: 0.5rem; overflow: auto;} .title { font-weight: bolder; font-size: 48px; margin: 10px 10px; text-shadow: 1px 1px 1px #202222, 2px 2px 2px #404444; float: left } body { margin: 0; } .text { font-size 1rem; } .small { color: #7D7D7D; font-size: 12px;} .code { font-family: monospace; font-size: 0.7rem; } </style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<p class="title">HYDE</p>
|
||||
<p style="float: right"><a href="https://adastra7.net/git/yessiest/hyde">Source code</a></p>
|
||||
</div>
|
||||
<div style="padding: 0.5rem">
|
||||
<p class="text">#{Util.escape_html(errortext)}</p>
|
||||
<pre><code class="text code">
|
||||
#{backtrace.map(&Util.method(:escape_html)).join('<br/>')}
|
||||
</code></pre>
|
||||
<hr/>
|
||||
<p class="small">#{Util.escape_html(Hyde::VLINE)}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTMLEOF
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
end
|
||||
end
|
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Hyde
|
||||
# Various things that exists for purely logical reasons
|
||||
module Util
|
||||
# Value container with recursive lookup
|
||||
class Lookup
|
||||
# @param parent [Lookup, nil]
|
||||
def initialize(parent = nil, hash = {})
|
||||
@parent = (parent or Hash.new(nil))
|
||||
@storage = hash
|
||||
end
|
||||
|
||||
# Initialize a Lookup from Hash
|
||||
# @param hash [Hash]
|
||||
def self.[](hash)
|
||||
Lookup.new(nil, Hash[hash])
|
||||
end
|
||||
|
||||
# Get a value by key
|
||||
# @param key [#hash] key for value
|
||||
# @return [Object,nil]
|
||||
def [](key)
|
||||
@storage[key] or @parent[key]
|
||||
end
|
||||
|
||||
# Set a value by key
|
||||
# @param key [#hash] key for value
|
||||
# @param value [Object] value value
|
||||
def []=(key, value)
|
||||
@storage[key] = value
|
||||
end
|
||||
|
||||
attr_accessor :parent
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../lib/hyde/util/lookup"
|
||||
require "test/unit"
|
||||
|
||||
class TestLookup < Test::Unit::TestCase
|
||||
include Hyde::Util
|
||||
def test_lookup
|
||||
assert_equal(true, Lookup.new().is_a?(Lookup))
|
||||
assert_equal(true, Lookup.new(Lookup.new()).is_a?(Lookup))
|
||||
assert_equal(true, Lookup[{"a" => :b}].is_a?(Lookup))
|
||||
testing_lookup = Lookup[{"a" => 1}]
|
||||
assert_equal(1, Lookup.new(testing_lookup)["a"])
|
||||
lookup2 = Lookup.new(testing_lookup)
|
||||
assert_equal(1, lookup2["a"])
|
||||
testing_lookup["a"] = 2
|
||||
assert_equal(2, lookup2["a"])
|
||||
lookup2["a"] = 3
|
||||
testing_lookup["a"] = 4
|
||||
assert_equal(3, lookup2["a"])
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue