Skip to content

Instantly share code, notes, and snippets.

@benhoyt
Created October 23, 2021 05:44

Revisions

  1. benhoyt created this gist Oct 23, 2021.
    76 changes: 76 additions & 0 deletions router.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,76 @@
    /*
    From Yuri Vishnevsky on Gophers Slack:
    Hi Ben! I read your great post on Go routing. Based on
    the ideas there I wound up implementing my own router for a small
    site I'm building, which combines a few ideas from your post.Instead
    of passing a format string followed by pieces as in match
    ("foo/+/baz", &bar) I decided to inline the pieces, so the match
    arguments read as the path does: match("foo", &bar, "baz"). I'm
    curious if you have any thoughts, since this seems quite nice to me,
    and has a simple implementation. No worries if you're too busy to
    think about this right now; I just thought you might find it
    interesting.
    */

    package main

    import (
    "fmt"
    "strconv"
    "strings"
    )

    // match reports whether `path` matches the given `pieces`
    // and assigns pointer pieces. A piece can be a string,
    // *string, or *int64. This function matches pieces greedily
    // and may assign pieces even when the path does not match.
    // Note: Does not normalize paths with path.Clean.
    // Note: Consecutive string path components need to be matched
    // with separate strings, since this always splits on /.
    func match(path string, pieces ...interface{}) bool {
    // Remove the initial "/" prefix
    if strings.HasPrefix(path, "/") {
    path = path[1:]
    }
    var head string
    for i, piece := range pieces {
    // Shift the next path component into `head`
    head, path = nextComponent(path)
    // Match pieces based on their type
    switch p := piece.(type) {
    case string:
    // Match a specific string
    if p != head {
    return false
    }
    case *string:
    // Match any string, including the empty string
    *p = head
    case *int64:
    // Match any 64-bit integer, including negative integers
    n, err := strconv.ParseInt(head, 10, 64)
    if err != nil {
    return false
    }
    *p = n
    default:
    panic(fmt.Sprintf("each piece must be a string, *string, or *int64. Got %T", piece))
    }
    // If the path is fully consumed, we're done if pieces are also fully consumed
    if path == "" {
    return i == len(pieces)-1
    }
    }
    // Pieces are consumed; return true if the path is too.
    return path == ""
    }

    // Accepts a path without leading slash and returns two strings:
    // its first component and the rest without leading slash
    func nextComponent(path string) (head, tail string) {
    i := strings.IndexByte(path, '/')
    if i == -1 {
    return path, ""
    }
    return path[:i], path[i+1:]
    }