Skip to content

Commit

Permalink
Simplify the implementation
Browse files Browse the repository at this point in the history
Switch back to http.ServeMux for package handlers,
and move the scanning logic to the index handler.
  • Loading branch information
abhinav committed Oct 17, 2023
1 parent c8ca51f commit 30ef30e
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 494 deletions.
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ require (
github.com/stretchr/testify v1.8.4
golang.org/x/net v0.15.0
gopkg.in/yaml.v3 v3.0.1
pgregory.net/rapid v1.1.0
)

require (
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,3 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw=
pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
169 changes: 104 additions & 65 deletions handler.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package main

import (
"errors"
"cmp"
"fmt"
"html/template"
"net/http"
"path"
"slices"
"strings"

"go.uber.org/sally/templates"
Expand All @@ -18,27 +19,22 @@ var (
template.New("package.html").Parse(templates.Package))
)

// Handler handles inbound HTTP requests.
//
// It provides the following endpoints:
// CreateHandler builds a new handler
// with the provided package configuration.
// The returned handler provides the following endpoints:
//
// GET /
// Index page listing all packages.
// GET /<name>
// Package page for the given package.
// Package page for the given package.
// GET /<dir>
// Page listing packages under the given directory,
// assuming that there's no package with the given name.
// GET /<name>/<subpkg>
// Package page for the given subpackage.
type Handler struct {
pkgs pathTree[*sallyPackage]
}

// CreateHandler builds a new handler
// with the provided package configuration.
func CreateHandler(config *Config) *Handler {
var pkgs pathTree[*sallyPackage]
func CreateHandler(config *Config) http.Handler {
mux := http.NewServeMux()
pkgs := make([]*sallyPackage, 0, len(config.Packages))
for name, pkg := range config.Packages {
baseURL := config.URL
if pkg.URL != "" {
Expand All @@ -48,22 +44,43 @@ func CreateHandler(config *Config) *Handler {
modulePath := path.Join(baseURL, name)
docURL := "https://" + path.Join(config.Godoc.Host, modulePath)

pkgs.Set(name, &sallyPackage{
pkg := &sallyPackage{
Name: name,
Desc: pkg.Desc,
ModulePath: modulePath,
DocURL: docURL,
GitURL: pkg.Repo,
})
}
}
pkgs = append(pkgs, pkg)

return &Handler{
pkgs: pkgs,
// Double-register so that "/foo"
// does not redirect to "/foo/" with a 300.
handler := &packageHandler{Pkg: pkg}
mux.Handle("/"+name, handler)
mux.Handle("/"+name+"/", handler)
}

mux.Handle("/", newIndexHandler(pkgs))
return requireMethod(http.MethodGet, mux)
}

var _ http.Handler = (*Handler)(nil)
func requireMethod(method string, handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != method {
http.NotFound(w, r)
return
}

handler.ServeHTTP(w, r)
})
}

type sallyPackage struct {
// Name of the package.
//
// This is the part after the base URL.
Name string

// Canonical import path for the package.
ModulePath string

Expand All @@ -77,72 +94,94 @@ type sallyPackage struct {
GitURL string
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.serveHTTP(w, r); err != nil {
if herr := new(httpError); errors.As(err, &herr) {
http.Error(w, herr.Message, herr.Code)
} else {
http.Error(w, err.Error(), 500)
}
}
type indexHandler struct {
pkgs []*sallyPackage // sorted by name
}

// httpError indicates that an HTTP error occurred.
//
// The caller will write the error code and message to the response.
type httpError struct {
Code int // HTTP status code
Message string // error message
}
var _ http.Handler = (*indexHandler)(nil)

func newIndexHandler(pkgs []*sallyPackage) *indexHandler {
slices.SortFunc(pkgs, func(a, b *sallyPackage) int {
return cmp.Compare(a.Name, b.Name)
})

func httpErrorf(code int, format string, args ...interface{}) error {
return &httpError{
Code: code,
Message: fmt.Sprintf(format, args...),
return &indexHandler{
pkgs: pkgs,
}
}

func (e *httpError) Error() string {
return fmt.Sprintf("status %d: %s", e.Code, e.Message)
}
func (h *indexHandler) rangeOf(path string) (start, end int) {
if len(path) == 0 {
return 0, len(h.pkgs)
}

// serveHTTP is similar to ServeHTTP, except it returns an error.
//
// If it returns an httpError,
// the caller will write the error code and message to the response.
func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodGet {
return httpErrorf(http.StatusNotFound, "method %q not allowed", r.Method)
// If the packages are sorted by name,
// we can scan adjacent packages to find the range of packages
// whose name descends from path.
start, _ = slices.BinarySearchFunc(h.pkgs, path, func(pkg *sallyPackage, path string) int {
return cmp.Compare(pkg.Name, path)
})

for idx := start; idx < len(h.pkgs); idx++ {
if !descends(path, h.pkgs[idx].Name) {
// End of matching sequences.
// The next path is not a descendant of path.
return start, idx
}
}

// All packages following start are descendants of path.
// Return the rest of the packages.
return start, len(h.pkgs)
}

func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, "/"), "/")
start, end := h.rangeOf(path)

if pkg, suffix, ok := h.pkgs.Lookup(path); ok {
return h.servePackage(w, pkg, suffix)
// If start == end, then there are no packages
if start == end {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "no packages found under path: %v\n", path)
return
}

err := indexTemplate.Execute(w,
struct{ Packages []*sallyPackage }{
Packages: h.pkgs[start:end],
})
if err != nil {
http.Error(w, err.Error(), 500)

Check warning on line 154 in handler.go

View check run for this annotation

Codecov / codecov/patch

handler.go#L154

Added line #L154 was not covered by tests
}
return h.serveIndex(w, path, h.pkgs.ListByPath(path))
}

func (h *Handler) servePackage(w http.ResponseWriter, pkg *sallyPackage, suffix string) error {
return packageTemplate.Execute(w,
type packageHandler struct {
Pkg *sallyPackage
}

var _ http.Handler = (*packageHandler)(nil)

func (h *packageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Extract the relative path to subpackages, if any.
// "/foo/bar" => "/bar"
// "/foo" => ""
relPath := strings.TrimPrefix(r.URL.Path, "/"+h.Pkg.Name)

err := packageTemplate.Execute(w,
struct {
ModulePath string
GitURL string
DocURL string
}{
ModulePath: pkg.ModulePath,
GitURL: pkg.GitURL,
DocURL: pkg.DocURL + suffix,
ModulePath: h.Pkg.ModulePath,
GitURL: h.Pkg.GitURL,
DocURL: h.Pkg.DocURL + relPath,
})
}

func (h *Handler) serveIndex(w http.ResponseWriter, path string, pkgs []*sallyPackage) error {
if len(pkgs) == 0 {
return httpErrorf(http.StatusNotFound, "no packages found under path: %s", path)
if err != nil {
http.Error(w, err.Error(), 500)
}
}

return indexTemplate.Execute(w,
struct{ Packages []*sallyPackage }{
Packages: pkgs,
})
func descends(from, to string) bool {
return to == from || (strings.HasPrefix(to, from) && to[len(from)] == '/')
}
87 changes: 85 additions & 2 deletions handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"sort"
"strings"
"testing"

Expand Down Expand Up @@ -83,10 +84,10 @@ func TestTrailingSlash(t *testing.T) {
<html>
<head>
<meta name="go-import" content="go.uber.org/yarpc git https://github.com/yarpc/yarpc-go">
<meta http-equiv="refresh" content="0; url=https://pkg.go.dev/go.uber.org/yarpc">
<meta http-equiv="refresh" content="0; url=https://pkg.go.dev/go.uber.org/yarpc/">
</head>
<body>
Nothing to see here. Please <a href="https://pkg.go.dev/go.uber.org/yarpc">move along</a>.
Nothing to see here. Please <a href="https://pkg.go.dev/go.uber.org/yarpc/">move along</a>.
</body>
</html>
`)
Expand Down Expand Up @@ -179,6 +180,88 @@ func TestPostRejected(t *testing.T) {
}
}

func TestIndexHandler_rangeOf(t *testing.T) {
tests := []struct {
desc string
pkgs []*sallyPackage
path string
want []string // names
}{
{
desc: "empty",
pkgs: []*sallyPackage{
{Name: "foo"},
{Name: "bar"},
},
want: []string{"foo", "bar"},
},
{
desc: "single child",
pkgs: []*sallyPackage{
{Name: "foo/bar"},
{Name: "baz"},
},
path: "foo",
want: []string{"foo/bar"},
},
{
desc: "multiple children",
pkgs: []*sallyPackage{
{Name: "foo/bar"},
{Name: "foo/baz"},
{Name: "qux"},
{Name: "quux/quuz"},
},
path: "foo",
want: []string{"foo/bar", "foo/baz"},
},
{
desc: "to end of list",
pkgs: []*sallyPackage{
{Name: "a"},
{Name: "b"},
{Name: "c/d"},
{Name: "c/e"},
},
path: "c",
want: []string{"c/d", "c/e"},
},
{
desc: "similar name",
pkgs: []*sallyPackage{
{Name: "foobar"},
{Name: "foo/bar"},
},
path: "foo",
want: []string{"foo/bar"},
},
{
desc: "no match",
pkgs: []*sallyPackage{
{Name: "foo"},
{Name: "bar"},
},
path: "baz",
},
}

for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
h := newIndexHandler(tt.pkgs)
start, end := h.rangeOf(tt.path)

var got []string
for _, pkg := range tt.pkgs[start:end] {
got = append(got, pkg.Name)
}
sort.Strings(got)
sort.Strings(tt.want)

assert.Equal(t, tt.want, got)
})
}
}

func BenchmarkHandlerDispatch(b *testing.B) {
handler := CreateHandler(&Config{
URL: "go.uberalt.org",
Expand Down
Loading

0 comments on commit 30ef30e

Please sign in to comment.