diff options
Diffstat (limited to 'handler.go')
| -rw-r--r-- | handler.go | 206 |
1 files changed, 132 insertions, 74 deletions
@@ -1,10 +1,12 @@ package main import ( + "cmp" "fmt" "html/template" "net/http" - "sort" + "path" + "slices" "strings" "go.uber.org/sally/templates" @@ -17,112 +19,168 @@ var ( template.New("package.html").Parse(templates.Package)) ) -// CreateHandler creates a Sally http.Handler +// 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. +// 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. func CreateHandler(config *Config) http.Handler { mux := http.NewServeMux() - - pkgs := make([]packageInfo, 0, len(config.Packages)) + pkgs := make([]*sallyPackage, 0, len(config.Packages)) for name, pkg := range config.Packages { - handler := newPackageHandler(config, name, pkg) + baseURL := config.URL + if pkg.URL != "" { + // Package-specific override for the base URL. + baseURL = pkg.URL + } + modulePath := path.Join(baseURL, name) + docURL := "https://" + path.Join(config.Godoc.Host, modulePath) + + pkg := &sallyPackage{ + Name: name, + Desc: pkg.Desc, + ModulePath: modulePath, + DocURL: docURL, + GitURL: pkg.Repo, + } + pkgs = append(pkgs, pkg) + // 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) - - pkgs = append(pkgs, packageInfo{ - Desc: pkg.Desc, - ImportPath: handler.canonicalURL, - GitURL: handler.gitURL, - GodocHome: handler.godocHost + "/" + handler.canonicalURL, - }) } - sort.Slice(pkgs, func(i, j int) bool { - return pkgs[i].ImportPath < pkgs[j].ImportPath + + mux.Handle("/", newIndexHandler(pkgs)) + return requireMethod(http.MethodGet, mux) +} + +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) }) - mux.Handle("/", &indexHandler{pkgs: pkgs}) +} - return mux +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 + + // Description of the package, if any. + Desc string + + // URL at which documentation for the package can be found. + DocURL string + + // URL at which the Git repository is hosted. + GitURL string } type indexHandler struct { - pkgs []packageInfo + pkgs []*sallyPackage // sorted by name } -type packageInfo struct { - Desc string // package description - ImportPath string // canonical import path - GitURL string // URL of the Git repository - GodocHome string // documentation home URL -} +var _ http.Handler = (*indexHandler)(nil) -func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Index handler only supports '/'. - // ServeMux will call us for any '/foo' that is not a known package. - if r.Method != http.MethodGet || r.URL.Path != "/" { - http.NotFound(w, r) - return - } +func newIndexHandler(pkgs []*sallyPackage) *indexHandler { + slices.SortFunc(pkgs, func(a, b *sallyPackage) int { + return cmp.Compare(a.Name, b.Name) + }) - data := struct{ Packages []packageInfo }{ - Packages: h.pkgs, - } - if err := indexTemplate.Execute(w, data); err != nil { - http.Error(w, err.Error(), 500) + return &indexHandler{ + pkgs: pkgs, } } -type packageHandler struct { - // Hostname of the godoc server, e.g. "godoc.org". - godocHost string +func (h *indexHandler) rangeOf(path string) (start, end int) { + if len(path) == 0 { + return 0, len(h.pkgs) + } - // Name of the package relative to the vanity base URL. - // For example, "zap" for "go.uber.org/zap". - name string + // 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) + }) - // Path at which the Git repository is hosted. - // For example, "github.com/uber-go/zap". - gitURL string + 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 + } + } - // Canonical import path for the package. - canonicalURL string + // All packages following start are descendants of path. + // Return the rest of the packages. + return start, len(h.pkgs) } -func newPackageHandler(cfg *Config, name string, pkg PackageConfig) *packageHandler { - baseURL := cfg.URL - if pkg.URL != "" { - baseURL = pkg.URL +func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, "/"), "/") + start, end := h.rangeOf(path) + + // 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 } - canonicalURL := fmt.Sprintf("%s/%s", baseURL, name) - return &packageHandler{ - godocHost: cfg.Godoc.Host, - name: name, - canonicalURL: canonicalURL, - gitURL: pkg.Repo, + err := indexTemplate.Execute(w, + struct{ Packages []*sallyPackage }{ + Packages: h.pkgs[start:end], + }) + if err != nil { + http.Error(w, err.Error(), 500) } } -func (h *packageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.NotFound(w, r) - return - } +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.name) - - data := struct { - Repo string - CanonicalURL string - GodocURL string + // "/foo/bar" => "/bar" + // "/foo" => "" + relPath := strings.TrimPrefix(r.URL.Path, "/"+h.Pkg.Name) + + err := packageTemplate.Execute(w, struct { + ModulePath string + GitURL string + DocURL string }{ - Repo: h.gitURL, - CanonicalURL: h.canonicalURL, - GodocURL: fmt.Sprintf("https://%s/%s%s", h.godocHost, h.canonicalURL, relPath), - } - if err := packageTemplate.Execute(w, data); err != nil { + ModulePath: h.Pkg.ModulePath, + GitURL: h.Pkg.GitURL, + DocURL: h.Pkg.DocURL + relPath, + }) + if err != nil { http.Error(w, err.Error(), 500) } } + +func descends(from, to string) bool { + return to == from || (strings.HasPrefix(to, from) && to[len(from)] == '/') +} |