internal/middleware: LatestVersion takes one function

Reorganize middleware.LatestVersion so it takes a single function that
returns all the information about latest versions and paths, instead
of two separate functions.

Change-Id: If644a8bcdde4137a1264b1b93c64e186f8550ffa
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/279454
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go
index 2f4e92e..8f47216 100644
--- a/cmd/frontend/main.go
+++ b/cmd/frontend/main.go
@@ -191,9 +191,9 @@
 		middleware.RequestLog(cmdconfig.Logger(ctx, cfg, "frontend-log")),
 		middleware.AcceptRequests(http.MethodGet, http.MethodPost), // accept only GETs and POSTs
 		middleware.Quota(cfg.Quota, cacheClient),
-		middleware.GodocURL(),                                                                 // potentially redirects so should be early in chain
-		middleware.SecureHeaders(!*disableCSP),                                                // must come before any caching for nonces to work
-		middleware.LatestVersions(server.GetLatestMinorVersion, server.GetLatestMajorVersion), // must come before caching for version badge to work
+		middleware.GodocURL(),                           // potentially redirects so should be early in chain
+		middleware.SecureHeaders(!*disableCSP),          // must come before any caching for nonces to work
+		middleware.LatestVersions(server.GetLatestInfo), // must come before caching for version badge to work
 		middleware.Panic(panicHandler),
 		ermw,
 		middleware.Timeout(54*time.Second),
diff --git a/internal/frontend/latest_version.go b/internal/frontend/latest_version.go
index c540ffe..fd5c2d3 100644
--- a/internal/frontend/latest_version.go
+++ b/internal/frontend/latest_version.go
@@ -11,12 +11,19 @@
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/log"
+	"golang.org/x/pkgsite/internal/middleware"
 )
 
-// GetLatestMajorVersion returns the latest module path and the full package path
+func (s *Server) GetLatestInfo(ctx context.Context, fullPath, modulePath string) (latest middleware.LatestInfo) {
+	latest.MinorVersion = s.getLatestMinorVersion(ctx, fullPath, internal.UnknownModulePath)
+	latest.MajorModulePath, latest.MajorPackagePath = s.getLatestMajorVersion(ctx, fullPath, modulePath)
+	return latest
+}
+
+// getLatestMajorVersion returns the latest module path and the full package path
 // of any major version found given the fullPath and the modulePath.
 // It is intended to be used as an argument to middleware.LatestVersions.
