This commit is contained in:
Jeffrey Paul 2024-06-02 15:21:39 -07:00
commit 44b915c54b
7 changed files with 255 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
example

37
Dockerfile Normal file
View File

@ -0,0 +1,37 @@
# First stage: Use the golangci-lint image to run the linter
FROM golangci/golangci-lint:latest as lint
# Set the Current Working Directory inside the container
WORKDIR /app
# Copy the go.mod file and the rest of the application code
COPY go.mod ./
COPY . .
# Run golangci-lint
RUN golangci-lint run
# Second stage: Use the official Golang image to run tests
FROM golang:1.22 as test
# Set the Current Working Directory inside the container
WORKDIR /app
# Copy the go.mod file and the rest of the application code
COPY go.mod ./
COPY . .
# Run tests
RUN go test -v ./...
# Final stage: Combine the linting and testing stages
FROM golang:1.22 as final
# Ensure that the linting stage succeeded
WORKDIR /app
COPY --from=lint /app .
COPY --from=test /app .
# Set the final CMD to something minimal since we only needed to verify lint and tests during build
CMD ["echo", "Build and tests passed successfully!"]

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

22
Makefile Normal file
View File

@ -0,0 +1,22 @@
# Targets
.PHONY: all run test clean
all: run
example: *.go ./cmd/example/*.go
go build -o $@ ./cmd/example/main.go
run: example
./example
test:
go test -v ./...
clean:
rm -f example
docker:
docker build --progress plain .
lint:
golangci-lint run

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module sneak.berlin/go/hnblogs
go 1.22.2

102
hnblogs.go Normal file
View File

@ -0,0 +1,102 @@
package hnblogs
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"sync"
)
const BlogsURL = "https://raw.githubusercontent.com/surprisetalk/blogs.hn/main/blogs.json"
// 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"`
}
var (
blogs []Blog
fetchError error
once sync.Once
)
// FetchBlogs fetches the list of blogs and memoizes it in RAM.
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)
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
})
return blogs, fetchError
}
// 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
}

77
hnblogs_test.go Normal file
View File

@ -0,0 +1,77 @@
package hnblogs
import (
"testing"
)
func TestFetchBlogs(t *testing.T) {
blogs, err := FetchBlogs()
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(blogs) == 0 {
t.Fatalf("Expected to fetch some blogs, got %d", len(blogs))
}
}
func TestGetBlogs(t *testing.T) {
blogs, err := GetBlogs()
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(blogs) == 0 {
t.Fatalf("Expected to fetch some blogs, got %d", len(blogs))
}
}
func TestRandomBlog(t *testing.T) {
blog, err := RandomBlog()
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if blog.URL == "" {
t.Fatalf("Expected a valid blog, got an empty URL")
}
}
func TestRandomBlogs(t *testing.T) {
n := 5
randomBlogs, err := RandomBlogs(n)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(randomBlogs) != n {
t.Fatalf("Expected %d random blogs, got %d", n, len(randomBlogs))
}
for _, blog := range randomBlogs {
if blog.URL == "" {
t.Fatalf("Expected a valid blog, got an empty URL")
}
}
}
func TestNthBlog(t *testing.T) {
blogs, err := GetBlogs()
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
blog, err := NthBlog(5)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if blog.URL != blogs[5].URL {
t.Fatalf("Expected %s, got %s", blogs[5].URL, blog.URL)
}
_, err = NthBlog(len(blogs))
if err == nil {
t.Fatalf("Expected error for out-of-range index, got none")
}
}