initial
This commit is contained in:
commit
44b915c54b
|
@ -0,0 +1 @@
|
||||||
|
example
|
|
@ -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!"]
|
||||||
|
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue