major refactor of main.go
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
APIKey string
|
||||||
|
DefaultCity string
|
||||||
|
DBHost string
|
||||||
|
DBPort string
|
||||||
|
DBUser string
|
||||||
|
DBPassword string
|
||||||
|
DBName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEnv(key string) string {
|
||||||
|
val := os.Getenv(key)
|
||||||
|
if val == "" {
|
||||||
|
log.Fatalf("missing required env var: %s", key)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() *Config {
|
||||||
|
return &Config{
|
||||||
|
APIKey: GetEnv("OPENWEATHERMAP_API_KEY"),
|
||||||
|
DefaultCity: GetEnv("WEATHER_CITY"),
|
||||||
|
DBHost: GetEnv("DB_HOST"),
|
||||||
|
DBPort: GetEnv("DB_PORT"),
|
||||||
|
DBUser: GetEnv("DB_USER"),
|
||||||
|
DBPassword: GetEnv("DB_PASSWORD"),
|
||||||
|
DBName: GetEnv("DB_NAME"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module gitea.coupxd.com/cody/go-weather
|
module go-weather
|
||||||
|
|
||||||
go 1.25.6
|
go 1.25.6
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"go-weather/models"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Health(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(models.HealthResponse{Status: "ok"})
|
||||||
|
}
|
||||||
|
func LatestTemperature(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(models.LatestTemperatureResponse{Temp: 69})
|
||||||
|
}
|
||||||
|
func Test(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(models.TestResponse{Status: "hello from go"})
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"go-weather/services"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WeatherHandler struct {
|
||||||
|
weatherService *services.WeatherService
|
||||||
|
dbService *services.DatabaseService
|
||||||
|
config struct {
|
||||||
|
DefaultCity string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWeatherHandler(ws *services.WeatherService, ds *services.DatabaseService, defaultCity string) *WeatherHandler {
|
||||||
|
return &WeatherHandler{
|
||||||
|
weatherService: ws,
|
||||||
|
dbService: ds,
|
||||||
|
config: struct{ DefaultCity string }{DefaultCity: defaultCity},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *WeatherHandler) FetchWeather(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
city := r.URL.Query().Get("city")
|
||||||
|
if city == "" {
|
||||||
|
city = h.config.DefaultCity
|
||||||
|
}
|
||||||
|
weather, err := h.weatherService.FetchWeather(city)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := h.dbService.SaveReading(weather)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"message": "weather fetched and saved",
|
||||||
|
"id": id,
|
||||||
|
"city": weather.Name,
|
||||||
|
"temp": weather.Main.Temp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *WeatherHandler) GetHistory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
city := r.URL.Query().Get("city")
|
||||||
|
if city == "" {
|
||||||
|
city = h.config.DefaultCity
|
||||||
|
}
|
||||||
|
readings, err := h.dbService.GetReadings(city, 25)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(readings)
|
||||||
|
}
|
||||||
@@ -2,332 +2,94 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"go-weather/config"
|
||||||
|
"go-weather/handlers"
|
||||||
|
"go-weather/services"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type WeatherResponse struct {
|
func startPoller(db *sql.DB, ws *services.WeatherService, defaultCity string, interval time.Duration) {
|
||||||
Name string `json:"name"`
|
// Use the service
|
||||||
Main struct {
|
weather, err := ws.FetchWeather(defaultCity)
|
||||||
Temp float64 `json:"temp"`
|
|
||||||
FeelsLike float64 `json:"feels_like"`
|
|
||||||
Humidity int `json:"humidity"`
|
|
||||||
}
|
|
||||||
|
|
||||||
Weather []struct {
|
|
||||||
Description string `json:"description"`
|
|
||||||
} `json:"weather"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Reading struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
City string `json:"city"`
|
|
||||||
Temp float64 `json:"temp"`
|
|
||||||
FeelsLike float64 `json:"feels_like"`
|
|
||||||
Humidity int `json:"humidity"`
|
|
||||||
Conditions string `json:"conditions"`
|
|
||||||
FetchedAt time.Time `json:"fetched_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HealthResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TestResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LatestTemperatureResponse struct {
|
|
||||||
Temp int `json:"temp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func getEnv(key string) string {
|
|
||||||
val := os.Getenv(key)
|
|
||||||
if val == "" {
|
|
||||||
log.Fatalf("missing required env var: %s", key)
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
func connectDB() *sql.DB {
|
|
||||||
connStr := fmt.Sprintf(
|
|
||||||
"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
|
||||||
getEnv("DB_HOST"),
|
|
||||||
getEnv("DB_PORT"),
|
|
||||||
getEnv("DB_USER"),
|
|
||||||
getEnv("DB_PASSWORD"),
|
|
||||||
getEnv("DB_NAME"),
|
|
||||||
)
|
|
||||||
|
|
||||||
db, err := sql.Open("postgres", connStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to connect to db: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.Ping()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to connect to db: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("connected to PostgreSQL")
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTable(db *sql.DB) {
|
|
||||||
_, err := db.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS weather_readings (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
city TEXT NOT NULL,
|
|
||||||
temp DOUBLE PRECISION NOT NULL,
|
|
||||||
feels_like DOUBLE PRECISION NOT NULL,
|
|
||||||
humidity INTEGER NOT NULL,
|
|
||||||
conditions TEXT NOT NULL,
|
|
||||||
fetched_at TIMESTAMP WITH TIME ZONE NOT NULL
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to create table: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println("weather_readings table ready")
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveReading(db *sql.DB, weather WeatherResponse) (int, error) {
|
|
||||||
conditions := ""
|
|
||||||
if len(weather.Weather) > 0 {
|
|
||||||
conditions = weather.Weather[0].Description
|
|
||||||
}
|
|
||||||
|
|
||||||
var id int
|
|
||||||
err := db.QueryRow(
|
|
||||||
`INSERT INTO weather_readings (city, temp, feels_like, humidity, conditions, fetched_at)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
|
||||||
RETURNING id`,
|
|
||||||
weather.Name,
|
|
||||||
weather.Main.Temp,
|
|
||||||
weather.Main.FeelsLike,
|
|
||||||
weather.Main.Humidity,
|
|
||||||
conditions,
|
|
||||||
time.Now(),
|
|
||||||
).Scan(&id)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to save reading: %w", err)
|
|
||||||
}
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getReadings(db *sql.DB, city string, limit int) ([]Reading, error) {
|
|
||||||
rows, err := db.Query(
|
|
||||||
`SELECT id, city, temp, feels_like, humidity, conditions, fetched_at
|
|
||||||
FROM weather_readings
|
|
||||||
WHERE city ILIKE $1
|
|
||||||
ORDER BY fetched_at DESC
|
|
||||||
LIMIT $2`,
|
|
||||||
city, limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Query failed: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var readings []Reading
|
|
||||||
for rows.Next() {
|
|
||||||
var r Reading
|
|
||||||
err := rows.Scan(&r.ID, &r.City, &r.Temp, &r.FeelsLike, &r.Humidity, &r.Conditions, &r.FetchedAt)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("scan failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
readings = append(readings, r)
|
|
||||||
}
|
|
||||||
return readings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchWeather(apiKey, city string) (WeatherResponse, error) {
|
|
||||||
url := fmt.Sprintf(
|
|
||||||
"https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=imperial",
|
|
||||||
city, apiKey,
|
|
||||||
)
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return WeatherResponse{}, fmt.Errorf("request failed: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return WeatherResponse{}, fmt.Errorf("API returned status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var weather WeatherResponse
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&weather)
|
|
||||||
if err != nil {
|
|
||||||
return WeatherResponse{}, fmt.Errorf("failed to parse response: %w", err)
|
|
||||||
}
|
|
||||||
return weather, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func startPoller(db *sql.DB, apiKey, city string, interval time.Duration) {
|
|
||||||
weather, err := fetchWeather(apiKey, city)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("poller: fetch failed: %v", err)
|
log.Printf("poller: fetch failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
id, err := saveReading(db, weather)
|
ds := services.NewDatabaseService(db)
|
||||||
|
id, err := ds.SaveReading(weather)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("poller: save failed %v", err)
|
log.Printf("poller: save failed %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("poller: saved reading #%d for %s (%.1f F)", id, weather.Name, weather.Main.Temp)
|
log.Printf("poller: saved reading #%d for %s (%.1f F)", id, weather.Name, weather.Main.Temp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(interval)
|
ticker := time.NewTicker(interval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
weather, err := fetchWeather(apiKey, city)
|
weather, err := ws.FetchWeather(defaultCity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("poller fetch failed: %v", err)
|
log.Printf("poller fetch failed: %v", err)
|
||||||
continue // don't crash
|
continue
|
||||||
}
|
}
|
||||||
|
ds := services.NewDatabaseService(db)
|
||||||
id, err := saveReading(db, weather)
|
id, err := ds.SaveReading(weather)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("poller: save failed: %v", err)
|
log.Printf("poller: save failed: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("poller: saved reading #%d for %s (%.1f F)", id, weather.Name, weather.Main.Temp)
|
log.Printf("poller: saved reading #%d for %s (%.1f F)", id, weather.Name, weather.Main.Temp)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
apiKey := getEnv("OPENWEATHERMAP_API_KEY")
|
config := config.Load()
|
||||||
defaultCity := getEnv("WEATHER_CITY")
|
// Connect to DB
|
||||||
|
connStr := fmt.Sprintf(
|
||||||
|
"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
config.DBHost, config.DBPort, config.DBUser, config.DBPassword, config.DBName,
|
||||||
|
)
|
||||||
|
|
||||||
db := connectDB()
|
database, err := services.ConnectDB(connStr)
|
||||||
defer db.Close()
|
if err != nil {
|
||||||
|
log.Fatalf("failed to connect to db: %v", err)
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
databaseService := services.NewDatabaseService(database)
|
||||||
|
|
||||||
createTable(db)
|
// Setup weather_readings table
|
||||||
|
if err := databaseService.CreateTable(); err != nil {
|
||||||
|
log.Fatalf("failed to create table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Start background polling in a goroutine
|
// Create services
|
||||||
go startPoller(db, apiKey, defaultCity, 10*time.Minute)
|
weatherService := services.NewWeatherService(config.APIKey)
|
||||||
|
|
||||||
// POST /weather/fetch?city=London - fetch current weather and save it
|
// Create route handlers
|
||||||
http.HandleFunc("/weather/fetch", func(w http.ResponseWriter, r *http.Request) {
|
weatherHandler := handlers.NewWeatherHandler(weatherService, databaseService, config.DefaultCity)
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
city := r.URL.Query().Get("city")
|
// Start background poller
|
||||||
if city == "" {
|
go startPoller(database, weatherService, config.DefaultCity, 10*time.Minute)
|
||||||
city = defaultCity
|
|
||||||
}
|
|
||||||
|
|
||||||
weather, err := fetchWeather(apiKey, city)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// note closure
|
|
||||||
id, err := saveReading(db, weather)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
||||||
"message": "weather fetched and saved",
|
|
||||||
"id": id,
|
|
||||||
"city": weather.Name,
|
|
||||||
"temp": weather.Main.Temp,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// GET /health
|
|
||||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// TODO: Check if database is ready as well
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
okStatus := HealthResponse{Status: "ok"}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(okStatus)
|
|
||||||
})
|
|
||||||
|
|
||||||
// GET /latest-temperature
|
|
||||||
http.HandleFunc("/latest-temperature", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// testing api integration
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
okStatus := LatestTemperatureResponse{Temp: 69}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(okStatus)
|
|
||||||
})
|
|
||||||
|
|
||||||
// GET /test
|
|
||||||
http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// testing deployment pipeline
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
okStatus := TestResponse{Status: "hello from go"}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(okStatus)
|
|
||||||
})
|
|
||||||
|
|
||||||
// GET /weather/history?city=London&limit=10
|
|
||||||
http.HandleFunc("/weather/history", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
|
|
||||||
city := r.URL.Query().Get("city")
|
|
||||||
if city == "" {
|
|
||||||
city = defaultCity
|
|
||||||
}
|
|
||||||
|
|
||||||
// note closure
|
|
||||||
readings, err := getReadings(db, city, 25)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(readings)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
http.HandleFunc("/weather/fetch", weatherHandler.FetchWeather)
|
||||||
|
http.HandleFunc("/weather/history", weatherHandler.GetHistory)
|
||||||
|
http.HandleFunc("/health", handlers.Health)
|
||||||
|
http.HandleFunc("/latest-temperature", handlers.LatestTemperature)
|
||||||
|
http.HandleFunc("/test", handlers.Test)
|
||||||
|
// Start server
|
||||||
go func() {
|
go func() {
|
||||||
fmt.Println("server listening on :8080")
|
fmt.Println("server listening on :8080")
|
||||||
if err := http.ListenAndServe(":8080", nil); err != nil {
|
if err := http.ListenAndServe(":8080", nil); err != nil {
|
||||||
log.Fatalf("server failed: %v", err)
|
log.Fatalf("server failed: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
// Wait for shutdown
|
||||||
// wait for Ctrl+C
|
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, os.Interrupt)
|
signal.Notify(quit, os.Interrupt)
|
||||||
<-quit
|
<-quit
|
||||||
fmt.Println("\nshutting down...")
|
fmt.Println("\nshutting down...")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WeatherResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Main struct {
|
||||||
|
Temp float64 `json:"temp"`
|
||||||
|
FeelsLike float64 `json:"feels_like"`
|
||||||
|
Humidity int `json:"humidity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Weather []struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
} `json:"weather"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Reading struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
City string `json:"city"`
|
||||||
|
Temp float64 `json:"temp"`
|
||||||
|
FeelsLike float64 `json:"feels_like"`
|
||||||
|
Humidity int `json:"humidity"`
|
||||||
|
Conditions string `json:"conditions"`
|
||||||
|
FetchedAt time.Time `json:"fetched_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HealthResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LatestTemperatureResponse struct {
|
||||||
|
Temp int `json:"temp"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"go-weather/models"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConnectDB(connStr string) (*sql.DB, error) {
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to db: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to db: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("connected to PostgreSQL")
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatabaseService struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDatabaseService(db *sql.DB) *DatabaseService {
|
||||||
|
return &DatabaseService{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DatabaseService) CreateTable() error {
|
||||||
|
_, err := s.db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS weather_readings (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
city TEXT NOT NULL,
|
||||||
|
temp DOUBLE PRECISION NOT NULL,
|
||||||
|
feels_like DOUBLE PRECISION NOT NULL,
|
||||||
|
humidity INTEGER NOT NULL,
|
||||||
|
conditions TEXT NOT NULL,
|
||||||
|
fetched_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Errorf("failed to create table: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("weather_readings table ready")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DatabaseService) SaveReading(weather models.WeatherResponse) (int, error) {
|
||||||
|
conditions := ""
|
||||||
|
if len(weather.Weather) > 0 {
|
||||||
|
conditions = weather.Weather[0].Description
|
||||||
|
}
|
||||||
|
|
||||||
|
var id int
|
||||||
|
err := s.db.QueryRow(
|
||||||
|
`INSERT INTO weather_readings (city, temp, feels_like, humidity, conditions, fetched_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
RETURNING id`,
|
||||||
|
weather.Name,
|
||||||
|
weather.Main.Temp,
|
||||||
|
weather.Main.FeelsLike,
|
||||||
|
weather.Main.Humidity,
|
||||||
|
conditions,
|
||||||
|
time.Now(),
|
||||||
|
).Scan(&id)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to save reading: %w", err)
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DatabaseService) GetReadings(city string, limit int) ([]models.Reading, error) {
|
||||||
|
rows, err := s.db.Query(
|
||||||
|
`SELECT id, city, temp, feels_like, humidity, conditions, fetched_at
|
||||||
|
FROM weather_readings
|
||||||
|
WHERE city ILIKE $1
|
||||||
|
ORDER BY fetched_at DESC
|
||||||
|
LIMIT $2`,
|
||||||
|
city, limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Query failed: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var readings []models.Reading
|
||||||
|
for rows.Next() {
|
||||||
|
var r models.Reading
|
||||||
|
err := rows.Scan(&r.ID, &r.City, &r.Temp, &r.FeelsLike, &r.Humidity, &r.Conditions, &r.FetchedAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scan failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
readings = append(readings, r)
|
||||||
|
}
|
||||||
|
return readings, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"go-weather/models"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WeatherService struct {
|
||||||
|
apiKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWeatherService(apiKey string) *WeatherService {
|
||||||
|
return &WeatherService{apiKey: apiKey}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WeatherService) FetchWeather(city string) (models.WeatherResponse, error) {
|
||||||
|
url := fmt.Sprintf(
|
||||||
|
"https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=imperial",
|
||||||
|
city, s.apiKey,
|
||||||
|
)
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return models.WeatherResponse{}, fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return models.WeatherResponse{}, fmt.Errorf("API returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var weather models.WeatherResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&weather)
|
||||||
|
if err != nil {
|
||||||
|
return models.WeatherResponse{}, fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
return weather, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user