All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
190 lines
5.4 KiB
Go
190 lines
5.4 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/ejr/weatherapi/internal/application/alerts"
|
|
"gitea.maximumdirect.net/ejr/weatherapi/internal/application/conditions"
|
|
"gitea.maximumdirect.net/ejr/weatherapi/internal/application/forecasts"
|
|
"gitea.maximumdirect.net/ejr/weatherapi/internal/application/observations"
|
|
"gitea.maximumdirect.net/ejr/weatherapi/internal/platform/constants"
|
|
"gitea.maximumdirect.net/ejr/weatherapi/internal/platform/timeparse"
|
|
)
|
|
|
|
type ObservationService interface {
|
|
GetRecent(ctx context.Context, count int, unitSystem string) (observations.Response, error)
|
|
}
|
|
|
|
type ConditionsService interface {
|
|
GetCurrent(ctx context.Context, unitSystem string) (conditions.Response, error)
|
|
}
|
|
|
|
type ForecastService interface {
|
|
GetByTimestamp(ctx context.Context, ts time.Time, unitSystem string) (forecasts.Response, error)
|
|
}
|
|
|
|
type AlertService interface {
|
|
GetCurrent(ctx context.Context) (alerts.Response, error)
|
|
}
|
|
|
|
type Server struct {
|
|
obsSvc ObservationService
|
|
conditionsSvc ConditionsService
|
|
fcSvc ForecastService
|
|
alertsSvc AlertService
|
|
}
|
|
|
|
func NewServer(obsSvc ObservationService, conditionsSvc ConditionsService, fcSvc ForecastService, alertsSvc AlertService) *Server {
|
|
return &Server{obsSvc: obsSvc, conditionsSvc: conditionsSvc, fcSvc: fcSvc, alertsSvc: alertsSvc}
|
|
}
|
|
|
|
func (s *Server) Handler() http.Handler {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/observations", s.handleObservations)
|
|
mux.HandleFunc("/conditions/current", s.handleConditionsCurrent)
|
|
mux.HandleFunc("/forecast", s.handleForecast)
|
|
mux.HandleFunc("/alerts/current", s.handleAlertsCurrent)
|
|
return mux
|
|
}
|
|
|
|
func (s *Server) handleConditionsCurrent(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only GET is supported")
|
|
return
|
|
}
|
|
|
|
unitSystem, err := parseUnits(r, constants.UnitSystemUS)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return
|
|
}
|
|
|
|
resp, err := s.conditionsSvc.GetCurrent(r.Context(), unitSystem)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "failed to fetch current conditions")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (s *Server) handleObservations(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only GET is supported")
|
|
return
|
|
}
|
|
|
|
unitSystem, err := parseUnits(r, constants.UnitSystemMetric)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return
|
|
}
|
|
|
|
count, err := parseCount(r)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return
|
|
}
|
|
|
|
resp, err := s.obsSvc.GetRecent(r.Context(), count, unitSystem)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "failed to fetch observations")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (s *Server) handleForecast(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only GET is supported")
|
|
return
|
|
}
|
|
|
|
unitSystem, err := parseUnits(r, constants.UnitSystemUS)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return
|
|
}
|
|
|
|
raw := r.URL.Query().Get("timestamp")
|
|
ts, err := timeparse.ParseTimestamp(raw)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return
|
|
}
|
|
|
|
resp, err := s.fcSvc.GetByTimestamp(r.Context(), ts, unitSystem)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "failed to fetch forecast")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (s *Server) handleAlertsCurrent(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only GET is supported")
|
|
return
|
|
}
|
|
|
|
resp, err := s.alertsSvc.GetCurrent(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "failed to fetch current alerts")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func parseCount(r *http.Request) (int, error) {
|
|
raw := strings.TrimSpace(r.URL.Query().Get("count"))
|
|
if raw == "" {
|
|
return constants.DefaultObservationCount, nil
|
|
}
|
|
|
|
count, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("count must be an integer between 1 and %d", constants.MaxObservationCount)
|
|
}
|
|
if count < 1 || count > constants.MaxObservationCount {
|
|
return 0, fmt.Errorf("count must be an integer between 1 and %d", constants.MaxObservationCount)
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func parseUnits(r *http.Request, defaultSystem string) (string, error) {
|
|
raw := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("units")))
|
|
if raw == "" {
|
|
return defaultSystem, nil
|
|
}
|
|
switch raw {
|
|
case constants.UnitSystemUS, constants.UnitSystemMetric:
|
|
return raw, nil
|
|
default:
|
|
return "", fmt.Errorf("units must be one of: us, metric")
|
|
}
|
|
}
|
|
|
|
type errorResponse struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, code, message string) {
|
|
writeJSON(w, status, errorResponse{Code: code, Message: message})
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(payload)
|
|
}
|