Rewrite: Most of the structure is done, several pattern matching bugs fixed, config.ru is now working

This commit is contained in:
Yessiest 2023-09-04 00:13:30 +04:00
parent 9d483aa163
commit a878ac58be
16 changed files with 543 additions and 486 deletions

View File

@ -1,12 +1,25 @@
# frozen_string_literal: true require_relative 'lib/hyde'
require 'rack' app = Hyde::Server.new do
app = Rack::Builder.new do |builder| path /^test\/\w+/ do
builder.use Rack::Lint probe "probe"
builder.run (proc do |env| end
pp env
[200, {"content-type" => "text/html"}, ["p","i","s","s"]] path "/subdir/test" do
end) 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 end
run app run app

View File

@ -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

View File

@ -1,6 +1,10 @@
# frozen_string_literal: true # 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 # Hyde is a hideously simple ruby web framework
# #

38
lib/hyde/LAYOUT.md Normal file
View File

@ -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]

View File

@ -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

40
lib/hyde/node.rb Normal file
View File

@ -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

View File

@ -1,27 +1,67 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'pattern_matching' require_relative 'pattern_matching'
require_relative 'node'
require_relative 'dsl/path_constructors'
require_relative 'util/lookup'
module Hyde module Hyde
# Primary building block of request navigation. # Protected interface that provides DSL context for setup block.
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
class PathBinding class PathBinding
include Hyde::DSL::PathConstructors
def initialize(path) def initialize(path)
@origin = path @origin = path
end end
end
def params # Primary building block of request navigation.
@origin.params.freeze 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 end
end end

View File

@ -5,7 +5,6 @@ require_relative 'pattern_matching/glob'
require_relative 'pattern_matching/rematch' require_relative 'pattern_matching/rematch'
module Hyde module Hyde
# Utility functions and pattern-generator classes. # Utility functions and pattern-generator classes.
# Used primarily to create patterns for path definitions. # Used primarily to create patterns for path definitions.
module PatternMatching end module PatternMatching end
@ -52,7 +51,7 @@ module Hyde
# @return [Boolean] # @return [Boolean]
def match?(input) def match?(input)
if @pattern.is_a? String if @pattern.is_a? String
Hyde::PatternMatching.canonicalize(input).start_with? pattern Hyde::PatternMatching.canonicalize(input).start_with? @pattern
else else
@pattern.match?(input) @pattern.match?(input)
end end
@ -66,7 +65,9 @@ module Hyde
.filter { |x| classdomain.const_get(x).is_a? Class } .filter { |x| classdomain.const_get(x).is_a? Class }
.map { |x| classdomain.const_get(x) } .map { |x| classdomain.const_get(x) }
.each do |pattern_generator| .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 end
Hyde::PatternMatching.canonicalize(pattern) Hyde::PatternMatching.canonicalize(pattern)
end end

View File

@ -107,7 +107,7 @@ module Hyde
\*\* | # Attached globstar \*\* | # Attached globstar
\* | # Regular glob \* | # Regular glob
\[!?\w-\w\]\+ | # Character group \[!?\w-\w\]\+ | # Character group
(?<=\/):[\w_]+(?=\/) | # Named glob (?<=\/):[\w_]+(?=(?:\/|$)) | # Named glob
\([\w\/|_-]+\) # Alternator group \([\w\/|_-]+\) # Alternator group
) )
/x.freeze /x.freeze

27
lib/hyde/probe.rb Normal file
View File

@ -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

View File

@ -9,7 +9,7 @@ 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. # Rack environment variable bindings. Should be public and readonly.
@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,6 +17,10 @@ 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. # Encapsulates all rack variables. Should not be public.
@rack = init_rack_vars(env) @rack = init_rack_vars(env)
end end
@ -26,7 +30,9 @@ module Hyde
@rack.input&.gets @rack.input&.gets
end 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 private
@ -34,26 +40,34 @@ module Hyde
# @param env [Hash] # @param env [Hash]
# @return Object # @return Object
def init_rack_vars(env) def init_rack_vars(env)
rack_vars = env.filter_map do |k| rack_vars = env.filter_map do |k, v|
k.delete_prefix "rack." if k.start_with? "rack." [k.delete_prefix("rack."), v] if k.start_with? "rack."
end end.to_h
rack_vars["multipart"] = init_multipart_vars return if rack_vars.empty?
rack_keys = rack_vars.keys.map(&:to_sym)
Struct.new(*rack_keys) rack_vars["multipart"] = init_multipart_vars(env)
.new(*rack_vars.values_at(rack_keys)) 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 .freeze
end end
# Initialize multipart parameters struct # Initialize multipart parameters struct
# @param env [Hash] # @param env [Hash]
# @return Object # @return Object
def init_multipart_vars def init_multipart_vars(env)
multipart_vars = env.filter_map do |k| multipart_vars = env.filter_map do |k, v|
k.delete_prefix "rack.multipart." if k.start_with? "rack.multipart" if k.start_with? "rack.multipart"
[k.delete_prefix("rack.multipart."), v]
end end
multipart_keys = multipart_vars.keys.map(&:to_sym) end.to_h
Struct.new(*multipart_keys) return if multipart_vars.empty?
.new(*multipart_vars.values_at(multipart_keys))
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 .freeze
end end
@ -69,19 +83,4 @@ module Hyde
headers.freeze headers.freeze
end end
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 end

76
lib/hyde/response.rb Normal file
View File

@ -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

46
lib/hyde/server.rb Normal file
View File

@ -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

107
lib/hyde/util/html.rb Normal file
View File

@ -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("&", "&amp;")
.gsub("<", "&lt;")
.gsub(">", "&gt;")
.gsub("\"", "&quot;")
.gsub("'", "&#39;")
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

37
lib/hyde/util/lookup.rb Normal file
View File

@ -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

22
test/Hyde_Util_Lookup.rb Normal file
View File

@ -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