| // Copyright 2026 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Comments beginning with "api:" are read by RouteInfos. |
| // They should not be removed. |
| // If a new route is added, provide all the "api:" comments for it. |
| |
| package api |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "net/http" |
| "slices" |
| "strconv" |
| "strings" |
| "time" |
| |
| "golang.org/x/pkgsite/internal" |
| "golang.org/x/pkgsite/internal/derrors" |
| "golang.org/x/pkgsite/internal/godoc" |
| "golang.org/x/pkgsite/internal/log" |
| "golang.org/x/pkgsite/internal/stdlib" |
| "golang.org/x/pkgsite/internal/version" |
| "golang.org/x/pkgsite/internal/vuln" |
| ) |
| |
| const ( |
| // maxSearchResults is the maximum number of search results to return for a search query. |
| maxSearchResults = 1000 |
| // defaultLimit is the default number of results to return per page for paginated results. |
| defaultLimit = 100 |
| ) |
| |
| // ServePackage handles requests for the v1 package metadata endpoint. |
| // api:route /v1/package/{path} |
| // api:desc Information about the package at {path}. |
| func ServePackage(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) { |
| defer derrors.Wrap(&err, "ServePackage") |
| |
| pkgPath := trimPath(r, "/v1/package/") |
| if pkgPath == "" { |
| return BadRequest("missing package path") |
| } |
| |
| // api:params PackageParams |
| var params PackageParams |
| if err := ParseParams(r.URL.Query(), ¶ms); err != nil { |
| return err |
| } |
| |
| um, err := resolveModulePath(r, ds, pkgPath, params.Module, params.Version) |
| if err != nil { |
| return err |
| } |
| |
| fs := internal.WithMain |
| if params.Licenses { |
| fs |= internal.WithLicenses |
| } |
| if params.Imports { |
| fs |= internal.WithImports |
| } |
| if params.Doc != "" || params.Examples { |
| fs |= internal.WithDocsSource |
| } |
| |
| bc := internal.BuildContext{GOOS: params.GOOS, GOARCH: params.GOARCH} |
| unit, err := ds.GetUnit(r.Context(), um, fs, bc) |
| if err != nil { |
| return err |
| } |
| |
| // api:response Package |
| resp, err := unitToPackage(unit, params) |
| if err != nil { |
| return err |
| } |
| |
| return serveJSON(w, http.StatusOK, resp, versionCacheDur(params.Version)) |
| } |
| |
| // ServeModule handles requests for the v1 module metadata endpoint. |
| // api:route /v1/module/{path} |
| // api:desc Information about the module at {path}. |
| func ServeModule(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) { |
| defer derrors.Wrap(&err, "ServeModule") |
| |
| modulePath := trimPath(r, "/v1/module/") |
| if modulePath == "" { |
| return BadRequest("missing module path") |
| } |
| |
| // api:params ModuleParams |
| var params ModuleParams |
| if err := ParseParams(r.URL.Query(), ¶ms); err != nil { |
| return err |
| } |
| |
| requestedVersion := params.Version |
| if requestedVersion == "" { |
| requestedVersion = version.Latest |
| } |
| // The served response is cacheDur if and only if the version is. |
| cacheDur := versionCacheDur(requestedVersion) |
| |
| // For modules, we can use GetUnitMeta on the module path. |
| um, err := ds.GetUnitMeta(r.Context(), modulePath, modulePath, requestedVersion) |
| if err != nil { |
| return err |
| } |
| |
| // api:response Module |
| resp := Module{ |
| Path: um.ModulePath, |
| Version: um.Version, |
| IsLatest: um.Version == um.LatestVersion, |
| IsStandardLibrary: stdlib.Contains(um.ModulePath), |
| IsRedistributable: um.IsRedistributable, |
| HasGoMod: um.HasGoMod, |
| } |
| // RepoURL needs to be extracted from source info if available |
| if um.SourceInfo != nil { |
| resp.RepoURL = um.SourceInfo.RepoURL() |
| } |
| |
| if !params.Readme && !params.Licenses { |
| return serveJSON(w, http.StatusOK, resp, cacheDur) |
| } |
| |
| fs := internal.MinimalFields |
| if params.Readme { |
| fs |= internal.WithMain // WithMain includes Readme in GetUnit |
| } |
| if params.Licenses { |
| fs |= internal.WithLicenses |
| } |
| unit, err := ds.GetUnit(r.Context(), um, fs, internal.BuildContext{}) |
| if err != nil { |
| return serveJSON(w, http.StatusOK, resp, cacheDur) |
| } |
| |
| if params.Readme && unit.Readme != nil { |
| resp.Readme = &Readme{ |
| Filepath: unit.Readme.Filepath, |
| Contents: unit.Readme.Contents, |
| } |
| } |
| if params.Licenses { |
| for _, l := range unit.LicenseContents { |
| resp.Licenses = append(resp.Licenses, License{ |
| Types: l.Metadata.Types, |
| FilePath: l.Metadata.FilePath, |
| Contents: string(l.Contents), |
| }) |
| } |
| } |
| |
| return serveJSON(w, http.StatusOK, resp, cacheDur) |
| } |
| |
| // ServeModuleVersions handles requests for the v1 module versions endpoint. |
| // api:route /v1/versions/{path} |
| // api:desc All versions of the module at {path}. |
| func ServeModuleVersions(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) { |
| defer derrors.Wrap(&err, "ServeModuleVersions") |
| |
| path := trimPath(r, "/v1/versions/") |
| if path == "" { |
| return BadRequest("missing path") |
| } |
| |
| // api:params VersionsParams |
| var params VersionsParams |
| if err := ParseParams(r.URL.Query(), ¶ms); err != nil { |
| return err |
| } |
| |
| infos, err := ds.GetVersionsForPath(r.Context(), path) |
| if err != nil { |
| return err |
| } |
| // If there are no versions for the path, then the module doesn't exist. |
| if len(infos) == 0 { |
| return fmt.Errorf("module %q: %w", path, derrors.NotFound) |
| } |
| |
| if params.Filter != "" { |
| infos = filter(infos, func(info *internal.ModuleInfo) bool { |
| return strings.Contains(info.Version, params.Filter) |
| }) |
| } |
| |
| // api:response PaginatedResponse[*internal.ModuleInfo] |
| resp, err := paginate(infos, params.ListParams, defaultLimit) |
| if err != nil { |
| return err |
| } |
| |
| // The response is never immutable, because a new version can arrive at any time. |
| return serveJSON(w, http.StatusOK, resp, shortCacheDur) |
| } |
| |
| // ServeModulePackages handles requests for the v1 module packages endpoint. |
| // api:route /v1/packages/{path} |
| // api:desc Information about packages of the module at {path}. |
| func ServeModulePackages(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) { |
| defer derrors.Wrap(&err, "ServeModulePackages") |
| |
| modulePath := trimPath(r, "/v1/packages/") |
| if modulePath == "" { |
| return BadRequest("missing module path") |
| } |
| |
| // api:params PackagesParams |
| var params PackagesParams |
| if err := ParseParams(r.URL.Query(), ¶ms); err != nil { |
| return err |
| } |
| |
| requestedVersion := params.Version |
| if requestedVersion == "" { |
| requestedVersion = version.Latest |
| } |
| |
| metas, err := ds.GetModulePackages(r.Context(), modulePath, requestedVersion) |
| if err != nil { |
| return err |
| } |
| |
| if params.Filter != "" { |
| metas = filter(metas, func(m *internal.PackageMeta) bool { |
| return strings.Contains(m.Path, params.Filter) || strings.Contains(m.Synopsis, params.Filter) |
| }) |
| } |
| var results []Package |
| for _, m := range metas { |
| results = append(results, Package{ |
| Path: m.Path, |
| ModulePath: modulePath, |
| ModuleVersion: requestedVersion, |
| Synopsis: m.Synopsis, |
| IsStandardLibrary: stdlib.Contains(modulePath), |
| }) |
| } |
| |
| // api:response PaginatedResponse[Package] |
| resp, err := paginate(results, params.ListParams, defaultLimit) |
| if err != nil { |
| return err |
| } |
| |
| return serveJSON(w, http.StatusOK, resp, versionCacheDur(requestedVersion)) |
| } |
| |
| // ServeSearch handles requests for the v1 search endpoint. |
| // api:route /v1/search |
| // api:desc Search results. |
| func ServeSearch(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) { |
| defer derrors.Wrap(&err, "ServeSearch") |
| |
| // api:params SearchParams |
| var params SearchParams |
| if err := ParseParams(r.URL.Query(), ¶ms); err != nil { |
| return err |
| } |
| |
| if params.Query == "" { |
| return BadRequest("missing query") |
| } |
| |
| dbresults, err := ds.Search(r.Context(), params.Query, internal.SearchOptions{ |
| MaxResults: maxSearchResults, |
| SearchSymbols: params.Symbol != "", |
| SymbolFilter: params.Symbol, |
| }) |
| if err != nil { |
| return err |
| } |
| |
| if params.Filter != "" { |
| dbresults = filter(dbresults, func(r *internal.SearchResult) bool { |
| return strings.Contains(r.Synopsis, params.Filter) || strings.Contains(r.PackagePath, params.Filter) |
| }) |
| } |
| var results []SearchResult |
| for _, r := range dbresults { |
| results = append(results, SearchResult{ |
| PackagePath: r.PackagePath, |
| ModulePath: r.ModulePath, |
| Version: r.Version, |
| Synopsis: r.Synopsis, |
| }) |
| } |
| |
| // api:response PaginatedResponse[SearchResult] |
| resp, err := paginate(results, params.ListParams, defaultLimit) |
| if err != nil { |
| return fmt.Errorf("%w: %s", derrors.InvalidArgument, err.Error()) |
| } |
| |
| // Search results are never immutable, because new modules are always being added. |
| // NOTE: the default cache freshness is set to 1 hour (see serveJSON). This seems |
| // like a reasonable time to cache a search, but be aware of complaints |
| // about stale search results. |
| return serveJSON(w, http.StatusOK, resp, shortCacheDur) |
| } |
| |
| // ServePackageSymbols handles requests for the v1 package symbols endpoint. |
| // api:route /v1/symbols/{path} |
| // api:desc List of symbols for the package at {path}. |
| func ServePackageSymbols(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) { |
| defer derrors.Wrap(&err, "ServePackageSymbols") |
| |
| pkgPath := trimPath(r, "/v1/symbols/") |
| if pkgPath == "" { |
| return BadRequest("missing package path") |
| } |
| |
| // api:params SymbolsParams |
| var params SymbolsParams |
| if err := ParseParams(r.URL.Query(), ¶ms); err != nil { |
| return err |
| } |
| |
| um, err := resolveModulePath(r, ds, pkgPath, params.Module, params.Version) |
| if err != nil { |
| return err |
| } |
| |
| bc := internal.BuildContext{GOOS: params.GOOS, GOARCH: params.GOARCH} |
| syms, err := ds.GetSymbols(r.Context(), pkgPath, um.ModulePath, um.Version, bc) |
| if err != nil { |
| return err |
| } |
| |
| if params.Filter != "" { |
| syms = filter(syms, func(s *internal.Symbol) bool { |
| return strings.Contains(s.Name, params.Filter) || strings.Contains(s.Synopsis, params.Filter) |
| }) |
| } |
| var items []Symbol |
| for _, s := range syms { |
| items = append(items, Symbol{ |
| ModulePath: um.ModulePath, |
| Version: um.Version, |
| Name: s.Name, |
| Kind: string(s.Kind), |
| Synopsis: s.Synopsis, |
| Parent: s.ParentName, |
| }) |
| } |
| |
| // api:response PaginatedResponse[Symbol] |
| resp, err := paginate(items, params.ListParams, defaultLimit) |
| if err != nil { |
| return err |
| } |
| |
| return serveJSON(w, http.StatusOK, resp, versionCacheDur(params.Version)) |
| } |
| |
| // ServePackageImportedBy handles requests for the v1 package imported-by endpoint. |
| // api:route /v1/imported-by/{path} |
| // api:desc Paths of packages importing the package at {path}, not including packages in the same module. |
| func ServePackageImportedBy(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) { |
| defer derrors.Wrap(&err, "ServePackageImportedBy") |
| |
| pkgPath := trimPath(r, "/v1/imported-by/") |
| if pkgPath == "" { |
| return BadRequest("missing package path") |
| } |
| |
| // api:params ImportedByParams |
| var params ImportedByParams |
| if err := ParseParams(r.URL.Query(), ¶ms); err != nil { |
| return err |
| } |
| |
| requestedVersion := params.Version |
| if requestedVersion == "" { |
| requestedVersion = version.Latest |
| } |
| |
| um, err := resolveModulePath(r, ds, pkgPath, params.Module, requestedVersion) |
| if err != nil { |
| return err |
| } |
| modulePath := um.ModulePath |
| |
| importedBy, err := ds.GetImportedBy(r.Context(), pkgPath, modulePath, 1000) |
| if err != nil { |
| return err |
| } |
| |
| count, err := ds.GetImportedByCount(r.Context(), pkgPath, modulePath) |
| if err != nil { |
| return err |
| } |
| |
| if params.Filter != "" { |
| importedBy = filter(importedBy, func(p string) bool { |
| return strings.Contains(p, params.Filter) |
| }) |
| } |
| |
| // api:response PaginatedResponse[string] |
| paged, err := paginate(importedBy, params.ListParams, defaultLimit) |
| if err != nil { |
| return err |
| } |
| |
| resp := PackageImportedBy{ |
| ModulePath: modulePath, |
| Version: requestedVersion, |
| ImportedBy: PaginatedResponse[string]{ |
| Items: paged.Items, |
| Total: count, |
| NextPageToken: paged.NextPageToken, |
| }, |
| } |
| |
| // The imported-by list is not immutable, because new modules are always being added. |
| return serveJSON(w, http.StatusOK, resp, shortCacheDur) |
| } |
| |
| // ServeVulnerabilities handles requests for the v1 module vulnerabilities endpoint. |
| // api:route /v1/vulns/{path} |
| // api:desc Vulnerabilities of the module at {path}. |
| func ServeVulnerabilities(vc *vuln.Client) func(w http.ResponseWriter, r *http.Request, ds internal.DataSource) error { |
| return func(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) { |
| defer derrors.Wrap(&err, "ServeVulnerabilities") |
| |
| modulePath := trimPath(r, "/v1/vulns/") |
| if modulePath == "" { |
| return BadRequest("missing module path") |
| } |
| |
| // api:params VulnParams |
| var params VulnParams |
| if err := ParseParams(r.URL.Query(), ¶ms); err != nil { |
| return err |
| } |
| |
| if vc == nil { |
| return InternalServerError("vulnerability client is nil") |
| } |
| |
| requestedVersion := params.Version |
| if requestedVersion == "" { |
| requestedVersion = version.Latest |
| } |
| |
| // Use VulnsForPackage from internal/vuln to get vulnerabilities for the module. |
| // Passing an empty packagePath gets all vulns for the module. |
| vulns := vuln.VulnsForPackage(r.Context(), modulePath, requestedVersion, "", vc) |
| |
| if params.Filter != "" { |
| vulns = filter(vulns, func(v vuln.Vuln) bool { |
| return strings.Contains(v.ID, params.Filter) || strings.Contains(v.Details, params.Filter) |
| }) |
| } |
| var items []Vulnerability |
| for _, v := range vulns { |
| items = append(items, Vulnerability{ |
| ID: v.ID, |
| Details: v.Details, |
| }) |
| } |
| |
| // api:response PaginatedResponse[Vulnerability] |
| resp, err := paginate(items, params.ListParams, defaultLimit) |
| if err != nil { |
| return err |
| } |
| |
| return serveJSON(w, http.StatusOK, resp, versionCacheDur(requestedVersion)) |
| } |
| } |
| |
| func trimPath(r *http.Request, prefix string) string { |
| path := strings.TrimPrefix(r.URL.Path, prefix) |
| return strings.Trim(path, "/") |
| } |
| |
| // resolveModulePath determines the correct module path for a given package path and version. |
| // If the module path is not provided, it searches through potential candidate module paths |
| // derived from the package path. |
| // |
| // Resolution logic: |
| // 1. Use internal.CandidateModulePaths(pkgPath) to get potential candidates (ordered longest first). |
| // 2. Fetch UnitMeta for each candidate that exists in the data source. |
| // 3. Check if um.ModulePath == mp (where mp is the candidate module path). If not, ignore it |
| // (this handles the case where GetUnitMeta falls back to another module when the requested |
| // module does not exist). |
| // 4. Filter candidates by eliminating those that are deprecated or retracted. |
| // 5. If exactly one candidate remains after filtering, return it (HTTP 200). |
| // 6. If multiple candidates remain, return HTTP 400 with the list of candidates (ambiguity). |
| // 7. If all candidates are eliminated (e.g., all are deprecated or retracted), fall back to |
| // the longest matching candidate among those that exist (HTTP 200). |
| func resolveModulePath(r *http.Request, ds internal.DataSource, pkgPath, modulePath, requestedVersion string) (*internal.UnitMeta, error) { |
| if requestedVersion == "" { |
| requestedVersion = version.Latest |
| } |
| if modulePath != "" { |
| um, err := ds.GetUnitMeta(r.Context(), pkgPath, modulePath, requestedVersion) |
| if err != nil { |
| return nil, err |
| } |
| return um, nil |
| } |
| |
| candidates := internal.CandidateModulePaths(pkgPath) |
| var validCandidates []*internal.UnitMeta |
| for _, mp := range candidates { |
| if um, err := ds.GetUnitMeta(r.Context(), pkgPath, mp, requestedVersion); err == nil { |
| // Critical check: ensure the DB actually found the candidate module we requested. |
| // GetUnitMeta falls back to the best match if the requested module doesn't exist, |
| // which could lead to false positives (e.g. google.golang.org matching because it |
| // falls back to google.golang.org/adk/agent). |
| if um.ModulePath == mp { |
| validCandidates = append(validCandidates, um) |
| } |
| } else if !errors.Is(err, derrors.NotFound) { |
| return nil, err |
| } |
| } |
| |
| if len(validCandidates) == 0 { |
| return nil, derrors.NotFound |
| } |
| |
| // Filter candidates based on signals (deprecation, retraction). |
| goodCandidates := slices.Clone(validCandidates) |
| goodCandidates = slices.DeleteFunc(goodCandidates, func(um *internal.UnitMeta) bool { |
| return um.Deprecated || um.Retracted |
| }) |
| |
| switch len(goodCandidates) { |
| case 1: |
| return goodCandidates[0], nil |
| case 0: |
| // If all candidates are deprecated or retracted, fall back to the longest match. |
| // Since candidates are ordered longest first, validCandidates[0] is the longest match. |
| return validCandidates[0], nil |
| default: |
| return nil, &Error{ |
| Code: http.StatusBadRequest, |
| Message: "ambiguous package path", |
| Candidates: makeCandidates(goodCandidates), |
| } |
| } |
| } |
| |
| func makeCandidates(ums []*internal.UnitMeta) []Candidate { |
| var r []Candidate |
| for _, um := range ums { |
| r = append(r, Candidate{ |
| ModulePath: um.ModulePath, |
| PackagePath: um.Path, |
| }) |
| } |
| return r |
| } |
| |
| // Values for the Cache-Control header. |
| // Compare with the TTLs for pkgsite's own cache, in internal/frontend/server.go |
| // (look for symbols ending in "TTL"). |
| // Those values are shorter to manage our cache's memory, but the job of |
| // Cache-Control is to reduce network traffic; downstream caches can manage |
| // their own memory. |
| const ( |
| // Immutable pages can theoretically, be cached indefinitely, |
| // but have them time out so that excluded modules don't |
| // live in caches forever. |
| longCacheDur = 3 * time.Hour |
| // The information on some pages can change relatively quickly. |
| shortCacheDur = 1 * time.Hour |
| // Errors should not be cached. |
| noCache = time.Duration(0) |
| ) |
| |
| func serveJSON(w http.ResponseWriter, status int, data any, cacheDur time.Duration) error { |
| var buf bytes.Buffer |
| if err := json.NewEncoder(&buf).Encode(data); err != nil { |
| return err |
| } |
| w.Header().Set("Content-Type", "application/json") |
| var ccHeader string |
| if cacheDur == 0 { |
| ccHeader = "no-store" |
| } else { |
| ccHeader = fmt.Sprintf("public, max-age=%d", int(cacheDur.Seconds())) |
| } |
| w.Header().Set("Cache-Control", ccHeader) |
| w.WriteHeader(status) |
| _, err := w.Write(buf.Bytes()) |
| return err |
| } |
| |
| func ServeError(w http.ResponseWriter, r *http.Request, err error) error { |
| var aerr *Error |
| if !errors.As(err, &aerr) { |
| status := derrors.ToStatus(err) |
| aerr = &Error{ |
| Code: status, |
| Message: strings.ToLower(http.StatusText(status)), |
| err: err, |
| } |
| } |
| log.Errorf(r.Context(), "API error %d: %v", aerr.Code, aerr) |
| return serveJSON(w, aerr.Code, aerr, noCache) |
| } |
| |
| // paginate returns a paginated response for the given list of items and pagination parameters. |
| // It uses offset-based pagination with a token that encodes the offset. |
| // The default limit is used if the provided limit is non-positive. |
| func paginate[T any](all []T, lp ListParams, defaultLimit int) (PaginatedResponse[T], error) { |
| limit := lp.Limit |
| if limit <= 0 { |
| limit = defaultLimit |
| } |
| |
| offset := 0 |
| if lp.Token != "" { |
| var err error |
| offset, err = strconv.Atoi(lp.Token) |
| if err != nil || offset < 0 { |
| return PaginatedResponse[T]{}, fmt.Errorf("%w: invalid token", derrors.InvalidArgument) |
| } |
| } |
| |
| if offset > len(all) { |
| offset = len(all) |
| } |
| end := min(offset+limit, len(all)) |
| |
| var nextToken string |
| if end < len(all) { |
| nextToken = strconv.Itoa(end) |
| } |
| |
| return PaginatedResponse[T]{ |
| Items: all[offset:end], |
| Total: len(all), |
| NextPageToken: nextToken, |
| }, nil |
| } |
| |
| // unitToPackage processes unit documentation into a Package struct. |
| func unitToPackage(unit *internal.Unit, params PackageParams) (*Package, error) { |
| if params.Examples && params.Doc == "" { |
| return nil, BadRequest("examples require doc format to be specified") |
| } |
| |
| // Although unit.Documentation is a slice, it will |
| // have at most one item, the documentation matching |
| // the build context. |
| synopsis := "" |
| var docs string |
| goos := params.GOOS |
| goarch := params.GOARCH |
| if len(unit.Documentation) > 0 { |
| d := unit.Documentation[0] |
| synopsis = d.Synopsis |
| // Return the more precise GOOS/GOARCH. |
| // If the user didn't provide them, use the unit's. |
| // If the user did, assume what they provided is at |
| // least as specific as the unit's, and use it. |
| if goos == "" { |
| goos = d.GOOS |
| } |
| if goarch == "" { |
| goarch = d.GOARCH |
| } |
| if params.Doc != "" { |
| var err error |
| docs, err = renderDocumentation(unit, d, params.Doc, params.Examples) |
| if err != nil { |
| return nil, err |
| } |
| } |
| } |
| |
| var licenses []License |
| for _, l := range unit.LicenseContents { |
| licenses = append(licenses, License{ |
| Types: l.Metadata.Types, |
| FilePath: l.Metadata.FilePath, |
| Contents: string(l.Contents), |
| }) |
| } |
| |
| return &Package{ |
| Path: unit.Path, |
| ModulePath: unit.ModulePath, |
| ModuleVersion: unit.Version, |
| Synopsis: synopsis, |
| IsStandardLibrary: stdlib.Contains(unit.ModulePath), |
| IsLatest: unit.Version == unit.LatestVersion, |
| GOOS: goos, |
| GOARCH: goarch, |
| Docs: docs, |
| Imports: unit.Imports, |
| Licenses: licenses, |
| }, nil |
| } |
| |
| // renderDocumentation renders the provided unit into the specified format. |
| func renderDocumentation(unit *internal.Unit, d *internal.Documentation, format string, examples bool) (string, error) { |
| // d.Source is an encoded AST. Decode it, then use |
| // go/doc (not pkgsite's renderer) to generate the |
| // result. |
| gpkg, err := godoc.DecodePackage(d.Source) |
| if err != nil { |
| return "", fmt.Errorf("renderDocumentation: %w", err) |
| } |
| innerPath := internal.Suffix(unit.Path, unit.ModulePath) |
| modInfo := &godoc.ModuleInfo{ModulePath: unit.ModulePath, ResolvedVersion: unit.Version} |
| dpkg, err := gpkg.DocPackage(innerPath, modInfo) |
| if err != nil { |
| return "", err |
| } |
| var r renderer |
| var sb strings.Builder |
| switch format { |
| case "text": |
| r = newTextRenderer(gpkg.Fset, &sb) |
| case "md", "markdown": |
| r = newMarkdownRenderer(gpkg.Fset, &sb) |
| case "html": |
| r = newHTMLRenderer(gpkg.Fset, &sb) |
| default: |
| return "", BadRequest("bad doc format: need one of 'text', 'md', 'markdown' or 'html'") |
| } |
| if err := renderDoc(dpkg, r, examples); err != nil { |
| return "", fmt.Errorf("renderDoc: %w", err) |
| } |
| return sb.String(), nil |
| } |
| |
| // versionCacheDur returns the duration used in the Cache-Control header |
| // appropriate for the given module version. |
| func versionCacheDur(v string) time.Duration { |
| immutable := !(v == "" || v == version.Latest || internal.DefaultBranches[v] || stdlib.SupportedBranches[v]) |
| if immutable { |
| return longCacheDur |
| } |
| return shortCacheDur |
| } |
| |
| // filter returns a new slice containing all elements in list for which pred is true. |
| func filter[T any](list []T, pred func(T) bool) []T { |
| var out []T |
| for _, e := range list { |
| if pred(e) { |
| out = append(out, e) |
| } |
| } |
| return out |
| } |