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
This commit is contained in:
56
hnblogs.go
56
hnblogs.go
@@ -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. 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"`
|
||||
@@ -20,37 +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.
|
||||
// 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() {
|
||||
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
|
||||
return blogs, loadError
|
||||
}
|
||||
|
||||
// GetBlogs returns the memoized list of blogs.
|
||||
|
||||
Reference in New Issue
Block a user