Files
age-api/main.go
T
unkinben d45111645c
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
Initial commit
- REST API for calculating age breakdowns (years, months, weeks, days, hours, minutes, seconds)
- Birthtime configured as Unix timestamps
- Sleeps until next birthday countdown
- Per-person lookup via GET /age/{name}
- Docker and Makefile build support
- Woodpecker CI pipelines
2026-06-27 23:59:00 +10:00

157 lines
3.8 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"gopkg.in/yaml.v3"
)
type Config struct {
People []Person `yaml:"people"`
}
type Person struct {
Name string `yaml:"name"`
Birthtime int64 `yaml:"birthtime"`
}
type AgeBreakdown struct {
Years int `json:"years"`
Months int `json:"months"`
Weeks int `json:"weeks"`
Days int `json:"days"`
Hours int64 `json:"hours"`
Minutes int64 `json:"minutes"`
Seconds int64 `json:"seconds"`
}
type PersonAge struct {
Name string `json:"name"`
DOB string `json:"dob"`
Birthtime int64 `json:"birthtime"`
SleepsUntilBirthday int `json:"sleeps_until_birthday"`
Age AgeBreakdown `json:"age"`
}
func sleepsUntilBirthday(dob time.Time, now time.Time) int {
next := time.Date(now.Year(), dob.Month(), dob.Day(), 0, 0, 0, 0, now.Location())
if !next.After(now) {
next = time.Date(now.Year()+1, dob.Month(), dob.Day(), 0, 0, 0, 0, now.Location())
}
return int(next.Sub(now).Hours()/24) + 1
}
func calculateAge(dob time.Time, now time.Time) AgeBreakdown {
// Calendar-based years and months
years := now.Year() - dob.Year()
months := int(now.Month()) - int(dob.Month())
if now.Day() < dob.Day() {
months--
}
if months < 0 {
years--
months += 12
}
totalMonths := years*12 + months
// Total duration for weeks/days/hours/minutes/seconds
dur := now.Sub(dob)
totalDays := int(dur.Hours() / 24)
totalWeeks := totalDays / 7
return AgeBreakdown{
Years: years,
Months: totalMonths,
Weeks: totalWeeks,
Days: totalDays,
Hours: int64(dur.Hours()),
Minutes: int64(dur.Minutes()),
Seconds: int64(dur.Seconds()),
}
}
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config: %w", err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
return &cfg, nil
}
func main() {
configPath := "config.yaml"
if v := os.Getenv("CONFIG_PATH"); v != "" {
configPath = v
}
cfg, err := loadConfig(configPath)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Index people by lowercase name
people := make(map[string]Person)
for _, p := range cfg.People {
people[strings.ToLower(p.Name)] = p
}
mux := http.NewServeMux()
// GET /age - all people
mux.HandleFunc("GET /age", func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
var results []PersonAge
for _, p := range cfg.People {
dob := time.Unix(p.Birthtime, 0)
results = append(results, PersonAge{
Name: p.Name,
DOB: dob.Format("2006-01-02"),
Birthtime: p.Birthtime,
SleepsUntilBirthday: sleepsUntilBirthday(dob, now),
Age: calculateAge(dob, now),
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(results)
})
// GET /age/{name} - single person
mux.HandleFunc("GET /age/{name}", func(w http.ResponseWriter, r *http.Request) {
name := strings.ToLower(r.PathValue("name"))
p, ok := people[name]
if !ok {
http.Error(w, fmt.Sprintf("person %q not found", name), http.StatusNotFound)
return
}
now := time.Now()
dob := time.Unix(p.Birthtime, 0)
result := PersonAge{
Name: p.Name,
DOB: dob.Format("2006-01-02"),
Birthtime: p.Birthtime,
SleepsUntilBirthday: sleepsUntilBirthday(dob, now),
Age: calculateAge(dob, now),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
})
addr := ":8080"
if v := os.Getenv("LISTEN_ADDR"); v != "" {
addr = v
}
log.Printf("Listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}