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 = 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
|
||||||
|
|
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
|
# 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
|
||||||
#
|
#
|
||||||
|
|
|
@ -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
|
# 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
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
|
||||||
|
|
|
@ -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