Tawesoft Logo

tawesoft.co.uk/go/router example: submatches.go

// Demonstrates HTTP routing with "submatch" patterns in a path component
// with a server at localhost:8080
package main

import (
    "encoding/json"
    "encoding/xml"
    "fmt"
    "html/template"
    "log"
    "net/http"
    "regexp"
    "strconv"
    
    "tawesoft.co.uk/go/router"
)

type User struct {
    Name string
    DOB string
}

var Users = map[int]User{
    0: {"Alan Turing",  "23 June 1912"},
    1: {"Grace Hopper", "9 December 1906"},
    2: {"Donald Knuth", "10 January 1938"},
}

func HandleIndex(w http.ResponseWriter, r *http.Request, match *router.Match) {
    const tpl = `
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Users</title>
    </head>
    <body>
        <h1>Users</h1>
        {{range $i, $u := . }}
            <div>{{ $u.Name }}:
                <a href="/users/{{ $i }}.json">JSON</a> |
                <a href="/users/{{ $i }}.xml">XML</a>
            </div>
        {{else}}
            <div><strong>no rows</strong></div>
        {{end}}
    </body>
</html>`
    
    t, err := template.New("webpage").Parse(tpl)
    if err != nil { panic(err) }
    
    err = t.Execute(w, Users)
    if err != nil { panic(err) }
}

func HandleUserById(w http.ResponseWriter, r *http.Request, match *router.Match, id int, format string) {
    user := Users[id] // range handling not done
    var out []byte
    var werr error
    
    switch format {
        case "":
            fallthrough // default to JSON
        case ".json":
            w.Header().Set("Content-Type", "application/json; charset=utf-8")
            out, werr = json.Marshal(user)
        case ".xml":
            w.Header().Set("Content-Type", "application/xml; charset=utf-8")
            out, werr = xml.Marshal(user)
        default:
            panic("invalid format")
    }
    
    if werr != nil { panic(werr) }
    w.Write(out)
    
}

type MyNormalHandlerType func(http.ResponseWriter, *http.Request, *router.Match)
type MyAPIHandlerType func (http.ResponseWriter, *http.Request, *router.Match, int, string)

func main() {
    // Let's make a convention: our API endpoints all end with a numeric ID
    // and an optional format specifier, like .xml or .json e.g.
    // "/users/123.json"
    
    // A regex that captures two submatches
    // e.g. "1234" => "1234", ""
    // "1234.json" => "1234", ".json"
    id := regexp.MustCompile(`^(\d{1,9})(\.\w{1,5})?$`)
    idNumberMatch := 1 // first capture
    idFormatMatch := 2 // second capture
    
    routes := router.Route{Children: []router.Route{
        {Pattern: "users", Children: []router.Route{
            {Pattern: id, Key: "id", Methods: "GET", Handler: MyAPIHandlerType(HandleUserById)},
        }},
        {Handler: MyNormalHandlerType(HandleIndex)},
    }}
    
    router, err := router.New(routes)
    if err != nil { panic(err) }
    
    log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
        match := router.MatchHttpRequest(r)
        
        // how you handle the match is up to you!
        if match != nil {
            
            if apiMatch, ok := match.Route.Handler.(MyAPIHandlerType); ok {

                // Get the integer id as the first submatch of the id component
                id, err := strconv.Atoi(match.Submatch("id", idNumberMatch))
                if err != nil { panic(err) }
                
                // Get the format as the second submatch of the id component
                format := match.Submatch("id", idFormatMatch)
            
                apiMatch(w, r, match, id, format)
            } else {
                match.Route.Handler.(MyNormalHandlerType)(w, r, match)
            }

        } else {
            http.NotFound(w, r)
            fmt.Fprintf(w, "Not Found: Sorry, no matching route!")
        }
    })))
}