From c6d3ba401b7b3ae9b710a8850c6ec5d49c4c1490 Mon Sep 17 00:00:00 2001 From: Tyler Bui-Palsulich <26876514+tbpg@users.noreply.github.com> Date: Wed, 3 Aug 2022 15:27:03 -0400 Subject: [PATCH] feat(godocfx): add friendlyAPIName (#6447) --- internal/godocfx/godocfx_test.go | 67 +++++++++++- internal/godocfx/main.go | 5 +- internal/godocfx/parse.go | 118 +++++++++++++++++---- internal/godocfx/testdata/golden/index.yml | 1 + 4 files changed, 166 insertions(+), 25 deletions(-) diff --git a/internal/godocfx/godocfx_test.go b/internal/godocfx/godocfx_test.go index 6142586362d..3d3064bef61 100644 --- a/internal/godocfx/godocfx_test.go +++ b/internal/godocfx/godocfx_test.go @@ -19,9 +19,12 @@ package main import ( "bytes" + "encoding/json" "flag" "fmt" "io/ioutil" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" @@ -42,9 +45,28 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +func fakeMetaServer() *httptest.Server { + meta := repoMetadata{ + "cloud.google.com/go/storage": repoMetadataItem{ + Description: "Storage API", + }, + "cloud.google.com/iam/apiv1beta1": repoMetadataItem{ + Description: "IAM", + }, + "cloud.google.com/go/cloudbuild/apiv1/v2": repoMetadataItem{ + Description: "Cloud Build API", + }, + } + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(meta) + })) +} + func TestParse(t *testing.T) { mod := "cloud.google.com/go/bigquery" - r, err := parse(mod+"/...", ".", []string{"README.md"}, nil) + metaServer := fakeMetaServer() + defer metaServer.Close() + r, err := parse(mod+"/...", ".", []string{"README.md"}, nil, &friendlyAPINamer{metaURL: metaServer.URL}) if err != nil { t.Fatalf("Parse: %v", err) } @@ -110,7 +132,9 @@ func TestGoldens(t *testing.T) { extraFiles := []string{"README.md"} testPath := "cloud.google.com/go/storage" - r, err := parse(testPath, ".", extraFiles, nil) + metaServer := fakeMetaServer() + defer metaServer.Close() + r, err := parse(testPath, ".", extraFiles, nil, &friendlyAPINamer{metaURL: metaServer.URL}) if err != nil { t.Fatalf("parse: %v", err) } @@ -282,3 +306,42 @@ Deprecated: use Reader.Attrs.Size.`, } } } + +func TestFriendlyAPIName(t *testing.T) { + metaServer := fakeMetaServer() + defer metaServer.Close() + namer := &friendlyAPINamer{metaURL: metaServer.URL} + + tests := []struct { + importPath string + want string + }{ + { + importPath: "cloud.google.com/go/storage", + want: "Storage API", + }, + { + importPath: "cloud.google.com/iam/apiv1beta1", + want: "IAM v1beta1", + }, + { + importPath: "cloud.google.com/go/cloudbuild/apiv1/v2", + want: "Cloud Build API v1", + }, + { + importPath: "not found", + want: "", + }, + } + + for _, test := range tests { + got, err := namer.friendlyAPIName(test.importPath) + if err != nil { + t.Errorf("friendlyAPIName(%q) got err: %v", test.importPath, err) + continue + } + if got != test.want { + t.Errorf("friendlyAPIName(%q) got %q, want %q", test.importPath, got, test.want) + } + } +} diff --git a/internal/godocfx/main.go b/internal/godocfx/main.go index 5e980f8c76b..0f2ef6b613b 100644 --- a/internal/godocfx/main.go +++ b/internal/godocfx/main.go @@ -171,7 +171,10 @@ func process(mod indexEntry, workingDir, outDir string, print bool) error { log.Println("Starting to parse") optionalExtraFiles := []string{} - r, err := parse(mod.Path+"/...", workingDir, optionalExtraFiles, filter) + namer := &friendlyAPINamer{ + metaURL: "https://raw.githubusercontent.com/googleapis/google-cloud-go/main/internal/.repo-metadata-full.json", + } + r, err := parse(mod.Path+"/...", workingDir, optionalExtraFiles, filter, namer) if err != nil { return fmt.Errorf("parse: %v", err) } diff --git a/internal/godocfx/parse.go b/internal/godocfx/parse.go index 56bdede75ab..b6253e82c06 100644 --- a/internal/godocfx/parse.go +++ b/internal/godocfx/parse.go @@ -25,16 +25,19 @@ package main import ( "bytes" + "encoding/json" "fmt" "go/format" "go/printer" "go/token" "log" + "net/http" "os" "path/filepath" "regexp" "sort" "strings" + "sync" goldmarkcodeblock "cloud.google.com/go/internal/godocfx/goldmark-codeblock" "cloud.google.com/go/internal/godocfx/pkgload" @@ -84,18 +87,19 @@ type example struct { // item represents a DocFX item. type item struct { - UID string `yaml:"uid"` - Name string `yaml:"name,omitempty"` - ID string `yaml:"id,omitempty"` - Summary string `yaml:"summary,omitempty"` - Parent string `yaml:"parent,omitempty"` - Type string `yaml:"type,omitempty"` - Langs []string `yaml:"langs,omitempty"` - Syntax syntax `yaml:"syntax,omitempty"` - Examples []example `yaml:"codeexamples,omitempty"` - Children []child `yaml:"children,omitempty"` - AltLink string `yaml:"alt_link,omitempty"` - Status string `yaml:"status,omitempty"` + UID string `yaml:"uid"` + Name string `yaml:"name,omitempty"` + ID string `yaml:"id,omitempty"` + Summary string `yaml:"summary,omitempty"` + Parent string `yaml:"parent,omitempty"` + Type string `yaml:"type,omitempty"` + Langs []string `yaml:"langs,omitempty"` + Syntax syntax `yaml:"syntax,omitempty"` + Examples []example `yaml:"codeexamples,omitempty"` + Children []child `yaml:"children,omitempty"` + AltLink string `yaml:"alt_link,omitempty"` + Status string `yaml:"status,omitempty"` + FriendlyAPIName string `yaml:"friendlyApiName,omitempty"` } func (p *page) addItem(i *item) { @@ -125,7 +129,7 @@ type result struct { // workingDir is the directory to use to run go commands. // // optionalExtraFiles is a list of paths relative to the module root to include. -func parse(glob string, workingDir string, optionalExtraFiles []string, filter []string) (*result, error) { +func parse(glob string, workingDir string, optionalExtraFiles []string, filter []string, namer *friendlyAPINamer) (*result, error) { pages := map[string]*page{} pkgInfos, err := pkgload.Load(glob, workingDir, filter) @@ -162,16 +166,21 @@ func parse(glob string, workingDir string, optionalExtraFiles []string, filter [ for _, pi := range pkgInfos { link := newLinker(pi) topLevelDecls := pkgsite.TopLevelDecls(pi.Doc) + friendly, err := namer.friendlyAPIName(pi.Doc.ImportPath) + if err != nil { + return nil, err + } pkgItem := &item{ - UID: pi.Doc.ImportPath, - Name: pi.Doc.ImportPath, - ID: pi.Doc.Name, - Summary: toHTML(pi.Doc.Doc), - Langs: onlyGo, - Type: "package", - Examples: processExamples(pi.Doc.Examples, pi.Fset), - AltLink: "https://pkg.go.dev/" + pi.Doc.ImportPath, - Status: pi.Status, + UID: pi.Doc.ImportPath, + Name: pi.Doc.ImportPath, + ID: pi.Doc.Name, + Summary: toHTML(pi.Doc.Doc), + Langs: onlyGo, + Type: "package", + Examples: processExamples(pi.Doc.Examples, pi.Fset), + AltLink: "https://pkg.go.dev/" + pi.Doc.ImportPath, + Status: pi.Status, + FriendlyAPIName: friendly, } pkgPage := &page{Items: []*item{pkgItem}} pages[pi.Doc.ImportPath] = pkgPage @@ -640,3 +649,68 @@ func hasPrefix(s string, prefixes []string) bool { } return false } + +// repoMetadata is the JSON format of the .repo-metadata-full.json file. +// See https://raw.githubusercontent.com/googleapis/google-cloud-go/main/internal/.repo-metadata-full.json. +type repoMetadata map[string]repoMetadataItem + +type repoMetadataItem struct { + Description string `json:"description"` +} + +type friendlyAPINamer struct { + // metaURL is the URL to .repo-metadata-full.json, which contains metadata + // about packages in this repo. See + // https://raw.githubusercontent.com/googleapis/google-cloud-go/main/internal/.repo-metadata-full.json. + metaURL string + + // metadata caches the repo metadata results. + metadata repoMetadata + // getOnce ensures we only fetch the metadata JSON once. + getOnce sync.Once +} + +// vNumberRE is a heuristic for API versions. +var vNumberRE = regexp.MustCompile(`apiv[0-9][^/]*`) + +// friendlyAPIName returns the friendlyAPIName for the given import path. +// We rely on the .repo-metadata-full.json file to get the description of the +// API for the given import path. We use the importPath to parse out the +// API version, since that isn't included in the metadata. +// +// If no API description is found, friendlyAPIName returns "". +// If no API version is found, friendlyAPIName only returns the description. +// +// See https://github.com/googleapis/google-cloud-go/issues/5949. +func (d *friendlyAPINamer) friendlyAPIName(importPath string) (string, error) { + var err error + d.getOnce.Do(func() { + resp, getErr := http.Get(d.metaURL) + if err != nil { + err = fmt.Errorf("error getting repo metadata: %v", getErr) + return + } + defer resp.Body.Close() + if decodeErr := json.NewDecoder(resp.Body).Decode(&d.metadata); err != nil { + err = fmt.Errorf("failed to decode repo metadata: %v", decodeErr) + return + } + }) + if err != nil { + return "", err + } + if d.metadata == nil { + return "", fmt.Errorf("no metadata found: earlier error fetching?") + } + pkg, ok := d.metadata[importPath] + if !ok { + return "", nil + } + + if apiV := vNumberRE.FindString(importPath); apiV != "" { + version := strings.TrimPrefix(apiV, "api") + return fmt.Sprintf("%s %s", pkg.Description, version), nil + } + + return pkg.Description, nil +} diff --git a/internal/godocfx/testdata/golden/index.yml b/internal/godocfx/testdata/golden/index.yml index 742c0941939..8f24e632b44 100644 --- a/internal/godocfx/testdata/golden/index.yml +++ b/internal/godocfx/testdata/golden/index.yml @@ -288,6 +288,7 @@ items: - cloud.google.com/go/storage.Writer.Write - cloud.google.com/go/storage.SignedURL alt_link: https://pkg.go.dev/cloud.google.com/go/storage + friendlyApiName: Storage API - uid: cloud.google.com/go/storage.DeleteAction,SetStorageClassAction,AbortIncompleteMPUAction name: DeleteAction, SetStorageClassAction, AbortIncompleteMPUAction id: DeleteAction,SetStorageClassAction,AbortIncompleteMPUAction