commit 44b915c54b41cb972dee0fa8ade12738efb02bfa Author: sneak Date: Sun Jun 2 15:21:39 2024 -0700 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33a9488 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +example diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3ccf9a2 --- /dev/null +++ b/Dockerfile @@ -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!"] + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9042c25 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6b3b7de --- /dev/null +++ b/Makefile @@ -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 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f278d15 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module sneak.berlin/go/hnblogs + +go 1.22.2 diff --git a/hnblogs.go b/hnblogs.go new file mode 100644 index 0000000..ddb93a2 --- /dev/null +++ b/hnblogs.go @@ -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 +} diff --git a/hnblogs_test.go b/hnblogs_test.go new file mode 100644 index 0000000..c95524b --- /dev/null +++ b/hnblogs_test.go @@ -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") + } +}