The dataset is vendored as blogs.json and compiled in with go:embed, so the package does no network I/O on any code path. FetchBlogs is removed rather than kept as a no-op wrapper, since nothing is fetched any more: GetBlogs decodes the embedded bytes on first call and memoizes the result, and RandomBlog, RandomBlogs and NthBlog go through it. Callers of FetchBlogs have to switch to GetBlogs. make update-data refreshes the vendored copy from the BlogsURL constant, downloading to a temporary file and replacing blogs.json only once that file parses as a non-empty JSON array of blog entries, so a complete-but-wrong response cannot overwrite the good dataset. A test walks the dependency graph of the non-test build and fails if any net/* package is reachable. Model: opus-5
104 lines
2.5 KiB
Go
104 lines
2.5 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. Nothing reads it at runtime;
|
|
// it documents where "make update-data" downloads 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"`
|
|
}
|
|
|
|
// GetBlogs returns the embedded list of blogs, decoding it on first call and
|
|
// memoizing the result for subsequent calls.
|
|
//
|
|
// 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 GetBlogs() ([]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
|
|
}
|
|
|
|
// 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
|
|
}
|