Added a /conditions/current endpoint with computed best-guess values for current conditions
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
This commit is contained in:
@@ -3,21 +3,30 @@ 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 {
|
||||
GetCurrent(ctx context.Context) (observations.CurrentResponse, error)
|
||||
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) (forecasts.Response, error)
|
||||
GetByTimestamp(ctx context.Context, ts time.Time, unitSystem string) (forecasts.Response, error)
|
||||
}
|
||||
|
||||
type AlertService interface {
|
||||
@@ -25,32 +34,67 @@ type AlertService interface {
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
obsSvc ObservationService
|
||||
fcSvc ForecastService
|
||||
alertsSvc AlertService
|
||||
obsSvc ObservationService
|
||||
conditionsSvc ConditionsService
|
||||
fcSvc ForecastService
|
||||
alertsSvc AlertService
|
||||
}
|
||||
|
||||
func NewServer(obsSvc ObservationService, fcSvc ForecastService, alertsSvc AlertService) *Server {
|
||||
return &Server{obsSvc: obsSvc, fcSvc: fcSvc, alertsSvc: alertsSvc}
|
||||
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/current", s.handleObservationsCurrent)
|
||||
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) handleObservationsCurrent(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
}
|
||||
|
||||
resp, err := s.obsSvc.GetCurrent(r.Context())
|
||||
unitSystem, err := parseUnits(r, constants.UnitSystemUS)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "failed to fetch current observations")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -63,6 +107,12 @@ func (s *Server) handleForecast(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
@@ -70,7 +120,7 @@ func (s *Server) handleForecast(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := s.fcSvc.GetByTimestamp(r.Context(), ts)
|
||||
resp, err := s.fcSvc.GetByTimestamp(r.Context(), ts, unitSystem)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "failed to fetch forecast")
|
||||
return
|
||||
@@ -94,6 +144,35 @@ func (s *Server) handleAlertsCurrent(w http.ResponseWriter, r *http.Request) {
|
||||
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"`
|
||||
|
||||
Reference in New Issue
Block a user