embed blogs.json instead of fetching it at runtime (closes #1)

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
This commit is contained in:
2026-09-05 03:08:25 +00:00
parent 44b915c54b
commit 7e1b010c4f
6 changed files with 248449 additions and 45 deletions

View File

@@ -1,15 +1,34 @@
// 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"
"net/http"
"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"`
@@ -20,42 +39,24 @@ type Blog struct {
Desc string `json:"desc"`
}
var (
blogs []Blog
fetchError error
once sync.Once
)
// FetchBlogs fetches the list of blogs and memoizes it in RAM.
func FetchBlogs() ([]Blog, error) {
// 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() {
resp, err := http.Get(BlogsURL)
if err != nil {
fetchError = fmt.Errorf("failed to fetch blogs: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fetchError = fmt.Errorf("failed to fetch blogs: status code %d", resp.StatusCode)
var decoded []Blog
if err := json.Unmarshal(blogsJSON, &decoded); err != nil {
loadError = fmt.Errorf("failed to decode embedded blogs JSON: %w", err)
return
}
var fetchedBlogs []Blog
if err := json.NewDecoder(resp.Body).Decode(&fetchedBlogs); err != nil {
fetchError = fmt.Errorf("failed to decode blogs JSON: %v", err)
return
}
blogs = fetchedBlogs
blogs = decoded
})
return blogs, fetchError
}
// GetBlogs returns the memoized list of blogs.
func GetBlogs() ([]Blog, error) {
return FetchBlogs()
return blogs, loadError
}
// RandomBlog returns a random blog from the list of blogs.