package main import ( "cmp" "fmt" "html/template" "net/http" "path" "slices" "strings" "go.uber.org/sally/templates" ) var ( indexTemplate = template.Must( template.New("index.html").Parse(templates.Index)) packageTemplate = template.Must( template.New("package.html").Parse(templates.Package)) ) // CreateHandler builds a new handler // with the provided package configuration. // The returned handler provides the following endpoints: // // GET / // Index page listing all packages. // GET / // Package page for the given package. // GET / // Page listing packages under the given directory, // assuming that there's no package with the given name. // GET // // Package page for the given subpackage. 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 != "" { // 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) } 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) }) } 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 []*sallyPackage // sorted by name } 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) }) return &indexHandler{ pkgs: pkgs, } } func (h *indexHandler) rangeOf(path string) (start, end int) { if len(path) == 0 { return 0, len(h.pkgs) } // 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 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) } } 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: 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)] == '/') }