From ae667b4222a718f99091ec8bb1fb130970b051e7 Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Sun, 15 May 2016 17:07:16 -0400 Subject: initial commit --- src/guff.cr | 319 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/guff/cli.cr | 2 + 2 files changed, 321 insertions(+) create mode 100644 src/guff.cr create mode 100644 src/guff/cli.cr (limited to 'src') diff --git a/src/guff.cr b/src/guff.cr new file mode 100644 index 0000000..944612a --- /dev/null +++ b/src/guff.cr @@ -0,0 +1,319 @@ +require "option_parser" +require "http/server" +require "./guff/*" + +module Guff + class Config + property :mode, :env, :host, :port, :data_dir, :assets_dir + + DEFAULTS = { + mode: "help", + env: "production", + host: "127.0.0.1", + port: "8989", + data_dir: "./data", + assets_dir: "/usr/local/share/guff", + } + + def initialize + @mode = DEFAULTS[:mode] as String + @env = (ENV["GUFF_ENVIRONMENT"]? || DEFAULTS[:env]) as String + @host = (ENV["GUFF_HOST"]? || DEFAULTS[:host]) as String + @port = (ENV["GUFF_PORT"]? || DEFAULTS[:port]) as String + @data_dir = (ENV["GUFF_DATA_DIR"]? || DEFAULTS[:data_dir]) as String + @assets_dir = (ENV["GUFF_ASSETS_DIR"]? || DEFAULTS[:assets_dir]) as String + end + + VALID_MODES = %w{init run help} + + def mode=(mode : String) + raise "unknown mode: \"#{mode}\"" unless VALID_MODES.includes?(mode) + @mode = mode + end + + VALID_ENVS = %w{development production} + + def env=(env : String) + raise "unknown environment: \"#{env}\"" unless VALID_ENVS.includes?(env) + @env = env + end + + def port=(port : String) + val = port.to_i + raise "invalid port: #{port}" unless val > 0 && val < 65535 + @port = port + end + + def assets_dir=(dir : String) + raise "missing assets dir: \"#{dir}\"" unless Dir.exists?(dir) + @assets_dir = dir + end + + def self.parse( + app : String, + args : Array(String) + ) : Config + r = Config.new + + raise "missing mode" unless args.size > 0 + + # get mode + r.mode = case mode = args.shift + when "-h", "--help" + "help" + else + mode + end + + # parse arguments + p = OptionParser.parse(args) do |p| + p.banner = "Usage: #{app} [mode] " + + p.separator + p.separator("Run Options:") + + p.on( + "-H HOST", "--host HOST", + "TCP host (defaults to \"#{DEFAULTS[:host]}\")" + ) do |arg| + r.host = arg + end + + p.on( + "-p PORT", "--port PORT", + "TCP port (defaults to \"#{DEFAULTS[:port]}\")" + ) do |arg| + r.port = arg + end + + p.separator + p.separator("Directory Options:") + + p.on( + "-D DIR", "--data-dir DIR", + "Data directory (defaults to \"#{DEFAULTS[:data_dir]}\")" + ) do |arg| + r.data_dir = arg + end + + p.on( + "-A DIR", "--assets-dir DIR", + "Guff assets directory (defaults to \"#{DEFAULTS[:assets_dir]}\")" + ) do |arg| + r.assets_dir = arg + end + + p.separator + p.separator("Development Options:") + + p.on( + "-E ENV", "--environment ENV", + "Environment (defaults to \"#{DEFAULTS[:env]})\"" + ) do |arg| + r.env = arg + end + + p.separator + p.separator("Other Options:") + + p.on("-h", "--help", "Print usage.") do + r.mode = "help" + end + end + + case r.mode + when "init" + # shortcut for -D parameter + r.data_dir = args.shift if args.size > 0 + when "help" + # print help + puts p + end + + # return config + r + end + end + + abstract class ModeRunner + def self.run(config : Config) + new(config).run + end + + def initialize(@config : Config) + end + + abstract def run + end + + class Builder < ModeRunner + def run + STDERR.puts "TODO: building directory" + end + end + + class Context + property :config #, :models + + def initialize(@config : Config) + # @models = nil + end + end + + module Handlers + abstract class Handler < HTTP::Handler + def initialize(@context : Context) + super() + end + end + + abstract class AuthenticatedHandler < Handler + def initialize(context : Context, roles : Array(String)) + super(context) + @roles = roles + end + + def call(context : HTTP::Server::Context) + role = context.request.headers["x-guff-role"]? + + if role && @roles.includes?(role) + authenticated_call(context) + else + call_next(context) + end + end + + abstract def authenticated_call(context : HTTP::Server::Context) + end + + class SessionHandler < Guff::Handlers::Handler + def call(context : HTTP::Server::Context) + if @context.config.env == "development" + # TODO: add handler support + context.request.headers["x-guff-user-id"] = "0" + context.request.headers["x-guff-role"] = "admin" + end + + call_next(context) + end + end + + class AssetsHandler < AuthenticatedHandler + PATH_RE = %r{^/_guff/assets/} + + def authenticated_call(context : HTTP::Server::Context) + path = context.request.path.not_nil! + + if path.match(PATH_RE) + # TODO + else + call_next(context) + end + end + end + + HANDLERS = [{ + dev: true, + id: :error, + }, { + dev: false, + id: :log, + }, { + dev: false, + id: :deflate, + }, { + dev: false, + id: :session, + }, { + dev: false, + id: :assets, + }] + + def self.get(context : Context) : Array(HTTP::Handler) + is_dev = (context.config.env == "development") + + HANDLERS.select { |row| + !(row[:dev] as Bool) || is_dev + }.map { |row| + make_handler(row[:id] as Symbol, context) + } + end + + def self.make_handler( + handler_id : Symbol, + context : Context + ) : HTTP::Handler + case handler_id + when :error + HTTP::ErrorHandler.new + when :log + HTTP::LogHandler.new + when :deflate + HTTP::DeflateHandler.new + when :session + SessionHandler.new(context) + when :assets + AssetsHandler.new(context, %w{admin editor}) + else + raise "unknown handler id: #{handler_id}" + end + end + end + + class Server < ModeRunner + def run + STDERR.puts "TODO: running web server" + check_dirs + + # create context + context = Context.new(@config) + + STDERR.puts "listening on %s:%s" % [@config.host, @config.port] + + # run server + HTTP::Server.new( + @config.host, + @config.port.to_i, + Handlers.get(context) + ).listen + end + + private def check_dirs + raise "missing assets directory: \"#{@config.assets_dir}\"" unless Dir.exists?(@config.assets_dir) + raise "missing data directory: \"#{@config.data_dir}\"" unless Dir.exists?(@config.data_dir) + end + end + + module CLI + def self.print_usage(app : String) + STDERR.puts "Usage: #{app} [mode] " + end + + def self.run(app : String, args : Array(String)) + begin + begin + config = Config.parse(app, args) + rescue err + raise "#{err}. Use --help for usage" + end + + case config.mode + when "init" + Builder.run(config) + when "run" + Server.run(config) + when "help" + # do nothing + else + # never reached + raise "unknown mode: #{config.mode}" + end + rescue err + STDERR.puts "ERROR: #{err}." + exit -1 + end + end + end +end + +Guff::CLI.run($0, ARGV) diff --git a/src/guff/cli.cr b/src/guff/cli.cr new file mode 100644 index 0000000..0bb3d4b --- /dev/null +++ b/src/guff/cli.cr @@ -0,0 +1,2 @@ +module Guff::CLI +end -- cgit v1.2.3