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)