Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pkgsite

Go Reference

A small, idiomatic Go client for the pkg.go.dev API. It gives Go tooling a typed interface to module and package metadata — search, version history, package and symbol listings, importers, and vulnerability data — instead of scraping the website.

⚠️ API stability: v1beta

This client targets the pkg.go.dev API at https://pkg.go.dev/v1beta/. The API is explicitly labelled v1beta: a stable v1 is planned but not yet released, and response shapes may change before then. Pin a version of this module and expect to update when the upstream API moves to v1.

Install

go get github.com/pouya1364/pkgsite

Requires Go 1.23 or later (the iterator methods use iter.Seq2).

Zero runtime dependencies

The library uses only the Go standard library. Running go get on it pulls in no transitive dependencies.

Quick start

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/pouya1364/pkgsite"
)

func main() {
	client := pkgsite.NewClient()

	pkg, err := client.Package(context.Background(), "golang.org/x/time/rate")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(pkg.Name, pkg.Synopsis)
}

NewClient accepts options: WithHTTPClient to supply your own *http.Client (for custom timeouts or transport), and WithBaseURL to point at a different API host (useful in tests).

The eight endpoints

Each example assumes a client := pkgsite.NewClient() and ctx.

Package — metadata for one package

pkg, err := client.Package(ctx, "golang.org/x/time/rate")
// pkg.ModulePath, pkg.Version, pkg.Name, pkg.Synopsis, ...

Module — metadata for one module

mod, err := client.Module(ctx, "golang.org/x/time")
// mod.Path, mod.Version, mod.IsLatest, mod.RepoURL, ...

Versions — a module's version history (paginated)

page, err := client.Versions(ctx, "golang.org/x/time")
for _, v := range page.Items {
	fmt.Println(v.Version, "latest:", v.LatestVersion)
}

ModulePackages — the packages in a module (paginated)

page, err := client.ModulePackages(ctx, "golang.org/x/time")
for _, p := range page.Items {
	fmt.Println(p.Path, p.Synopsis)
}

Search — ranked search results (paginated)

page, err := client.Search(ctx, "rate limiter", pkgsite.SearchOptions{
	Symbol: "Limiter", // optional: narrow to packages exporting this symbol
})
for _, r := range page.Items {
	fmt.Println(r.PackagePath)
}

Symbols — exported symbols in a package (paginated)

page, err := client.Symbols(ctx, "golang.org/x/time/rate")
for _, s := range page.Items {
	fmt.Println(s.Kind, s.Name)
}

ImportedBy — packages that import a package (paginated)

page, err := client.ImportedBy(ctx, "golang.org/x/time/rate")
for _, importPath := range page.Items { // items are import path strings
	fmt.Println(importPath)
}

Vulnerabilities — known vulnerabilities for a module or package

page, err := client.Vulnerabilities(ctx, "github.com/dgrijalva/jwt-go")
for _, v := range page.Items {
	fmt.Println(v.ID, v.Details, "fixed in:", v.FixedVersion)
}

Pagination and iterators

Paginated endpoints return a Page[T]:

type Page[T any] struct {
	Items         []T
	Total         int
	NextPageToken string
}

func (p Page[T]) HasMore() bool // true when another page exists

You can page manually with the Token option:

page, _ := client.Search(ctx, "bloom filter")
for page.HasMore() {
	page, _ = client.Search(ctx, "bloom filter", pkgsite.SearchOptions{
		ListOptions: pkgsite.ListOptions{Token: page.NextPageToken},
	})
}

Or let the iterator handle paging for you. Every paginated endpoint has an …Iter variant returning a Go 1.23 iter.Seq2[T, error], so you can range over all results across all pages:

for result, err := range client.SearchIter(ctx, "bloom filter") {
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result.PackagePath)
}

The iterators are VersionsIter, ModulePackagesIter, SearchIter, SymbolsIter, and ImportedByIter. Breaking out of the loop stops cleanly without fetching further pages.

Filters

The paginated list options accept a Filter — a Go expression the API evaluates against each item. The library URL-encodes it for you:

page, err := client.ModulePackages(ctx, "golang.org/x/tools", pkgsite.ListOptions{
	Filter: `contains(path,"internal")`,
})

Errors

Failures come back as typed errors you can inspect with errors.As:

_, err := client.Package(ctx, "encoding/json/v2")
switch {
case err == nil:
	// ok
default:
	var ambiguous *pkgsite.ErrAmbiguousPath
	var rateLimited *pkgsite.ErrRateLimit
	var apiErr *pkgsite.APIError
	switch {
	case errors.As(err, &ambiguous):
		// The path matches more than one module. Retry with the Module option
		// set to one of ambiguous.Candidates.
		_, _ = client.Package(ctx, "encoding/json/v2", pkgsite.PackageOptions{
			Module: ambiguous.Candidates[0],
		})
	case errors.As(err, &rateLimited):
		// HTTP 429. rateLimited.RetryAfter is the Unix timestamp when the
		// limit resets. The API allows 40 requests/sec per IP.
	case errors.As(err, &apiErr):
		// Any other non-2xx response. apiErr.Code, apiErr.Message, apiErr.Fixes.
	}
}

Testing with the mock package

pkgsite.Client is a concrete type. To test your own code without real HTTP calls, declare an interface in your package listing the methods you use, accept that interface, and substitute *mock.Client in tests. Both *pkgsite.Client and *mock.Client satisfy such an interface.

package myapp

import (
	"context"

	"github.com/pouya1364/pkgsite"
)

// The slice of the API this code depends on.
type PackageGetter interface {
	Package(ctx context.Context, path string, opts ...pkgsite.PackageOptions) (*pkgsite.PackageInfo, error)
}

func Synopsis(ctx context.Context, c PackageGetter, path string) (string, error) {
	pkg, err := c.Package(ctx, path)
	if err != nil {
		return "", err
	}
	return pkg.Synopsis, nil
}
package myapp_test

import (
	"context"
	"testing"

	"github.com/pouya1364/pkgsite"
	"github.com/pouya1364/pkgsite/mock"
)

func TestSynopsis(t *testing.T) {
	c := &mock.Client{
		PackageFn: func(ctx context.Context, path string, opts ...pkgsite.PackageOptions) (*pkgsite.PackageInfo, error) {
			return &pkgsite.PackageInfo{Synopsis: "Package rate provides a rate limiter."}, nil
		},
	}

	got, err := Synopsis(context.Background(), c, "golang.org/x/time/rate")
	if err != nil || got != "Package rate provides a rate limiter." {
		t.Fatalf("got %q, %v", got, err)
	}
}

Each method on mock.Client has a matching …Fn field. A method whose field is left nil returns the zero value and a nil error.

Integration tests

This repository's own tests use net/http/httptest and make no network calls. A separate set of integration tests exercises the live API; they are guarded by the integration build tag and the PKGSITE_INTEGRATION environment variable, so they never run by accident:

PKGSITE_INTEGRATION=1 go test -tags integration -run TestIntegration ./...

License

MIT

About

A Go client for the pkg.go.dev API (v1beta).

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages