sourceafBedSheet::Route.fan


** Matches uri paths to request handler methods, converting any remaining path segments into method 
** arguments. Use '*' to capture (non-greedy) method arguments, '**' to capture all 
** remaining path segments and '***' to capture the remaining url Examples:
** 
** pre>
**   glob pattern     uri             arguments
**   -----------------------------------------------------
**   /user/*      --> /user/       => ""
**   /user/*      --> /user/42     => "42"
**   /user/*      --> /user/42/dee => no match
**
**   /user/*/*    --> /user/       => no match
**   /user/*/*    --> /user/42     => no match
**   /user/*/*    --> /user/42/dee => "42", "dee"
** 
**   /user/**     --> /user/       => ""
**   /user/**     --> /user/42     => "42"
**   /user/**     --> /user/42/dee => "42", "dee"
**
**   /user/***    --> /user/       => ""
**   /user/***    --> /user/42     => "42"
**   /user/***    --> /user/42/dee => "42/dee"
** <pre
** 
** Method arguments with default values are mapped to optional path segments. Use '**' to capture 
** optional segments in the uri. Example:
** 
** pre>
** using afBedSheet
** using afIoc
** 
** class AppModule {
**   @Contribute
**   static Void contributeRoutes(OrderedConfig conf) {
**     conf.add(Route(`/hello/**`, HelloPage#hello))
**   }
** }
** 
** class HelloPage {
**   Text hello(Str name, Int iq := 666) {
**     return Text.fromPlain("Hello! I'm $name and I have an IQ of $iq!")
**   }
** }
** 
** '/hello/Traci/69' => helloPage.hello("Traci", 69) => "Hello! I'm Traci and I have an IQ of 69"
** 'hello/Luci'      => helloPage.hello("Luci")      => "Hello! I'm Luci and I have an IQ of 666"
** 'dude/'           => no match
** 'hello/'          => no match
** 'hello/1/2/3      => no match
** <pre
** 
** Path segments are converted to Objs via the [ValueEncoder]`ValueEncoder` service.
**  
** > TIP: Contribute 'ValueEncoders' to convert path segments into Entities. BedSheet can then call 
** handlers with real Entities, not just str IDs!
** 
** Parameters of type 'Str[]' are *capture all* parameters and match the remaining uri (split on '/').
**
** Request uri's (for matching purposes) are treated as case-insensitive. In the example above, both
** 
**  - 'hello/Luci' and 
**  - 'HELLO/Luci' 
**
** would be matched.
** 
** Use '?' to optional match the last character. Use to optionally match a trailing slash. e.g.
** 
** pre>
**   glob         uri
**   -----------------------------
**   /index/? --> /index  => match
**   /index/? --> /index/ => match
**   vs
**   /index/  --> /index  => no match
**   /index   --> /index/ => no match
** <pre
** 
** If a handler class is a service, it is obtained from the IoC registry, otherwise it is
** [autobuilt]`afIoc::Registry.autobuild`. If the class is 'const', the instance is cached for 
** future use.
** 
** Note: There is no special handling for nullable types in handler method arguments as BedSheet 
** can not determine when 'null' is a suitable replacement for an empty str. Instead contribute a
** `ValueEncoder` or use default values.
** 
const class Route {
    private static const Str star   := "(.*?)"
    ** The uri regex this route matches.
    const Regex routeRegex

    ** Method handler for this route. 
    const Method handler

    ** HTTP method used for this route
    const Str httpMethod

    private const Regex[]   httpMethodGlob
    private const Bool      matchAllArgs
    private const Bool      matchToEnd
    private const Bool      isGlob

    ** Make a Route that matches on the given glob pattern.
    ** 
    ** 'glob' must start with a slash "/"
    ** 
    ** 'httpMethod' may be a glob. Example, use "*" to match all methods.
    new makeFromGlob(Uri glob, Method handler, Str httpMethod := "GET") {
        if (glob.scheme != null || glob.host != null || glob.port!= null )
            throw BedSheetErr(BsMsgs.routeShouldBePathOnly(glob))
        if (!glob.isPathAbs)
            throw BedSheetErr(BsMsgs.routeShouldStartWithSlash(glob))

        uriGlob := glob.toStr
        regex   := "(?i)^"
        uriGlob.each |c, i| {
            if (c.isAlphaNum || c == '?')
                regex += c.toChar
            else if (c == '*')  
                regex += star
            else 
                regex += ("\\" + c.toChar)
        }
        
        matchRemaining  := false
        matchToEnd      := false
        if (uriGlob.endsWith("***")) {
            regex = regex[0..<-star.size*3] + "(.*)"
            matchToEnd  = true
            
        } else if (uriGlob.endsWith("**")) {
            regex = regex[0..<-star.size*2] + "(.*?)\\/?"
            matchRemaining = true
        }
        
        regex += "\$"
        
        this.routeRegex     = Regex.fromStr(regex)
        this.handler        = handler
        this.httpMethod     = httpMethod
        // split on both space and ','
        this.httpMethodGlob = httpMethod.split.map { it.split(',') }.flatten.map { Regex.glob(it) }
        this.matchToEnd     = matchToEnd
        this.matchAllArgs   = matchRemaining
        this.isGlob         = true
    }

    ** For hardcore users; make a Route from a regex. Capture groups are used to match arguments.
    ** Example:
    ** 
    **   Route(Regex<|(?i)^\/index\/(.*?)$|>, #foo, "GET", true) -> Route(`/index/**`)
    ** 
    ** Set 'matchRemaining' to 'true' to have the last capture group mimic the glob '**' operator, 
    ** splitting on "/" to match all remaining segments.  
    new makeFromRegex(Regex uriRegex, Method handler, Str httpMethod := "GET", Bool matchRemaining := false) {
        this.routeRegex     = uriRegex
        this.handler        = handler
        this.httpMethod     = httpMethod
        // split on both space and ','
        this.httpMethodGlob = httpMethod.split.map { it.split(',') }.flatten.map { Regex.glob(it) }
        this.matchAllArgs   = matchRemaining
        this.isGlob         = false
    }

    internal Str[]? match(Uri uri, Str httpMethod) {
        if (!httpMethodGlob.any { it.matches(httpMethod) })
            return null

        segs := matchUri(uri)
        if (segs == null)
            return null
        
        args := matchArgs(segs)
        if (args == null)
            return null
        
        return args
    }

    ** Returns null if the given uri does not match the uri regex
    internal Str[]? matchUri(Uri uri) {
        matcher := routeRegex.matcher(uri.pathOnly.toStr)
        find := matcher.find 
        if (!find)
            return null
        
        groups := Str[,]
        
        // use find as supplied Regex may not have ^...$
        while (find) {
            groupCunt := matcher.groupCount
            if (groupCunt == 0)
                return Str#.emptyList
            
            (1..groupCunt).each |i| {
                g := matcher.group(i)
                groups.add(g)
            }
        
            find = matcher.find
        }

        if (matchAllArgs && !groups.isEmpty) {
            last := groups.removeAt(-1)
            groups.addAll(last.split('/'))
        }
        
        if (isGlob && !matchToEnd && !matchAllArgs && groups[-1].contains("/"))
            return null
                        
        return groups
    }
    
    ** Returns null if uriSegments do not match (optional) method handler arguments
    internal Str[]? matchArgs(Str[] uriSegments) {
        if (handler.params.size == uriSegments.size)
            return uriSegments
        
        paramRange  := (handler.params.findAll { !it.hasDefault }.size..<handler.params.size)
        if (paramRange.contains(uriSegments.size))
            return uriSegments

        return null
    }

    override Str toStr() {
        "Route:$routeRegex - $httpMethod -> $handler.qname"
    }
}