Table of Contents

Description

Uses the Hex.pm API to fetch information about a user's packages formatted using a template:

Usage

go run src/packages.go --template=packages/packages.md.template <username>

Code

package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path"
	"strings"
	"text/template"
	"time"
)

const (
	version = "0.1.0"
)

type State struct {
	user     string
	template string
	file     *os.File
	timeout  time.Duration
	verbose  int
}

type User struct {
	Email    string `json:"email"`
	FullName string `json:"full_name"`
	Handles  struct {
		GitHub string `json:"GitHub"`
	} `json:"handles"`
	InsertedAt    time.Time         `json:"inserted_at"`
	OwnedPackages map[string]string `json:"owned_packages"`
	Packages      []struct {
		HTMLURL    string `json:"html_url"`
		Name       string `json:"name"`
		Repository string `json:"repository"`
		URL        string `json:"url"`
	} `json:"packages"`
	UpdatedAt time.Time `json:"updated_at"`
	URL       string    `json:"url"`
	Username  string    `json:"username"`
}

type Package struct {
	Configs struct {
		ErlangMk    string `json:"erlang.mk"`
		MixExs      string `json:"mix.exs"`
		RebarConfig string `json:"rebar.config"`
	} `json:"configs"`
	DocsHTMLURL string `json:"docs_html_url"`
	Downloads   struct {
		All    int `json:"all"`
		Day    int `json:"day"`
		Recent int `json:"recent"`
		Week   int `json:"week"`
	} `json:"downloads"`
	HTMLURL             string    `json:"html_url"`
	InsertedAt          time.Time `json:"inserted_at"`
	LatestStableVersion string    `json:"latest_stable_version"`
	LatestVersion       string    `json:"latest_version"`
	Meta                struct {
		Description string   `json:"description"`
		Licenses    []string `json:"licenses"`
		Links       struct {
			Github string `json:"Github"`
		} `json:"links"`
		Maintainers []string `json:"maintainers"`
	} `json:"meta"`
	Name   string `json:"name"`
	Owners []struct {
		Email    string `json:"email"`
		URL      string `json:"url"`
		Username string `json:"username"`
	} `json:"owners"`
	Releases []struct {
		HasDocs    bool      `json:"has_docs"`
		InsertedAt time.Time `json:"inserted_at"`
		URL        string    `json:"url"`
		Version    string    `json:"version"`
	} `json:"releases"`
	Repository  string    `json:"repository"`
	Retirements struct{}  `json:"retirements"`
	UpdatedAt   time.Time `json:"updated_at"`
	URL         string    `json:"url"`
}

func args() *State {
	flag.Usage = func() {
		_, _ = fmt.Fprintf(os.Stderr, `%s v%s
Usage: %s [<option>] <user>

`, path.Base(os.Args[0]), version, os.Args[0])
		flag.PrintDefaults()
	}

	filePath := flag.String("file", "",
		"Write output to file")
	timeout := flag.Duration("timeout", 1*time.Minute,
		"HTTP request timeout")
	template := flag.String("template", "packages.md.template",
		"Path to template file")
	verbose := flag.Int("verbose", 0,
		"Enable debug messages")

	flag.Parse()

	if flag.NArg() == 0 {
		flag.Usage()
		os.Exit(1)
	}

	user := flag.Arg(0)

	content, err := os.ReadFile(*template)
	if err != nil {
		flag.Usage()
		os.Exit(1)
	}

	file := os.Stdout
	if *filePath != "" {
		file, err = os.OpenFile(
			*filePath,
			os.O_WRONLY|os.O_TRUNC|os.O_CREATE,
			os.ModePerm,
		)
		if err != nil {
			flag.Usage()
			os.Exit(1)
		}
	}

	return &State{
		user:     user,
		file:     file,
		template: string(content),
		timeout:  *timeout,
		verbose:  *verbose,
	}
}

func main() {
	state := args()

	users, err := state.users()
	if err != nil {
		log.Fatalf("%s: %+v", err, state.user)
	}

	pkgs := make([]Package, 0, len(users.Packages))

	for _, pkg := range users.Packages {
		j, err := state.packages(pkg.URL)
		if err != nil {
			log.Fatalf("%s: %+v", err, pkg)
		}
		pkgs = append(pkgs, *j)
	}

	if err := state.render(pkgs); err != nil {
		log.Fatalf("%s: %+v", err, pkgs)
	}
}

func (state *State) users() (*User, error) {
	uri := fmt.Sprintf("https://hex.pm/api/users/%s", state.user)

	body, err := state.request(uri)
	if err != nil {
		return nil, err
	}

	users := &User{}

	if err := json.Unmarshal(body, users); err != nil {
		return nil, err
	}

	if state.verbose > 1 {
		log.Printf("%+v\n", users)
	}

	return users, nil
}

func (state *State) packages(uri string) (*Package, error) {
	body, err := state.request(uri)
	if err != nil {
		return nil, err
	}

	packages := &Package{}

	if err := json.Unmarshal(body, packages); err != nil {
		return nil, err
	}

	if state.verbose > 1 {
		log.Printf("%+v\n", packages)
	}

	return packages, nil
}

func (state *State) request(uri string) ([]byte, error) {
	if state.verbose > 0 {
		log.Println(uri)
	}

	req, err := http.NewRequest(http.MethodGet, uri, nil)
	if err != nil {
		log.Fatal(err)
	}

	ctx, cancel := context.WithTimeout(
		context.Background(),
		state.timeout,
	)

	defer cancel()

	req = req.WithContext(ctx)
	c := &http.Client{}
	res, err := c.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	return io.ReadAll(res.Body)
}

func (state *State) render(pkg []Package) error {
	funcMap := template.FuncMap{
		"strip": strip,
	}
	tmpl, err := template.New("template").Funcs(funcMap).Parse(state.template)
	if err != nil {
		return err
	}

	if err := tmpl.Execute(state.file, pkg); err != nil {
		return err
	}

	return nil
}

func strip(s string) string {
	return strings.ReplaceAll(s, "\n", " ")
}

(markdown)