Files
hnblogs/hnblogs.go
sneak f84378c426 embed blogs.json instead of fetching it at runtime (closes #1)
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 no net/* import returns to non-test code.

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, replaces blogs.json only on a complete
download, and runs the test suite against the new data.

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
2026-09-05 03:08:25 +00:00

109 lines
2.6 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".
//
//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
}