The library needed network access on first use and its results changed under the caller between runs. blogs.json is now vendored and compiled in with go:embed, so the dataset is fixed for a given build. FetchBlogs keeps its name, signature and sync.Once memoization but now decodes the embedded bytes; its error return is only reachable if the committed blogs.json is malformed. net/http is gone from the package, and a test asserts that no net/* package appears anywhere in the dependency graph of the non-test build, not only in its direct imports. make update-data refreshes the vendored file, reading the upstream location from the BlogsURL constant so the URL has one definition. It downloads to a temporary file and replaces blogs.json only once that file parses as a non-empty JSON array of blog entries, so neither a truncated transfer nor a complete-but-wrong response such as an error page can overwrite the good dataset. It then runs the test suite against the new data. That target now needs jq as well as curl. blogs.json is an unmodified copy of a third party's file, redistributed here and in every binary that links the package, and the upstream repository publishes no licence. The README and the embed doc comment now record where it came from and that this repository's LICENSE does not extend to it. Whether that arrangement is acceptable is the owner's call. The dataset is committed verbatim as upstream serves it, which is ~8 MB of JSON in the repo and in every linking binary; most of that is per-blog post history that the Blog struct does not expose. Model: opus-5
111 lines
2.8 KiB
Go
111 lines
2.8 KiB
Go
// Package hnblogs provides access to the blogs.hn dataset.
|
|
//
|
|
// The dataset is vendored into this repository as blogs.json and compiled into
|
|
// the package with go:embed, so nothing here touches the network at runtime and
|
|
// the results are stable for a given version of the module. Refresh the
|
|
// vendored copy with "make update-data" and commit the result.
|
|
package hnblogs
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"sync"
|
|
)
|
|
|
|
// BlogsURL is the upstream source of blogs.json. It is not fetched at runtime;
|
|
// it documents where "make update-data" pulls the vendored copy from.
|
|
const BlogsURL = "https://raw.githubusercontent.com/surprisetalk/blogs.hn/main/blogs.json"
|
|
|
|
// blogsJSON is the vendored dataset, refreshed by "make update-data". It is an
|
|
// unmodified copy of a third party's file, not this project's work; see "Where
|
|
// blogs.json came from" in README.md.
|
|
//
|
|
//go:embed blogs.json
|
|
var blogsJSON []byte
|
|
|
|
var (
|
|
blogs []Blog
|
|
loadError error
|
|
once sync.Once
|
|
)
|
|
|
|
// Blog represents a single blog entry.
|
|
type Blog struct {
|
|
URL string `json:"url"`
|
|
Title string `json:"title"`
|
|
About string `json:"about"`
|
|
Now string `json:"now"`
|
|
Feed string `json:"feed"`
|
|
Desc string `json:"desc"`
|
|
}
|
|
|
|
// FetchBlogs returns the embedded list of blogs, decoding it on first call and
|
|
// memoizing the result for subsequent calls.
|
|
//
|
|
// Despite the name it performs no I/O: the data is compiled into the binary, so
|
|
// the only error it can return is a malformed embedded blogs.json, which would
|
|
// mean the committed dataset is broken.
|
|
func FetchBlogs() ([]Blog, error) {
|
|
once.Do(func() {
|
|
var decoded []Blog
|
|
if err := json.Unmarshal(blogsJSON, &decoded); err != nil {
|
|
loadError = fmt.Errorf("failed to decode embedded blogs JSON: %w", err)
|
|
return
|
|
}
|
|
|
|
blogs = decoded
|
|
})
|
|
|
|
return blogs, loadError
|
|
}
|
|
|
|
// GetBlogs returns the memoized list of blogs.
|
|
func GetBlogs() ([]Blog, error) {
|
|
return FetchBlogs()
|
|
}
|
|
|
|
// RandomBlog returns a random blog from the list of blogs.
|
|
func RandomBlog() (Blog, error) {
|
|
blogs, err := GetBlogs()
|
|
if err != nil {
|
|
return Blog{}, err
|
|
}
|
|
|
|
return blogs[rand.Intn(len(blogs))], nil
|
|
}
|
|
|
|
// RandomBlogs returns n random blogs from the list of blogs.
|
|
func RandomBlogs(n int) ([]Blog, error) {
|
|
blogs, err := GetBlogs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if n <= 0 || n > len(blogs) {
|
|
return nil, fmt.Errorf("invalid number of blogs requested")
|
|
}
|
|
|
|
selected := make([]Blog, n)
|
|
for i := range selected {
|
|
selected[i] = blogs[rand.Intn(len(blogs))]
|
|
}
|
|
|
|
return selected, nil
|
|
}
|
|
|
|
// NthBlog returns the nth blog from the list of blogs.
|
|
func NthBlog(n int) (Blog, error) {
|
|
blogs, err := GetBlogs()
|
|
if err != nil {
|
|
return Blog{}, err
|
|
}
|
|
|
|
if n < 0 || n >= len(blogs) {
|
|
return Blog{}, fmt.Errorf("index out of range")
|
|
}
|
|
|
|
return blogs[n], nil
|
|
}
|