From ab4b6a99d7828154fc803870bbb2f18835d6470b Mon Sep 17 00:00:00 2001 From: Ethan Lee Date: Wed, 11 Mar 2026 21:00:08 +0000 Subject: internal/api: implement module metadata endpoint Change-Id: Id6b8686012803c88c9b7ea71e4b1c0058b7967b0 Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/754861 Auto-Submit: Ethan Lee Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI kokoro-CI: kokoro --- internal/api/api.go | 54 ++++++++++++++++++++++++++++++++ internal/api/api_test.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) (limited to 'internal/api') diff --git a/internal/api/api.go b/internal/api/api.go index d27e49b0..97c3b5a1 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -158,6 +158,60 @@ func ServePackage(w http.ResponseWriter, r *http.Request, ds internal.DataSource return serveJSON(w, http.StatusOK, resp) } +// ServeModule handles requests for the v1 module metadata endpoint. +func ServeModule(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) { + defer derrors.Wrap(&err, "ServeModule") + + modulePath := strings.TrimPrefix(r.URL.Path, "/v1/module/") + if modulePath == "" { + return serveErrorJSON(w, http.StatusBadRequest, "missing module path", nil) + } + + var params ModuleParams + if err := ParseParams(r.URL.Query(), ¶ms); err != nil { + return serveErrorJSON(w, http.StatusBadRequest, err.Error(), nil) + } + + requestedVersion := params.Version + if requestedVersion == "" { + requestedVersion = version.Latest + } + + // For modules, we can use GetUnitMeta on the module path. + um, err := ds.GetUnitMeta(r.Context(), modulePath, modulePath, requestedVersion) + if err != nil { + if errors.Is(err, derrors.NotFound) { + return serveErrorJSON(w, http.StatusNotFound, err.Error(), nil) + } + return err + } + + resp := Module{ + Path: um.ModulePath, + Version: um.Version, + IsStandardLibrary: stdlib.Contains(um.ModulePath), + IsRedistributable: um.IsRedistributable, + } + // RepoURL needs to be extracted from source info if available + if um.SourceInfo != nil { + resp.RepoURL = um.SourceInfo.RepoURL() + } + + if params.Readme { + readme, err := ds.GetModuleReadme(r.Context(), um.ModulePath, um.Version) + if err == nil && readme != nil { + resp.Readme = &Readme{ + Filepath: readme.Filepath, + Contents: readme.Contents, + } + } + } + + // Future: handle licenses param. + + 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 e087b13e..f61c8581 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -166,3 +166,84 @@ func TestServePackage(t *testing.T) { }) } } + +func TestServeModule(t *testing.T) { + ctx := context.Background() + ds := fakedatasource.New() + + const ( + modulePath = "example.com" + version = "v1.2.3" + ) + + ds.MustInsertModule(ctx, &internal.Module{ + ModuleInfo: internal.ModuleInfo{ + ModulePath: modulePath, + Version: version, + }, + Units: []*internal.Unit{{ + UnitMeta: internal.UnitMeta{ + Path: modulePath, + ModuleInfo: internal.ModuleInfo{ + ModulePath: modulePath, + Version: version, + }, + }, + Readme: &internal.Readme{Filepath: "README.md", Contents: "Hello world"}, + }}, + }) + + for _, test := range []struct { + name string + url string + wantStatus int + want *Module + }{ + { + name: "basic module metadata", + url: "/v1/module/example.com?version=v1.2.3", + wantStatus: http.StatusOK, + want: &Module{ + Path: modulePath, + Version: version, + }, + }, + { + name: "module with readme", + url: "/v1/module/example.com?version=v1.2.3&readme=true", + wantStatus: http.StatusOK, + want: &Module{ + Path: modulePath, + Version: version, + Readme: &Readme{ + Filepath: "README.md", + Contents: "Hello world", + }, + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + r := httptest.NewRequest("GET", test.url, nil) + w := httptest.NewRecorder() + + err := ServeModule(w, r, ds) + if err != nil { + t.Fatalf("ServeModule returned error: %v", err) + } + + if w.Code != test.wantStatus { + t.Errorf("status = %d, want %d", w.Code, test.wantStatus) + } + + if test.want != nil { + var got Module + if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { + t.Fatalf("json.Unmarshal: %v", err) + } + if diff := cmp.Diff(test.want, &got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + } + }) + } +} -- cgit v1.3