module Nyara class Route REQUIRED_ATTRS = [:http_method, :scope, :prefix, :suffix, :controller, :id, :conv] attr_reader *REQUIRED_ATTRS attr_writer :http_method, :id # NOTE `id` is stored in symbol for C-side conenience, but returns as string for Ruby-side goodness def id @id.to_s end # optional attr_accessor :accept_exts, :accept_mimes, :classes # @private attr_accessor :path, :blk def initialize &p instance_eval &p if p end # http_method in string form def http_method_to_s m, _ = HTTP_METHODS.find{|k,v| v == http_method} m end # nil for get / post def http_method_override m = http_method_to_s if m != 'GET' and m != 'POST' m end end # enum all combinations of matching selectors def selectors if classes [id, *classes, *classes.map{|k| "#{k}:#{http_method_to_s}"}, ":#{http_method_to_s}"] else [id, ":#{http_method_to_s}"] end end # find blocks in filters that match selectors def matched_lifecycle_callbacks filters actions = [] selectors = selectors() if selectors and filters # iterate with filter's order to preserve define order filters.each do |sel, blks| actions.concat blks if selectors.include?(sel) end end actions end def path_template File.join @scope, (@path.gsub '%z', '%s') end # Compute prefix, suffix, conv
# NOTE routes may be inherited, so late-setting controller is necessary def compile controller, scope @controller = controller @scope = scope path = scope.sub /\/?$/, @path if path.empty? path = '/' end @prefix, suffix = analyse_path path @suffix, @conv = compile_re suffix end # Compute accept_exts, accept_mimes def set_accept_exts a @accept_exts = {} @accept_mimes = [] if a a.each do |e| e = e.to_s.dup.freeze @accept_exts[e] = true if MIME_TYPES[e] v1, v2 = MIME_TYPES[e].split('/') raise "bad mime type: #{MIME_TYPES[e].inspect}" if v1.nil? or v2.nil? @accept_mimes << [v1, v2, e] end end end @accept_mimes = nil if @accept_mimes.empty? @accept_exts = nil if @accept_exts.empty? end def validate REQUIRED_ATTRS.each do |attr| unless instance_variable_get("@#{attr}") raise ArgumentError, "missing #{attr}" end end raise ArgumentError, "id must be symbol" unless @id.is_a?(Symbol) end # --- # private # +++ TOKEN = /%(?:[sz]|(?>\.\d+)?[dfux])/ FORWARD_SPLIT = /(?=#{TOKEN})/ # #### Returns # # [str_re, conv] # def compile_re suffix return ['', []] unless suffix conv = [] segs = suffix.split(FORWARD_SPLIT).flat_map do |s| if (s =~ TOKEN) == 0 part1 = s[TOKEN] [part1, s.slice(part1.size..-1)] else s end end re_segs = segs.map do |s| case s when /\A%(?>\.\d+)?([dfux])\z/ case $1 when 'd' conv << :to_i '(-?\d+)' when 'f' conv << :to_f # just copied from scanf '([-+]?(?:0[xX](?:\.\h+|\h+(?:\.\h*)?)[pP][-+]\d+|\d+(?![\d.])|\d*\.\d*(?:[eE][-+]?\d+)?))' when 'u' conv << :to_i '(\d+)' when 'x' conv << :hex '(\h+)' end when '%s' conv << :to_s '([^/]+)' when '%z' conv << :to_s '(.*)' else Regexp.quote s end end ["^#{re_segs.join}$", conv] end # Split the path into 2 parts:
# a fixed prefix and a variable suffix def analyse_path path raise 'path must contain no new line' if path.index "\n" raise 'path must start with /' unless path.start_with? '/' path.split(FORWARD_SPLIT, 2) end end # class methods class << Route def routes @routes || [] end # #### Param # # * `controller` - string or class which inherits [Nyara::Controller](Controller.html) # # NOTE controller may be not defined when register_controller is called def register_controller scope, controller unless scope.is_a?(String) raise ArgumentError, "route prefix should be a string" end scope = scope.dup.freeze (@controllers ||= []) << [scope, controller] end def compile @global_path_templates = {} # "name#id" => path mapped_controllers = {} @routes = @controllers.flat_map do |scope, c| if c.is_a?(String) c = name2const c end name = c.controller_name || const2name(c) raise "#{c.inspect} is not a Nyara::Controller" unless Controller > c if mapped_controllers[c] raise "controller #{c.inspect} was already mapped" end mapped_controllers[c] = true c.nyara_compile_routes(scope).each do |e| @global_path_templates[name + e.id] = e.path_template end end @routes.sort_by! &:prefix @routes.reverse! mapped_controllers.each do |c, _| c.path_templates = @global_path_templates.merge c.path_templates end Ext.clear_route @routes.each do |e| Ext.register_route e end end def global_path_template id @global_path_templates[id] end # remove `.klass` and `:method` from selector, and validate selector format def canonicalize_callback_selector selector /\A (?\#\w++(?:\-\w++)*)? (?\.\w++(?:\-\w++)*)? (?:\w+)? \z/x =~ selector unless id or klass or method raise ArgumentError, "bad selector: #{selector.inspect}", caller[1..-1] end id.presence or selector.sub(/:\w+\z/, &:upcase) end def clear # gc mark fail if wrong order? Ext.clear_route @controllers = [] end def print_routes puts "All routes:" Nyara::Route.routes.each do |route| cname = const2name route.controller print "#{cname}#{route.id}".rjust(30), " " print route.http_method_to_s.ljust(6), " " print route.path_template puts end end # private def const2name c name = c.to_s.sub /Controller$/, '' name.gsub!(/(?