From a878ac58be086203643f653e571a9bcd3b1c077d Mon Sep 17 00:00:00 2001 From: Yessiest Date: Mon, 4 Sep 2023 00:13:30 +0400 Subject: [PATCH] Rewrite: Most of the structure is done, several pattern matching bugs fixed, config.ru is now working --- config.ru | 29 +- hyde.old.rb | 423 ------------------------------ lib/hyde.rb | 6 +- lib/hyde/LAYOUT.md | 38 +++ lib/hyde/dsl/path_constructors.rb | 30 +++ lib/hyde/node.rb | 40 +++ lib/hyde/path.rb | 68 ++++- lib/hyde/pattern_matching.rb | 7 +- lib/hyde/pattern_matching/glob.rb | 12 +- lib/hyde/probe.rb | 27 ++ lib/hyde/request.rb | 61 +++-- lib/hyde/response.rb | 76 ++++++ lib/hyde/server.rb | 46 ++++ lib/hyde/util/html.rb | 107 ++++++++ lib/hyde/util/lookup.rb | 37 +++ test/Hyde_Util_Lookup.rb | 22 ++ 16 files changed, 543 insertions(+), 486 deletions(-) delete mode 100644 hyde.old.rb create mode 100644 lib/hyde/LAYOUT.md create mode 100644 lib/hyde/dsl/path_constructors.rb create mode 100644 lib/hyde/node.rb create mode 100644 lib/hyde/probe.rb create mode 100644 lib/hyde/response.rb create mode 100644 lib/hyde/server.rb create mode 100644 lib/hyde/util/html.rb create mode 100644 lib/hyde/util/lookup.rb create mode 100644 test/Hyde_Util_Lookup.rb diff --git a/config.ru b/config.ru index fab56e2..b1bb75d 100644 --- a/config.ru +++ b/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 diff --git a/hyde.old.rb b/hyde.old.rb deleted file mode 100644 index 78421fb..0000000 --- a/hyde.old.rb +++ /dev/null @@ -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 - - - - #{WEBrick::HTMLUtils.escape(errortext)} - - - -
-

HYDE

-

Source code

-
-
-

#{WEBrick::HTMLUtils.escape(errortext)}

-

-                  #{WEBrick::HTMLUtils.escape(backtrace) or "\n\n\n"}
-                  
-
-

#{WEBrick::HTMLUtils.escape(Hyde::VLINE)}

-
- - - 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 /(?#{url}.\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 diff --git a/lib/hyde.rb b/lib/hyde.rb index 9a2ddb2..dc7cbe7 100644 --- a/lib/hyde.rb +++ b/lib/hyde.rb @@ -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 # diff --git a/lib/hyde/LAYOUT.md b/lib/hyde/LAYOUT.md new file mode 100644 index 0000000..4f43ff7 --- /dev/null +++ b/lib/hyde/LAYOUT.md @@ -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] diff --git a/lib/hyde/dsl/path_constructors.rb b/lib/hyde/dsl/path_constructors.rb new file mode 100644 index 0000000..42f784d --- /dev/null +++ b/lib/hyde/dsl/path_constructors.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 diff --git a/lib/hyde/node.rb b/lib/hyde/node.rb new file mode 100644 index 0000000..aaa795a --- /dev/null +++ b/lib/hyde/node.rb @@ -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 diff --git a/lib/hyde/path.rb b/lib/hyde/path.rb index f3a932c..d1964e0 100644 --- a/lib/hyde/path.rb +++ b/lib/hyde/path.rb @@ -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 diff --git a/lib/hyde/pattern_matching.rb b/lib/hyde/pattern_matching.rb index e932f81..7530d29 100644 --- a/lib/hyde/pattern_matching.rb +++ b/lib/hyde/pattern_matching.rb @@ -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 diff --git a/lib/hyde/pattern_matching/glob.rb b/lib/hyde/pattern_matching/glob.rb index dc23da7..af9fe1b 100644 --- a/lib/hyde/pattern_matching/glob.rb +++ b/lib/hyde/pattern_matching/glob.rb @@ -103,12 +103,12 @@ module Hyde # Regexp pattern to match glob tokens TOKENS = / ( # Glob-specific tokens - \/\*\*\/ | # Freestanding globstar - \*\* | # Attached globstar - \* | # Regular glob - \[!?\w-\w\]\+ | # Character group - (?<=\/):[\w_]+(?=\/) | # Named glob - \([\w\/|_-]+\) # Alternator group + \/\*\*\/ | # Freestanding globstar + \*\* | # Attached globstar + \* | # Regular glob + \[!?\w-\w\]\+ | # Character group + (?<=\/):[\w_]+(?=(?:\/|$)) | # Named glob + \([\w\/|_-]+\) # Alternator group ) /x.freeze diff --git a/lib/hyde/probe.rb b/lib/hyde/probe.rb new file mode 100644 index 0000000..ea69935 --- /dev/null +++ b/lib/hyde/probe.rb @@ -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 diff --git a/lib/hyde/request.rb b/lib/hyde/request.rb index fd8ea51..e2b71b8 100644 --- a/lib/hyde/request.rb +++ b/lib/hyde/request.rb @@ -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" - end - multipart_keys = multipart_vars.keys.map(&:to_sym) - Struct.new(*multipart_keys) - .new(*multipart_vars.values_at(multipart_keys)) + 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 + 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 diff --git a/lib/hyde/response.rb b/lib/hyde/response.rb new file mode 100644 index 0000000..c04435c --- /dev/null +++ b/lib/hyde/response.rb @@ -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 diff --git a/lib/hyde/server.rb b/lib/hyde/server.rb new file mode 100644 index 0000000..deb90c7 --- /dev/null +++ b/lib/hyde/server.rb @@ -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 diff --git a/lib/hyde/util/html.rb b/lib/hyde/util/html.rb new file mode 100644 index 0000000..ed797f2 --- /dev/null +++ b/lib/hyde/util/html.rb @@ -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 + + + + #{Util.escape_html(errortext)} + + + +
+

HYDE

+

Source code

+
+
+

#{Util.escape_html(errortext)}

+

+        #{backtrace.map(&Util.method(:escape_html)).join('
')} +
+
+

#{Util.escape_html(Hyde::VLINE)}

+
+ + + HTMLEOF + end + # rubocop:enable Metrics/MethodLength + end +end diff --git a/lib/hyde/util/lookup.rb b/lib/hyde/util/lookup.rb new file mode 100644 index 0000000..431ad76 --- /dev/null +++ b/lib/hyde/util/lookup.rb @@ -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 diff --git a/test/Hyde_Util_Lookup.rb b/test/Hyde_Util_Lookup.rb new file mode 100644 index 0000000..d85ca01 --- /dev/null +++ b/test/Hyde_Util_Lookup.rb @@ -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