-func (s *Server) GetLatestMajorVersion(ctx context.Context, fullPath, modulePath string) (_ string, _ string) {
+func (s *Server) getLatestMajorVersion(ctx context.Context, fullPath, modulePath string) (_ string, _ string) {
 	latestModulePath, latestPackagePath, err := s.getDataSource(ctx).GetLatestMajorVersion(ctx, fullPath, modulePath)
 	if err != nil {
 		if !errors.Is(err, derrors.NotFound) {
@@ -27,10 +34,10 @@
 	return latestModulePath, latestPackagePath
 }
 
-// GetLatestMinorVersion returns the latest minor version of the package or module.
+// getLatestMinorVersion returns the latest minor version of the package or module.
 // The linkable form of the minor version is returned and is an empty string on error.
 // It is intended to be used as an argument to middleware.LatestVersions.
-func (s *Server) GetLatestMinorVersion(ctx context.Context, packagePath, modulePath string) string {
+func (s *Server) getLatestMinorVersion(ctx context.Context, packagePath, modulePath string) string {
 	// It is okay to use a different DataSource (DB connection) than the rest of the
 	// request, because this makes a self-contained call on the DB.
 	v, err := latestMinorVersion(ctx, s.getDataSource(ctx), packagePath, modulePath)
diff --git a/internal/frontend/server_test.go b/internal/frontend/server_test.go
index d14ab30..93f8a27 100644
--- a/internal/frontend/server_test.go
+++ b/internal/frontend/server_test.go
@@ -1218,7 +1218,7 @@
 		t.Fatal(err)
 	}
 	mw := middleware.Chain(
-		middleware.LatestVersions(s.GetLatestMinorVersion, s.GetLatestMajorVersion),
+		middleware.LatestVersions(s.GetLatestInfo),
 		middleware.Experiment(exp))
 	return s, mw(mux), func() {
 		teardown()
diff --git a/internal/middleware/latestversion.go b/internal/middleware/latestversion.go
index 6b793ca..27dc3b9 100644
--- a/internal/middleware/latestversion.go
+++ b/internal/middleware/latestversion.go
@@ -12,7 +12,6 @@
 	"strings"
 
 	"golang.org/x/mod/module"
-	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/log"
 )
 
@@ -27,13 +26,19 @@
 // latestInfoRegexp extracts values needed to determine the latest-version badge from a page's HTML.
 var latestInfoRegexp = regexp.MustCompile(`data-version="([^"]*)" data-mpath="([^"]*)" data-ppath="([^"]*)" data-pagetype="([^"]*)"`)
 
-type latestMinorFunc func(ctx context.Context, packagePath, modulePath string) string
-type latestMajorFunc func(ctx context.Context, fullPath, modulePath string) (string, string)
+// LatestInfo holds information about the latest versions and paths of a unit.
+type LatestInfo struct {
+	MinorVersion     string // latest minor version for unit path, regardless of module
+	MajorModulePath  string // path of latest version of module
+	MajorPackagePath string // path of unit in latest version of module
+}
+
+type latestFunc func(ctx context.Context, packagePath, modulePath string) LatestInfo
 
 // LatestVersions replaces the HTML placeholder values for the badge and banner
 // that displays whether the version of the package or module being served is
 // the latest minor version (badge) and the latest major version (banner).
-func LatestVersions(latestMinor latestMinorFunc, latestMajor latestMajorFunc) Middleware {
+func LatestVersions(getLatest latestFunc) Middleware {
 	return func(h http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			crw := &capturingResponseWriter{ResponseWriter: w}
@@ -47,19 +52,18 @@
 				modulePath := string(matches[2])
 				_, majorVersion, _ := module.SplitPathVersion(modulePath)
 				packagePath := string(matches[3])
-				latestMinorVersion := latestMinor(r.Context(), packagePath, internal.UnknownModulePath)
+				latest := getLatest(r.Context(), packagePath, modulePath)
 				latestMinorClass := "DetailsHeader-badge"
 				switch {
-				case latestMinorVersion == "":
+				case latest.MinorVersion == "":
 					latestMinorClass += "--unknown"
-				case latestMinorVersion == version:
+				case latest.MinorVersion == version:
 					latestMinorClass += "--latest"
 				default:
 					latestMinorClass += "--goToLatest"
 				}
 
-				latestModulePath, latestPackagePath := latestMajor(r.Context(), packagePath, modulePath)
-				_, latestMajorVersion, ok := module.SplitPathVersion(latestModulePath)
+				_, latestMajorVersion, ok := module.SplitPathVersion(latest.MajorModulePath)
 				var latestMajorVersionText string
 				if ok && len(latestMajorVersion) > 0 {
 					latestMajorVersionText = latestMajorVersion[1:]
@@ -73,10 +77,10 @@
 					latestMajorClass += " DetailsHeader-banner--latest"
 				}
 				body = bytes.ReplaceAll(body, []byte(latestMinorClassPlaceholder), []byte(latestMinorClass))
-				body = bytes.ReplaceAll(body, []byte(LatestMinorVersionPlaceholder), []byte(latestMinorVersion))
+				body = bytes.ReplaceAll(body, []byte(LatestMinorVersionPlaceholder), []byte(latest.MinorVersion))
 				body = bytes.ReplaceAll(body, []byte(latestMajorClassPlaceholder), []byte(latestMajorClass))
 				body = bytes.ReplaceAll(body, []byte(LatestMajorVersionPlaceholder), []byte(latestMajorVersionText))
-				body = bytes.ReplaceAll(body, []byte(LatestMajorVersionURL), []byte(latestPackagePath))
+				body = bytes.ReplaceAll(body, []byte(LatestMajorVersionURL), []byte(latest.MajorPackagePath))
 			}
 			if _, err := w.Write(body); err != nil {
 				log.Errorf(r.Context(), "LatestVersions, writing: %v", err)
diff --git a/internal/middleware/latestversion_test.go b/internal/middleware/latestversion_test.go
index 01ed123..7639287 100644
--- a/internal/middleware/latestversion_test.go
+++ b/internal/middleware/latestversion_test.go
@@ -16,13 +16,13 @@
 func TestLatestMinorVersion(t *testing.T) {
 	for _, test := range []struct {
 		name   string
-		latest latestMinorFunc
+		latest latestFunc
 		in     string
 		want   string
 	}{
 		{
 			name:   "package version is not latest",
-			latest: func(context.Context, string, string) string { return "v1.2.3" },
+			latest: constLatestFunc("v1.2.3", "", ""),
 			in: `
                 <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
 					 data-version="v1.0.0" data-mpath="p1/p2" data-ppath="p1/p2/p3" data-pagetype="pkg">
@@ -38,7 +38,7 @@
 		},
 		{
 			name:   "package version is latest",
-			latest: func(context.Context, string, string) string { return "v1.2.3" },
+			latest: constLatestFunc("v1.2.3", "", ""),
 			in: `
                 <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
 					 data-version="v1.2.3" data-mpath="p1/p2" data-ppath="p1/p2/p3" data-pagetype="pkg">
@@ -54,7 +54,7 @@
 		},
 		{
 			name:   "package version with build is latest",
-			latest: func(context.Context, string, string) string { return "v1.2.3+build" },
+			latest: constLatestFunc("v1.2.3+build", "", ""),
 			in: `
                 <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
 					 data-version="v1.2.3&#43;build" data-mpath="p1/p2" data-ppath="p1/p2/p3" data-pagetype="pkg">
@@ -70,7 +70,7 @@
 		},
 		{
 			name:   "module version is not latest",
-			latest: func(context.Context, string, string) string { return "v1.2.3" },
+			latest: constLatestFunc("v1.2.3", "", ""),
 			in: `
                 <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
 					 data-version="v1.0.0" data-mpath="p1/p2" data-ppath="" data-pagetype="pkg">
@@ -86,7 +86,7 @@
 		},
 		{
 			name:   "module version is latest",
-			latest: func(context.Context, string, string) string { return "v1.2.3" },
+			latest: constLatestFunc("v1.2.3", "", ""),
 			in: `
                 <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
 					 data-version="v1.2.3" data-mpath="p1/p2" data-ppath="" data-pagetype="pkg">
@@ -102,7 +102,7 @@
 		},
 		{
 			name:   "latest func returns empty string",
-			latest: func(context.Context, string, string) string { return "" },
+			latest: constLatestFunc("", "", ""),
 			in: `
                 <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
 					 data-version="v1.2.3" data-mpath="p1/p2" data-ppath="" data-pagetype="pkg">
@@ -118,7 +118,7 @@
 		},
 		{
 			name:   "no regexp match",
-			latest: func(context.Context, string, string) string { return "v1.2.3" },
+			latest: constLatestFunc("v1.2.3", "", ""),
 			in: `
                 <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$">
                     <span>Latest</span>
@@ -135,8 +135,7 @@
 			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				fmt.Fprint(w, test.in)
 			})
-			latestMajor := func(context.Context, string, string) (string, string) { return "", "" }
-			ts := httptest.NewServer(LatestVersions(test.latest, latestMajor)(handler))
+			ts := httptest.NewServer(LatestVersions(test.latest)(handler))
 			defer ts.Close()
 			resp, err := ts.Client().Get(ts.URL)
 			if err != nil {
@@ -154,19 +153,23 @@
 	}
 }
 
+func constLatestFunc(minorVersion, majorModPath, majorPackagePath string) latestFunc {
+	return func(context.Context, string, string) LatestInfo {
+		return LatestInfo{minorVersion, majorModPath, majorPackagePath}
+	}
+}
+
 func TestLatestMajorVersion(t *testing.T) {
 	for _, test := range []struct {
 		name        string
-		latest      latestMajorFunc
+		latest      latestFunc
 		modulePaths []string
 		in          string
 		want        string
 	}{
 		{
-			name: "module path is not at latest",
-			latest: func(context.Context, string, string) (string, string) {
-				return "foo.com/bar/v3", "foo.com/bar/v3"
-			},
+			name:   "module path is not at latest",
+			latest: constLatestFunc("", "foo.com/bar/v3", "foo.com/bar/v3"),
 			modulePaths: []string{
 				"foo.com/bar",
 				"foo.com/bar/v2",
@@ -189,7 +192,7 @@
 		},
 		{
 			name:   "module path is at latest",
-			latest: func(context.Context, string, string) (string, string) { return "foo.com/bar/v3", "foo.com/bar/v3" },
+			latest: constLatestFunc("", "foo.com/bar/v3", "foo.com/bar/v3"),
 			modulePaths: []string{
 				"foo.com/bar",
 				"foo.com/bar/v2",
@@ -212,7 +215,7 @@
 		},
 		{
 			name:   "full path is not at the latest",
-			latest: func(context.Context, string, string) (string, string) { return "foo.com/bar/v3", "foo.com/bar/v3/far" },
+			latest: constLatestFunc("", "foo.com/bar/v3", "foo.com/bar/v3/far"),
 			modulePaths: []string{
 				"foo.com/bar",
 				"foo.com/bar/v2",
@@ -238,8 +241,7 @@
 			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 				fmt.Fprint(w, test.in)
 			})
-			latestMinor := func(context.Context, string, string) string { return "" }
-			ts := httptest.NewServer(LatestVersions(latestMinor, test.latest)(handler))
+			ts := httptest.NewServer(LatestVersions(test.latest)(handler))
 			defer ts.Close()
 			resp, err := ts.Client().Get(ts.URL)
 			if err != nil {
diff --git a/internal/testing/integration/frontend_test.go b/internal/testing/integration/frontend_test.go
index 78b4903..23f1547 100644
--- a/internal/testing/integration/frontend_test.go
+++ b/internal/testing/integration/frontend_test.go
@@ -62,7 +62,7 @@
 	mw := middleware.Chain(
 		middleware.AcceptRequests(http.MethodGet, http.MethodPost),
 		middleware.SecureHeaders(enableCSP),
-		middleware.LatestVersions(s.GetLatestMinorVersion, s.GetLatestMajorVersion),
+		middleware.LatestVersions(s.GetLatestInfo),
 		middleware.Experiment(experimenter),
 	)
 	return httptest.NewServer(mw(mux))