ready set go
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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 main() {
|
||||
apiKey := getEnv("OPENWEATHERMAP_API_KEY")
|
||||
defaultCity := getEnv("WEATHER_CITY")
|
||||
|
||||
db := connectDB()
|
||||
defer db.Close()
|
||||
|
||||
createTable(db)
|
||||
|
||||
// POST /weather/fetch?city=London - fetch current weather and save it
|
||||
http.HandleFunc("/weather/fetch", func(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 = 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 /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)
|
||||
})
|
||||
|
||||
port := ":8080"
|
||||
fmt.Printf("server listening on %s\n", port)
|
||||
log.Fatal(http.ListenAndServe(port, nil))
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user