forked from gsn/predictor
feat: refactor
This commit is contained in:
parent
82ef1cb3b8
commit
51bbf3c579
44 changed files with 8589 additions and 0 deletions
30
internal/transport/middleware/log.go
Normal file
30
internal/transport/middleware/log.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ogen-go/ogen/middleware"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Logging returns an ogen middleware that logs request duration.
|
||||
func Logging(log *zap.Logger) middleware.Middleware {
|
||||
return func(req middleware.Request, next func(req middleware.Request) (middleware.Response, error)) (middleware.Response, error) {
|
||||
lg := log.With(zap.String("operation", req.OperationID))
|
||||
|
||||
start := time.Now()
|
||||
resp, err := next(req)
|
||||
dur := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
lg.Error("request failed",
|
||||
zap.Duration("duration", dur),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
lg.Info("request completed",
|
||||
zap.Duration("duration", dur))
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
16
internal/transport/rest/handler/deps.go
Normal file
16
internal/transport/rest/handler/deps.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"predictor-refactored/internal/dataset"
|
||||
"predictor-refactored/internal/elevation"
|
||||
)
|
||||
|
||||
// Service defines the interface the handler needs from the service layer.
|
||||
type Service interface {
|
||||
Ready() bool
|
||||
DatasetTime() (time.Time, bool)
|
||||
Dataset() *dataset.File
|
||||
Elevation() *elevation.Dataset
|
||||
}
|
||||
216
internal/transport/rest/handler/handler.go
Normal file
216
internal/transport/rest/handler/handler.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"predictor-refactored/internal/prediction"
|
||||
api "predictor-refactored/pkg/rest"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var _ api.Handler = (*Handler)(nil)
|
||||
|
||||
// Handler implements the ogen-generated api.Handler interface.
|
||||
type Handler struct {
|
||||
svc Service
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
// New creates a new Handler.
|
||||
func New(svc Service, log *zap.Logger) *Handler {
|
||||
return &Handler{svc: svc, log: log}
|
||||
}
|
||||
|
||||
// PerformPrediction implements the prediction endpoint.
|
||||
func (h *Handler) PerformPrediction(ctx context.Context, params api.PerformPredictionParams) (*api.PredictionResponse, error) {
|
||||
if !h.svc.Ready() {
|
||||
return nil, newError(http.StatusServiceUnavailable, "no dataset loaded, service is starting up")
|
||||
}
|
||||
|
||||
ds := h.svc.Dataset()
|
||||
if ds == nil {
|
||||
return nil, newError(http.StatusServiceUnavailable, "dataset unavailable")
|
||||
}
|
||||
|
||||
dsEpoch := float64(ds.DSTime.Unix())
|
||||
|
||||
// Parse parameters with defaults
|
||||
profile := "standard_profile"
|
||||
if p, ok := params.Profile.Get(); ok {
|
||||
profile = string(p)
|
||||
}
|
||||
|
||||
ascentRate := 5.0
|
||||
if v, ok := params.AscentRate.Get(); ok {
|
||||
ascentRate = v
|
||||
}
|
||||
|
||||
burstAltitude := 28000.0
|
||||
if v, ok := params.BurstAltitude.Get(); ok {
|
||||
burstAltitude = v
|
||||
}
|
||||
|
||||
descentRate := 5.0
|
||||
if v, ok := params.DescentRate.Get(); ok {
|
||||
descentRate = v
|
||||
}
|
||||
|
||||
launchAlt := 0.0
|
||||
if v, ok := params.LaunchAltitude.Get(); ok {
|
||||
launchAlt = v
|
||||
}
|
||||
|
||||
// Normalize longitude to [0, 360)
|
||||
lng := params.LaunchLongitude
|
||||
if lng < 0 {
|
||||
lng += 360.0
|
||||
}
|
||||
|
||||
launchTime := float64(params.LaunchDatetime.Unix())
|
||||
|
||||
warnings := &prediction.Warnings{}
|
||||
|
||||
// Build profile chain
|
||||
elev := h.svc.Elevation()
|
||||
var stages []prediction.Stage
|
||||
switch profile {
|
||||
case "standard_profile":
|
||||
stages = prediction.StandardProfile(
|
||||
ascentRate, burstAltitude, descentRate,
|
||||
ds, dsEpoch, warnings, elev)
|
||||
case "float_profile":
|
||||
floatAlt := 25000.0
|
||||
if v, ok := params.FloatAltitude.Get(); ok {
|
||||
floatAlt = v
|
||||
}
|
||||
stopTime := params.LaunchDatetime.Add(24 * time.Hour)
|
||||
if v, ok := params.StopDatetime.Get(); ok {
|
||||
stopTime = v
|
||||
}
|
||||
stages = prediction.FloatProfile(
|
||||
ascentRate, floatAlt, stopTime,
|
||||
ds, dsEpoch, warnings)
|
||||
default:
|
||||
return nil, newError(http.StatusBadRequest, "unknown profile: "+profile)
|
||||
}
|
||||
|
||||
// Run prediction
|
||||
startTime := time.Now().UTC()
|
||||
results := prediction.RunPrediction(launchTime, params.LaunchLatitude, lng, launchAlt, stages)
|
||||
completeTime := time.Now().UTC()
|
||||
|
||||
// Build response
|
||||
stageNames := []string{"ascent", "descent"}
|
||||
if profile == "float_profile" {
|
||||
stageNames = []string{"ascent", "float"}
|
||||
}
|
||||
|
||||
var predItems []api.PredictionResponsePredictionItem
|
||||
for i, sr := range results {
|
||||
stageName := "ascent"
|
||||
if i < len(stageNames) {
|
||||
stageName = stageNames[i]
|
||||
}
|
||||
|
||||
var stageEnum api.PredictionResponsePredictionItemStage
|
||||
switch stageName {
|
||||
case "ascent":
|
||||
stageEnum = api.PredictionResponsePredictionItemStageAscent
|
||||
case "descent":
|
||||
stageEnum = api.PredictionResponsePredictionItemStageDescent
|
||||
case "float":
|
||||
stageEnum = api.PredictionResponsePredictionItemStageFloat
|
||||
}
|
||||
|
||||
var traj []api.PredictionResponsePredictionItemTrajectoryItem
|
||||
for _, pt := range sr.Points {
|
||||
ptLng := pt.Lng
|
||||
if ptLng > 180 {
|
||||
ptLng -= 360
|
||||
}
|
||||
traj = append(traj, api.PredictionResponsePredictionItemTrajectoryItem{
|
||||
Datetime: time.Unix(int64(pt.T), 0).UTC(),
|
||||
Latitude: pt.Lat,
|
||||
Longitude: ptLng,
|
||||
Altitude: pt.Alt,
|
||||
})
|
||||
}
|
||||
|
||||
predItems = append(predItems, api.PredictionResponsePredictionItem{
|
||||
Stage: stageEnum,
|
||||
Trajectory: traj,
|
||||
})
|
||||
}
|
||||
|
||||
resp := &api.PredictionResponse{
|
||||
Prediction: predItems,
|
||||
Metadata: api.PredictionResponseMetadata{
|
||||
StartDatetime: startTime,
|
||||
CompleteDatetime: completeTime,
|
||||
},
|
||||
}
|
||||
|
||||
// Echo request
|
||||
resp.Request = api.NewOptPredictionResponseRequest(api.PredictionResponseRequest{
|
||||
Dataset: api.NewOptString(ds.DSTime.Format("2006-01-02T15:04:05Z")),
|
||||
LaunchLatitude: api.NewOptFloat64(params.LaunchLatitude),
|
||||
LaunchLongitude: api.NewOptFloat64(params.LaunchLongitude),
|
||||
LaunchDatetime: api.NewOptString(params.LaunchDatetime.Format(time.RFC3339)),
|
||||
LaunchAltitude: params.LaunchAltitude,
|
||||
})
|
||||
|
||||
// Warnings
|
||||
warnMap := warnings.ToMap()
|
||||
if len(warnMap) > 0 {
|
||||
resp.Warnings = api.NewOptPredictionResponseWarnings(api.PredictionResponseWarnings{})
|
||||
}
|
||||
|
||||
h.log.Info("prediction complete",
|
||||
zap.String("profile", profile),
|
||||
zap.Int("stages", len(results)),
|
||||
zap.Duration("elapsed", completeTime.Sub(startTime)))
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ReadinessCheck implements the health check endpoint.
|
||||
func (h *Handler) ReadinessCheck(ctx context.Context) (*api.ReadinessResponse, error) {
|
||||
resp := &api.ReadinessResponse{}
|
||||
|
||||
if h.svc.Ready() {
|
||||
resp.Status = api.ReadinessResponseStatusOk
|
||||
if dsTime, ok := h.svc.DatasetTime(); ok {
|
||||
resp.DatasetTime = api.NewOptDateTime(dsTime)
|
||||
}
|
||||
} else {
|
||||
resp.Status = api.ReadinessResponseStatusNotReady
|
||||
resp.ErrorMessage = api.NewOptString("no dataset loaded")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// NewError creates an ErrorStatusCode from an error returned by a handler.
|
||||
func (h *Handler) NewError(ctx context.Context, err error) *api.ErrorStatusCode {
|
||||
if statusErr, ok := err.(*api.ErrorStatusCode); ok {
|
||||
return statusErr
|
||||
}
|
||||
|
||||
h.log.Error("unhandled error", zap.Error(err))
|
||||
return newError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
func newError(status int, description string) *api.ErrorStatusCode {
|
||||
return &api.ErrorStatusCode{
|
||||
StatusCode: status,
|
||||
Response: api.Error{
|
||||
Error: api.ErrorError{
|
||||
Type: http.StatusText(status),
|
||||
Description: description,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
75
internal/transport/rest/transport.go
Normal file
75
internal/transport/rest/transport.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"predictor-refactored/internal/transport/middleware"
|
||||
"predictor-refactored/internal/transport/rest/handler"
|
||||
api "predictor-refactored/pkg/rest"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Transport wraps the ogen HTTP server.
|
||||
type Transport struct {
|
||||
srv *api.Server
|
||||
handler *handler.Handler
|
||||
port int
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
// New creates a new REST transport.
|
||||
func New(h *handler.Handler, port int, log *zap.Logger) (*Transport, error) {
|
||||
srv, err := api.NewServer(
|
||||
h,
|
||||
api.WithMiddleware(middleware.Logging(log)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create ogen server: %w", err)
|
||||
}
|
||||
|
||||
return &Transport{
|
||||
srv: srv,
|
||||
handler: h,
|
||||
port: port,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run starts the HTTP server. Blocks until the server stops.
|
||||
func (t *Transport) Run() error {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", t.srv)
|
||||
|
||||
httpSrv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", t.port),
|
||||
Handler: corsMiddleware(mux),
|
||||
}
|
||||
|
||||
t.log.Info("starting HTTP server", zap.Int("port", t.port))
|
||||
return httpSrv.ListenAndServe()
|
||||
}
|
||||
|
||||
// Shutdown gracefully stops the HTTP server.
|
||||
func (t *Transport) Shutdown(ctx context.Context) error {
|
||||
// The ogen server doesn't have a shutdown method;
|
||||
// shutdown is handled by the http.Server in main.go
|
||||
return nil
|
||||
}
|
||||
|
||||
func corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue