Skip to content

Commit

Permalink
internal/middleware: add latest major version middleware function
Browse files Browse the repository at this point in the history
This change introduces a middleware function for the new
GetLatestMajorVersion function. We also rename the function name to
LatestVersions to clarify the scope better.

The latest major version value is generated and writen into new
placeholder values that is prepared for the upcoming HTML changes.

Updates golang/go#37765

Change-Id: I1f3b19a4e2cfb290000f756e8548670cbfd4033d
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/251157
Reviewed-by: Jonathan Amsterdam <jba@google.com>
  • Loading branch information
Miguel Acero committed Aug 31, 2020
1 parent 6e7412d commit 6f12538
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 19 deletions.
6 changes: 3 additions & 3 deletions cmd/frontend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,9 @@ func main() {
middleware.RequestLog(requestLogger),
middleware.AcceptRequests(http.MethodGet, http.MethodPost), // accept only GETs and POSTs
middleware.Quota(cfg.Quota),
middleware.GodocURL(), // potentially redirects so should be early in chain
middleware.SecureHeaders(), // must come before any caching for nonces to work
middleware.LatestVersion(server.LatestVersion), // must come before caching for version badge to work
middleware.GodocURL(), // potentially redirects so should be early in chain
middleware.SecureHeaders(), // 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.Panic(panicHandler),
middleware.Timeout(54*time.Second),
middleware.Experiment(experimenter),
Expand Down
25 changes: 20 additions & 5 deletions internal/frontend/latest_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,25 @@ import (
"golang.org/x/pkgsite/internal/log"
)

// LatestVersion returns the latest version of the package or module.
// The linkable form of the version is returned.
// It returns the empty string on error.
// It is intended to be used as an argument to middleware.LatestVersion.
func (s *Server) LatestVersion(ctx context.Context, packagePath, modulePath, pageType string) string {
// GetLatestMajorVersion returns the major version of a package or module.
// If a module isn't found from the series path or an error ocurs, an empty string is returned
// It is intended to be used as an argument to middleware.LatestVersions.
func (s *Server) GetLatestMajorVersion(ctx context.Context, seriesPath string) string {
mv, err := s.getDataSource(ctx).GetLatestMajorVersion(ctx, seriesPath)
if err != nil {
if !errors.Is(err, derrors.NotFound) {
log.Errorf(ctx, "GetLatestMajorVersion: %v", err)
}
return ""
}

return mv
}

// 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, pageType 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, pageType)
Expand All @@ -29,6 +43,7 @@ func (s *Server) LatestVersion(ctx context.Context, packagePath, modulePath, pag
}
return ""
}

return v
}

Expand Down
2 changes: 1 addition & 1 deletion internal/frontend/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1125,7 +1125,7 @@ func newTestServer(t *testing.T, proxyModules []*proxy.Module, experimentNames .
t.Fatal(err)
}
mw := middleware.Chain(
middleware.LatestVersion(s.LatestVersion),
middleware.LatestVersions(s.GetLatestMinorVersion, s.GetLatestMajorVersion),
middleware.Experiment(exp))
return s, mw(mux), func() {
teardown()
Expand Down
33 changes: 27 additions & 6 deletions internal/middleware/latestversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,29 @@ import (
"regexp"
"strings"

"golang.org/x/mod/module"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/log"
)

const (
latestMinorClassPlaceholder = "$$GODISCOVERY_LATESTMINORCLASS$$"
LatestMinorVersionPlaceholder = "$$GODISCOVERY_LATESTMINORVERSION$$"
latestMajorClassPlaceholder = "$$GODISCOVERY_LATESTMAJORCLASS$$"
LatestMajorVersionPlaceholder = "$$GODISCOVERY_LATESTMAJORVERSION$$"
LatestMajorVersionURL = "$$GODISCOVERY_LATESTMAJORVERSIONURL$$"
)

// 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 latestFunc func(ctx context.Context, packagePath, modulePath, pageType string) string
type latestMinorFunc func(ctx context.Context, packagePath, modulePath, pageType string) string
type latestMajorFunc func(ctx context.Context, seriesPath string) string

// LatestVersion supports the badge that displays whether the version of the
// package or module being served is the latest one.
func LatestVersion(latest latestFunc) Middleware {
// 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 {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
crw := &capturingResponseWriter{ResponseWriter: w}
Expand All @@ -38,9 +45,11 @@ func LatestVersion(latest latestFunc) Middleware {
// The template package converts '+' to its HTML entity.
version = strings.Replace(version, "&#43;", "+", -1)
modulePath := string(matches[2])
seriesPath := internal.SeriesPathForModule(modulePath)
_, majorVersion, _ := module.SplitPathVersion(modulePath)
packagePath := string(matches[3])
pageType := string(matches[4])
latestMinorVersion := latest(r.Context(), packagePath, modulePath, pageType)
latestMinorVersion := latestMinor(r.Context(), packagePath, modulePath, pageType)
latestMinorClass := "DetailsHeader-badge"
switch {
case latestMinorVersion == "":
Expand All @@ -50,11 +59,23 @@ func LatestVersion(latest latestFunc) Middleware {
default:
latestMinorClass += "--goToLatest"
}
latestMajorVersion := latestMajor(r.Context(), seriesPath)
latestMajorClass := ""
// If the latest major version is the same as the major version of the current
// module path, it is currently the latest version so we don't show the banner.
// If an error occurs finding a major version (i.e: not found) an empty string
// is returned in which case we also don't show the banner.
if majorVersion == latestMajorVersion || latestMajorVersion == "" {
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(latestMajorClassPlaceholder), []byte(latestMajorClass))
body = bytes.ReplaceAll(body, []byte(LatestMajorVersionPlaceholder), []byte(latestMajorVersion))
body = bytes.ReplaceAll(body, []byte(LatestMajorVersionURL), []byte(seriesPath+latestMajorVersion))
}
if _, err := w.Write(body); err != nil {
log.Errorf(r.Context(), "LatestVersion, writing: %v", err)
log.Errorf(r.Context(), "LatestVersions, writing: %v", err)
}
})
}
Expand Down
85 changes: 82 additions & 3 deletions internal/middleware/latestversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import (
"testing"
)

