internal: instantiate v1/package/{path} endpoint
- Create handler for serving v1 package endpoint.
- Create tests to verify endpoint behavior.diff --git a/internal/api/api.go b/internal/api/api.go
new file mode 100644
index 0000000..37645a3
--- /dev/null
+++ b/internal/api/api.go
@@ -0,0 +1,93 @@
+// 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.
+
+package api
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strings"
+
+ "golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/derrors"
+ "golang.org/x/pkgsite/internal/stdlib"
+ "golang.org/x/pkgsite/internal/version"
+)
+
+// ServePackageV1 handles requests for the v1 package metadata endpoint.
+// It is intended to be wrapped by a server's error handler.
+func ServePackageV1(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) {
+ defer derrors.Wrap(&err, "ServePackageV1")
+
+ // The path is expected to be /v1/package/{path}
+ pkgPath := strings.TrimPrefix(r.URL.Path, "/v1/package/")
+ if pkgPath == "" {
+ return &apiError{status: http.StatusBadRequest, err: errors.New("missing package path")}
+ }
+
+ var params PackageParams
+ if err := ParseParams(r.URL.Query(), ¶ms); err != nil {
+ return &apiError{status: http.StatusBadRequest, err: err}
+ }
+
+ requestedVersion := params.Version
+ if requestedVersion == "" {
+ requestedVersion = version.Latest
+ }
+
+ modulePath := params.Module
+ if modulePath == "" {
+ modulePath = internal.UnknownModulePath
+ }
+
+ um, err := ds.GetUnitMeta(r.Context(), pkgPath, modulePath, requestedVersion)
+ if err != nil {
+ if errors.Is(err, derrors.NotFound) {
+ return &apiError{status: http.StatusNotFound, err: err}
+ }
+ return err
+ }
+
+ // Use GetUnit to get the synopsis from documentation.
+ bc := internal.BuildContext{GOOS: params.GOOS, GOARCH: params.GOARCH}
+ unit, err := ds.GetUnit(r.Context(), um, internal.AllFields, bc)
+ if err != nil {
+ return err
+ }
+
+ synopsis := ""
+ if len(unit.Documentation) > 0 {
+ synopsis = unit.Documentation[0].Synopsis
+ }
+
+ resp := Package{
+ Path: unit.Path,
+ ModulePath: unit.ModulePath,
+ ModuleVersion: unit.Version,
+ Synopsis: synopsis,
+ IsStandardLibrary: stdlib.Contains(unit.ModulePath),
+ IsLatest: unit.Version == requestedVersion,
+ GOOS: params.GOOS,
+ GOARCH: params.GOARCH,
+ }
+
+ return serveJSON(w, http.StatusOK, resp)
+}
+
+func serveJSON(w http.ResponseWriter, status int, data any) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ return json.NewEncoder(w).Encode(data)
+}
+
+// apiError is a local error type that can be used to signal HTTP status codes
+// if the caller's error handler supports it.
+type apiError struct {
+ status int
+ err error
+}
+
+func (e *apiError) Error() string { return e.err.Error() }
+func (e *apiError) StatusCode() int { return e.status }
diff --git a/internal/api/api_test.go b/internal/api/api_test.go
new file mode 100644
index 0000000..190f6ed
--- /dev/null
+++ b/internal/api/api_test.go
@@ -0,0 +1,117 @@
+// 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.
+
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/testing/fakedatasource"
+)
+
+func TestServePackageV1(t *testing.T) {
+ ctx := context.Background()
+ ds := fakedatasource.New()
+
+ const (
+ pkgPath = "example.com/pkg"
+ 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: pkgPath,
+ ModuleInfo: internal.ModuleInfo{
+ ModulePath: modulePath,
+ Version: version,
+ },
+ Name: "pkg",
+ },
+ Documentation: []*internal.Documentation{
+ {
+ Synopsis: "This is a test package.",
+ },
+ },
+ },
+ },
+ })
+
+ for _, test := range []struct {
+ name string
+ url string
+ wantStatus int
+ want *Package
+ }{
+ {
+ name: "basic metadata",
+ url: "/v1/package/example.com/pkg?version=v1.2.3",
+ wantStatus: http.StatusOK,
+ want: &Package{
+ Path: pkgPath,
+ ModulePath: modulePath,
+ ModuleVersion: version,
+ Synopsis: "This is a test package.",
+ },
+ },
+ {
+ name: "not found",
+ url: "/v1/package/example.com/nonexistent",
+ wantStatus: http.StatusNotFound,
+ },
+ {
+ name: "missing path",
+ url: "/v1/package/",
+ wantStatus: http.StatusBadRequest,
+ },
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ r := httptest.NewRequest("GET", test.url, nil)
+ w := httptest.NewRecorder()
+
+ err := ServePackageV1(w, r, ds)
+
+ // If it's an apiError, check the status
+ if err != nil {
+ var aerr *apiError
+ if errors.As(err, &aerr) {
+ if aerr.status != test.wantStatus {
+ t.Errorf("status = %d, want %d", aerr.status, test.wantStatus)
+ }
+ } else {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return
+ }
+
+ if w.Code != test.wantStatus {
+ t.Errorf("status = %d, want %d", w.Code, test.wantStatus)
+ }
+ if test.want != nil {
+ var got Package
+ if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
+ t.Fatalf("json.Unmarshal: %v", err)
+ }
+ // Clear fields we don't strictly test here or that might be dynamic
+ got.IsLatest = false
+ if diff := cmp.Diff(test.want, &got); diff != "" {
+ t.Errorf("mismatch (-want +got):\n%s", diff)
+ }
+ }
+ })
+ }
+}
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index 5a55463..a03e45c 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -235,7 +235,9 @@
handle("GET /golang.org/x", s.staticPageHandler("subrepo", "Sub-repositories"))
handle("GET /files/", http.StripPrefix("/files", s.fileMux))
handle("GET /vuln/", vulnHandler)
+ handle("GET /v1/package/", s.errorHandler(api.ServePackageV1))
handle("/opensearch.xml", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
serveFileFS(w, r, s.staticFS, "shared/opensearch.xml")
}))
handle("/", detailHandler)
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: FAILURE
Logs at: https://source.cloud.google.com/results/invocations/46da591b-a33f-4977-8a10-b8012ac26db9
| kokoro-CI | -1 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: FAILURE
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: FAILURE
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: FAILURE
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/83a6cc23-b11d-4313-b4bf-9a705f4bb8c8
| kokoro-CI | +1 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: SUCCESS
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
} else {
modulePath = internal.UnknownModulePath
}I think the only case here is zero candidates. Shouldn't that be a bad request (not found) error?
} else if modulePath != "" && modulePath != internal.UnknownModulePath && !isSentinel(requestedVersion) {Again, I don't know when this would happen, since you've tried all the candidates.
if fs&internal.WithImports != 0 {Why do you need this check?
if fs&internal.WithLicenses != 0 {ditto
isLatest := unit.Version == requestedVersion || (requestedVersion == version.Latest && unit.Version != "")I don't understand this. Why is it the latest version just because it's the version you asked for?
func isSentinel(v string) bool {needsResolution?
w.WriteHeader(status)encode to a bytes.Buffer, check error, then write the bytes to w.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
docs = base64.StdEncoding.EncodeToString(d.Source)This isn't going to be useful to the user. It consists of encoded ASTs. They need to be rendered.
docs = base64.StdEncoding.EncodeToString(d.Source)This isn't going to be useful to the user. It consists of encoded ASTs. They need to be rendered.
Add a TODO(jba) and I'll take care of it.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
} else {
modulePath = internal.UnknownModulePath
}I think the only case here is zero candidates. Shouldn't that be a bad request (not found) error?
Done
} else if modulePath != "" && modulePath != internal.UnknownModulePath && !isSentinel(requestedVersion) {Again, I don't know when this would happen, since you've tried all the candidates.
Done
docs = base64.StdEncoding.EncodeToString(d.Source)Jonathan AmsterdamThis isn't going to be useful to the user. It consists of encoded ASTs. They need to be rendered.
Add a TODO(jba) and I'll take care of it.
Done
Why do you need this check?
Done
if fs&internal.WithLicenses != 0 {Ethan Leeditto
Done
isLatest := unit.Version == requestedVersion || (requestedVersion == version.Latest && unit.Version != "")I don't understand this. Why is it the latest version just because it's the version you asked for?
Yeah that's wrong. I updated it to check that the version is the same as the module latest good version.
func isSentinel(v string) bool {Ethan LeeneedsResolution?
Done
encode to a bytes.Buffer, check error, then write the bytes to w.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: FAILURE
Logs at: https://source.cloud.google.com/results/invocations/b4698c1f-cfc4-439f-a221-f4f839d1f621
| kokoro-CI | -1 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: FAILURE
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Kokoro presubmit build finished with status: FAILURE
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +2 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |