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)) }