func TestLatestVersion(t *testing.T) {
func TestLatestMinorVersion(t *testing.T) {
for _, test := range []struct {
name string
latest latestFunc
latest latestMinorFunc
in string
want string
}{
Expand Down Expand Up @@ -135,7 +135,86 @@ func TestLatestVersion(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, test.in)
})
ts := httptest.NewServer(LatestVersion(test.latest)(handler))
latestMajor := func(context.Context, string) string { return "" }
ts := httptest.NewServer(LatestVersions(test.latest, latestMajor)(handler))
defer ts.Close()
resp, err := ts.Client().Get(ts.URL)
if err != nil {
t.Fatal(err)
}
got, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
_ = resp.Body.Close()
if string(got) != test.want {
t.Errorf("\ngot %s\nwant %s", got, test.want)
}
})
}
}

func TestLatestMajorVersion(t *testing.T) {
for _, test := range []struct {
name string
latest latestMajorFunc
modulePaths []string
in string
want string
}{
{
name: "module path is not at latest",
latest: func(context.Context, string) string { return "/v3" },
modulePaths: []string{
"foo.com/bar",
"foo.com/bar/v2",
"foo.com/bar/v3",
},
in: `
<div class="DetailsHeader-banner$$GODISCOVERY_LATESTMAJORCLASS$$">
data-version="v1.0.0" data-mpath="foo.com/bar" data-ppath="foo.com/bar/far" data-pagetype="pkg">
<p>
The latest major version is available at <a href="/$$GODISCOVERY_LATESTMAJORVERSIONURL$$">$$GODISCOVERY_LATESTMAJORVERSIONURL$$</a>
</p>
</div>`,
want: `
<div class="DetailsHeader-banner">
data-version="v1.0.0" data-mpath="foo.com/bar" data-ppath="foo.com/bar/far" data-pagetype="pkg">
<p>
The latest major version is available at <a href="/foo.com/bar/v3">foo.com/bar/v3</a>
</p>
</div>`,
},
{
name: "module path is at latest",
latest: func(context.Context, string) string { return "/v3" },
modulePaths: []string{
"foo.com/bar",
"foo.com/bar/v2",
"foo.com/bar/v3",
},
in: `
<div class="DetailsHeader-banner$$GODISCOVERY_LATESTMAJORCLASS$$">
data-version="v3.0.0" data-mpath="foo.com/bar/v3" data-ppath="foo.com/bar/far" data-pagetype="pkg">
<p>
The latest major version is available at <a href="/$$GODISCOVERY_LATESTMAJORVERSIONURL$$">$$GODISCOVERY_LATESTMAJORVERSIONURL$$</a>
</p>
</div>`,
want: `
<div class="DetailsHeader-banner DetailsHeader-banner--latest">
data-version="v3.0.0" data-mpath="foo.com/bar/v3" data-ppath="foo.com/bar/far" data-pagetype="pkg">
<p>
The latest major version is available at <a href="/foo.com/bar/v3">foo.com/bar/v3</a>
</p>
</div>`,
},
} {
t.Run(test.name, func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, test.in)
})
latestMinor := func(context.Context, string, string, string) string { return "" }
ts := httptest.NewServer(LatestVersions(latestMinor, test.latest)(handler))
defer ts.Close()
resp, err := ts.Client().Get(ts.URL)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/testing/integration/frontend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func setupFrontend(ctx context.Context, t *testing.T, q queue.Queue) *httptest.S
mw := middleware.Chain(
middleware.AcceptRequests(http.MethodGet, http.MethodPost),
middleware.SecureHeaders(),
middleware.LatestVersion(s.LatestVersion),
middleware.LatestVersions(s.GetLatestMinorVersion, s.GetLatestMajorVersion),
middleware.Experiment(experimenter),
)
return httptest.NewServer(mw(mux))
Expand Down

0 comments on commit 6f12538

Please sign in to comment.