lib/angelo/base.rb in angelo-0.3.3 vs lib/angelo/base.rb in angelo-0.4.0

- old
+ new

@@ -1,26 +1,34 @@ module Angelo class Base + extend Forwardable include ParamsParser include Celluloid::Logger + include Tilt::ERB + include Mustermann - extend Forwardable - def_delegators :@responder, :content_type, :headers, :redirect, :request, :transfer_encoding + def_delegators :@responder, :content_type, :headers, :mustermann, :redirect, :request, :transfer_encoding def_delegators :@klass, :public_dir, :report_errors?, :sse_event, :sse_message, :sses, :websockets attr_accessor :responder class << self attr_accessor :app_file, :server def inherited subclass - # set app_file from caller stack + # Set app_file by groveling up the caller stack until we find + # the first caller from a directory different from __FILE__. + # This allows base.rb to be required from an arbitrarily deep + # nesting of require "angelo/<whatever>" and still set + # app_file correctly. # - subclass.app_file = caller(1).map {|l| l.split(/:(?=|in )/, 3)[0,1]}.flatten[0] + subclass.app_file = caller_locations.map(&:absolute_path).find do |f| + !f.start_with?(File.dirname(__FILE__) + File::SEPARATOR) + end # bring RequestError into this namespace # subclass.class_eval 'class RequestError < Angelo::RequestError; end' @@ -28,20 +36,31 @@ subclass.port DEFAULT_PORT subclass.ping_time DEFAULT_PING_TIME subclass.log_level DEFAULT_LOG_LEVEL + # Parse command line options if angelo/main has been required. + # They could also be parsed in run, but this makes them + # available to and overridable by the DSL. + # + subclass.parse_options(ARGV.dup) if @angelo_main + class << subclass def root @root ||= File.expand_path '..', app_file end end end + end + # Methods defined in module DSL will be available in the DSL both + # in Angelo::Base subclasses and in the top level DSL. + + module DSL def addr a = nil @addr = a if a @addr end @@ -74,78 +93,82 @@ def report_errors! @report_errors = true end - def report_errors? - !!@report_errors + HTTPABLE.each do |m| + define_method m do |path, opts = {}, &block| + path = ::Mustermann.new path, opts + routes[m][path] = Responder.new &block + end end - def compile! name, &block - define_method name, &block - method = instance_method name - remove_method name - method + def websocket path, &block + path = ::Mustermann.new path + routes[:websocket][path] = Responder::Websocket.new &block end - def routes - @routes ||= Hash.new{|h,k| h[k] = {}} + def eventsource path, headers = nil, &block + path = ::Mustermann.new path + routes[:get][path] = Responder::Eventsource.new headers, &block end - def filters - @filters ||= {before: {default: []}, after: {default: []}} + def task name, &block + Angelo::Server.define_task name, &block end - def filter which, opts = {}, &block - f = compile! :filter, &block - case opts - when String - filter_by which, opts, f - when Hash - if opts[:path] - filter_by which, opts[:path], f - else - filters[which][:default] << f - end - end - end - - def filter_by which, path, meth - filters[which][path] ||= [] - filters[which][path] << meth - end - def before opts = {}, &block filter :before, opts, &block end def after opts = {}, &block filter :after, opts, &block end - HTTPABLE.each do |m| - define_method m do |path, &block| - routes[m][path] = Responder.new &block - end + def on_pong &block + Responder::Websocket.on_pong = block end - def websocket path, &block - routes[:websocket][path] = Responder::Websocket.new &block + end + + # Make the DSL methods available to subclass-level code. + # main.rb makes them available to the top level. + + extend DSL + + class << self + def report_errors? + !!@report_errors end - def eventsource path, headers = nil, &block - routes[:get][path] = Responder::Eventsource.new headers, &block + def routes + @routes ||= Hash.new{|h,k| h[k] = RouteMap.new} end - def on_pong &block - Responder::Websocket.on_pong = block + def filters + @filters ||= {before: {default: []}, after: {default: []}} end - def task name, &block - Angelo::Server.define_task name, &block + def filter which, opts = {}, &block + case opts + when String + filter_by which, opts, block + when Hash + if opts[:path] + filter_by which, opts[:path], block + else + filters[which][:default] << block + end + end end + def filter_by which, path, block + pattern = ::Mustermann.new path + filters[which][pattern] ||= [] + filters[which][pattern] << block + end + def websockets reject = true @websockets ||= Stash::Websocket.new server @websockets.reject! &:closed? if reject @websockets end @@ -213,12 +236,12 @@ def params @params ||= case request.method when GET, DELETE, OPTIONS parse_query_string when POST, PUT - parse_post_body - end + parse_query_string_and_post_body + end.merge mustermann.params(request.path) end def request_headers @request_headers ||= Hash.new do |hash, key| if Symbol === key @@ -317,18 +340,77 @@ def sleep time Celluloid.sleep time end - def filter which - fs = self.class.filters[which][:default] - fs += self.class.filters[which][request.path] if self.class.filters[which][request.path] - fs.each {|f| f.bind(self).call} - end - def chunked_response &block transfer_encoding :chunked ChunkedResponse.new &block + end + + def filter which + self.class.filters[which].each do |pattern, filters| + case pattern + when :default + filters.each {|filter| instance_eval &filter} + when ::Mustermann + if pattern.match request.path + @pre_filter_params = params + @params = @pre_filter_params.merge pattern.params(request.path) + filters.each {|filter| instance_eval &filter} + @params = @pre_filter_params + end + end + end + end + + # It seems more sensible to put this in main.rb since it's used + # only if angelo/main is required, but it's here so it can be + # tested, since requiring angelo/main doesn't play well with the + # test code. + + def self.parse_options(argv) + require "optparse" + + optparse = OptionParser.new do |op| + op.banner = "Usage: #{$0} [options]" + + op.on('-p port', OptionParser::DecimalInteger, "set the port (default is #{port})") {|val| port val} + op.on('-o addr', "set the host (default is #{addr})") {|val| addr val} + op.on('-h', '--help', "Show this help") do + puts op + exit + end + end + + begin + optparse.parse(argv) + rescue OptionParser::ParseError => ex + $stderr.puts ex + $stderr.puts optparse + exit 1 + end + end + + class RouteMap + + def initialize + @hash = Hash.new + end + + def []= route, responder + @hash[route] = responder + end + + def [] route + responder = nil + if mustermann = @hash.keys.select {|k| k.match(route)}.first + responder = @hash.fetch mustermann + responder.mustermann = mustermann + end + responder + end + end class EventSource extend Forwardable