aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEthan Lee <ethanalee@google.com>2026-03-11 21:08:19 +0000
committerGopher Robot <gobot@golang.org>2026-03-24 10:46:32 -0700
commit9da8f15f1c5721ddc1866bda9091a7f49c63fa10 (patch)
treecaa115cf134e6d93321dc671f0adfd85c6826a1a
parent3619741c8e028c954fd53a4d554c3d4efe9a2356 (diff)
downloadgo-x-pkgsite-9da8f15f1c5721ddc1866bda9091a7f49c63fa10.tar.xz
internal/api: implement module packages endpoint
- Implement the ServeModulePackages handler and introduce GetModulePackages to return the list of packages for a given module version. Change-Id: I20c1618e2fdbf0126cb913665a1d7457b8951177 Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/754863 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> kokoro-CI: kokoro <noreply+kokoro@google.com> Auto-Submit: Ethan Lee <ethanalee@google.com> Reviewed-by: Jonathan Amsterdam <jba@google.com>
-rw-r--r--internal/api/api.go56
-rw-r--r--internal/api/api_test.go58
-rw-r--r--internal/datasource.go2
-rw-r--r--internal/fetchdatasource/fetchdatasource.go29
-rw-r--r--internal/frontend/server.go1
-rw-r--r--internal/postgres/unit.go5
-rw-r--r--internal/postgres/unit_test.go24
-rw-r--r--internal/testing/fakedatasource/fakedatasource.go28
8 files changed, 199 insertions, 4 deletions
diff --git a/internal/api/api.go b/internal/api/api.go
index d94098e3..1d21ba7e 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -283,6 +283,62 @@ func ServeModuleVersions(w http.ResponseWriter, r *http.Request, ds internal.Dat
return serveJSON(w, http.StatusOK, resp)
}
+// ServeModulePackages handles requests for the v1 module packages endpoint.
+func ServeModulePackages(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) {
+ defer derrors.Wrap(&err, "ServeModulePackages")
+
+ modulePath := strings.TrimPrefix(r.URL.Path, "/v1/packages/")
+ if modulePath == "" {
+ return serveErrorJSON(w, http.StatusBadRequest, "missing module path", nil)
+ }
+
+ var params PackagesParams
+ if err := ParseParams(r.URL.Query(), &params); err != nil {
+ return serveErrorJSON(w, http.StatusBadRequest, err.Error(), nil)
+ }
+
+ requestedVersion := params.Version
+ if requestedVersion == "" {
+ requestedVersion = version.Latest
+ }
+
+ metas, err := ds.GetModulePackages(r.Context(), modulePath, requestedVersion)
+ if err != nil {
+ if errors.Is(err, derrors.NotFound) {
+ return serveErrorJSON(w, http.StatusNotFound, err.Error(), nil)
+ }
+ return err
+ }
+
+ // TODO: Handle params.Token and params.Filter.
+ // For now, we just use params.Limit to limit the number of packages returned.
+ limit := params.Limit
+ if limit <= 0 {
+ limit = 100
+ }
+ if limit > len(metas) {
+ limit = len(metas)
+ }
+
+ var items []Package
+ for _, m := range metas[:limit] {
+ items = append(items, Package{
+ Path: m.Path,
+ ModulePath: modulePath,
+ ModuleVersion: requestedVersion,
+ Synopsis: m.Synopsis,
+ IsStandardLibrary: stdlib.Contains(modulePath),
+ })
+ }
+
+ resp := PaginatedResponse[Package]{
+ Items: items,
+ Total: len(metas),
+ }
+
+ return serveJSON(w, http.StatusOK, resp)
+}
+
// needsResolution reports whether the version string is a sentinel like "latest" or "master".
func needsResolution(v string) bool {
return v == version.Latest || v == version.Master || v == version.Main
diff --git a/internal/api/api_test.go b/internal/api/api_test.go
index 9983a8cf..a0b63904 100644
--- a/internal/api/api_test.go
+++ b/internal/api/api_test.go
@@ -322,11 +322,61 @@ func TestServeModuleVersions(t *testing.T) {
if len(got.Items) != test.wantCount {
t.Errorf("count = %d, want %d", len(got.Items), test.wantCount)
}
- if test.name == "with limit" && got.NextPageToken != "1" {
- t.Errorf("nextPageToken = %q, want %q", got.NextPageToken, "1")
+ }
+ })
+ }
+}
+
+func TestServeModulePackages(t *testing.T) {
+ ctx := context.Background()
+ ds := fakedatasource.New()
+
+ const (
+ modulePath = "example.com"
+ version = "v1.0.0"
+ )
+
+ ds.MustInsertModule(ctx, &internal.Module{
+ ModuleInfo: internal.ModuleInfo{ModulePath: modulePath, Version: version},
+ Units: []*internal.Unit{
+ {UnitMeta: internal.UnitMeta{Path: modulePath, Name: "pkg1"}},
+ {UnitMeta: internal.UnitMeta{Path: modulePath + "/sub", Name: "pkg2"}},
+ },
+ })
+
+ for _, test := range []struct {
+ name string
+ url string
+ wantStatus int
+ wantCount int
+ }{
+ {
+ name: "all packages",
+ url: "/v1/packages/example.com?version=v1.0.0",
+ wantStatus: http.StatusOK,
+ wantCount: 2,
+ },
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ r := httptest.NewRequest("GET", test.url, nil)
+ w := httptest.NewRecorder()
+
+ err := ServeModulePackages(w, r, ds)
+ if err != nil {
+ t.Fatalf("ServeModulePackages returned error: %v", err)
+ }
+
+ if w.Code != test.wantStatus {
+ t.Errorf("status = %d, want %d", w.Code, test.wantStatus)
+ }
+
+ if test.wantStatus == http.StatusOK {
+ var got PaginatedResponse[Package]
+ if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
+ t.Fatalf("json.Unmarshal: %v", err)
}
- if test.name == "pagination" && got.NextPageToken != "2" {
- t.Errorf("nextPageToken = %q, want %q", got.NextPageToken, "2")
+ if len(got.Items) != test.wantCount {
+ t.Errorf("count = %d, want %d", len(got.Items), test.wantCount)
}
}
})
diff --git a/internal/datasource.go b/internal/datasource.go
index 912b54de..f6bf8c84 100644
--- a/internal/datasource.go
+++ b/internal/datasource.go
@@ -93,6 +93,8 @@ type DataSource interface {
GetLatestInfo(ctx context.Context, unitPath, modulePath string, latestUnitMeta *UnitMeta) (LatestInfo, error)
// GetVersionsForPath returns a list of versions for the given path.
GetVersionsForPath(ctx context.Context, path string) ([]*ModuleInfo, error)
+ // GetModulePackages returns a list of packages in the given module version.
+ GetModulePackages(ctx context.Context, modulePath, version string) ([]*PackageMeta, error)
// SearchSupport reports the search types supported by this datasource.
SearchSupport() SearchSupport
diff --git a/internal/fetchdatasource/fetchdatasource.go b/internal/fetchdatasource/fetchdatasource.go
index 41573ae5..1643d009 100644
--- a/internal/fetchdatasource/fetchdatasource.go
+++ b/internal/fetchdatasource/fetchdatasource.go
@@ -404,3 +404,32 @@ func (ds *FetchDataSource) Search(ctx context.Context, q string, opts internal.S
return results, nil
}
+
+// GetModulePackages returns a list of packages in the given module version.
+func (ds *FetchDataSource) GetModulePackages(ctx context.Context, modulePath, version string) ([]*internal.PackageMeta, error) {
+ m, err := ds.getModule(ctx, modulePath, version)
+ if err != nil {
+ return nil, err
+ }
+ var metas []*internal.PackageMeta
+ for _, um := range m.UnitMetas {
+ if um.IsPackage() {
+ u, err := ds.findUnit(ctx, m, um.Path)
+ if err != nil {
+ return nil, err
+ }
+ var synopsis string
+ if len(u.Documentation) > 0 {
+ synopsis = u.Documentation[0].Synopsis
+ }
+ metas = append(metas, &internal.PackageMeta{
+ Path: u.Path,
+ Name: u.Name,
+ Synopsis: synopsis,
+ IsRedistributable: u.IsRedistributable,
+ Licenses: u.Licenses,
+ })
+ }
+ }
+ return metas, nil
+}
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index b229af0d..acd9c909 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -239,6 +239,7 @@ func (s *Server) Install(handle func(string, http.Handler), cacher Cacher, authV
handle("GET /v1/package/", s.errorHandler(api.ServePackage))
handle("GET /v1/module/", s.errorHandler(api.ServeModule))
handle("GET /v1/versions/", s.errorHandler(api.ServeModuleVersions))
+ handle("GET /v1/packages/", s.errorHandler(api.ServeModulePackages))
handle("/opensearch.xml", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
serveFileFS(w, r, s.staticFS, "shared/opensearch.xml")
}))
diff --git a/internal/postgres/unit.go b/internal/postgres/unit.go
index 765d541e..fee52439 100644
--- a/internal/postgres/unit.go
+++ b/internal/postgres/unit.go
@@ -277,6 +277,11 @@ func (db *DB) GetUnit(ctx context.Context, um *internal.UnitMeta, fields interna
return u, nil
}
+// GetModulePackages returns a list of packages in the given module version.
+func (db *DB) GetModulePackages(ctx context.Context, modulePath, version string) ([]*internal.PackageMeta, error) {
+ return getPackagesInUnit(ctx, db.db, modulePath, modulePath, version, -1, db.bypassLicenseCheck)
+}
+
func (db *DB) getUnitID(ctx context.Context, fullPath, modulePath, resolvedVersion string) (_ int, _ bool, err error) {
defer derrors.WrapStack(&err, "getUnitID(ctx, %q, %q, %q)", fullPath, modulePath, resolvedVersion)
defer stats.Elapsed(ctx, "getUnitID")()
diff --git a/internal/postgres/unit_test.go b/internal/postgres/unit_test.go
index 82dbd9d9..a35d5658 100644
--- a/internal/postgres/unit_test.go
+++ b/internal/postgres/unit_test.go
@@ -917,3 +917,27 @@ func newUnitMeta(path, modulePath, version string) *internal.UnitMeta {
},
}
}
+
+func TestGetModulePackages(t *testing.T) {
+ t.Parallel()
+ testDB, release := acquire(t)
+ defer release()
+ ctx := context.Background()
+
+ m := sample.Module("m.com", "v1.2.3", "a", "a/b", "c")
+ MustInsertModule(ctx, t, testDB, m)
+
+ got, err := testDB.GetModulePackages(ctx, "m.com", "v1.2.3")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var gotPaths []string
+ for _, p := range got {
+ gotPaths = append(gotPaths, p.Path)
+ }
+ wantPaths := []string{"m.com/a", "m.com/a/b", "m.com/c"}
+ if diff := cmp.Diff(wantPaths, gotPaths); diff != "" {
+ t.Errorf("GetModulePackages mismatch (-want +got):\n%s", diff)
+ }
+}
diff --git a/internal/testing/fakedatasource/fakedatasource.go b/internal/testing/fakedatasource/fakedatasource.go
index 46de2a8d..82212163 100644
--- a/internal/testing/fakedatasource/fakedatasource.go
+++ b/internal/testing/fakedatasource/fakedatasource.go
@@ -309,6 +309,34 @@ func (ds *FakeDataSource) GetLatestInfo(ctx context.Context, unitPath, modulePat
}, nil
}
+// GetModulePackages returns a list of packages in the given module version.
+func (ds *FakeDataSource) GetModulePackages(ctx context.Context, modulePath, version string) ([]*internal.PackageMeta, error) {
+ m := ds.getModule(modulePath, version)
+ if m == nil {
+ return nil, derrors.NotFound
+ }
+ var pkgs []*internal.PackageMeta
+ for _, u := range m.Units {
+ if u.IsPackage() {
+ var syn string
+ if len(u.Documentation) > 0 {
+ syn = u.Documentation[0].Synopsis
+ }
+ pkgs = append(pkgs, &internal.PackageMeta{
+ Path: u.Path,
+ Name: u.Name,
+ Synopsis: syn,
+ IsRedistributable: u.IsRedistributable,
+ Licenses: u.Licenses,
+ })
+ }
+ }
+ sort.Slice(pkgs, func(i, j int) bool {
+ return pkgs[i].Path < pkgs[j].Path
+ })
+ return pkgs, nil
+}
+
// SearchSupport reports the search types supported by this datasource.
func (ds *FakeDataSource) SearchSupport() internal.SearchSupport {
// internal/frontend.TestDetermineSearchAction depends on us returning FullSearch