add base handler and refactor H

This commit is contained in:
Pedro Pérez 2025-10-28 21:38:58 +01:00
parent fb4d31afba
commit c7024d7f4e
7 changed files with 118 additions and 22 deletions

View File

@ -0,0 +1,3 @@
package app
type H map[string]any

View File

@ -0,0 +1,16 @@
package domains
import (
"encoding/json"
"net/http"
)
type (
BaseHandler struct{}
)
func (bh *BaseHandler) ToJSON(w http.ResponseWriter, statusCode int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}

View File

@ -5,8 +5,6 @@ import (
"time" "time"
) )
type H map[string]any
type MeteoData struct { type MeteoData struct {
Timestamp time.Time `csv:"fecha"` Timestamp time.Time `csv:"fecha"`
Location string `csv:"ciudad"` Location string `csv:"ciudad"`
@ -29,18 +27,52 @@ type FileStats struct {
FileChecksum string `json:"file_checksum"` FileChecksum string `json:"file_checksum"`
} }
type GetMeteoData struct {
Location string
From string
To string
Page int
Limit int
}
func (mt *GetMeteoData) Validate() error {
if mt.Location == "" {
return ErrMissingOrInvalidLocation
}
if mt.From == "" {
return ErrMissingOrInvalidFromDate
}
if mt.To == "" {
return ErrMissingOrInvalidToDate
}
if _, err := time.Parse("2006-01-02", mt.From); err != nil {
return ErrMissingOrInvalidFromDate
}
if _, err := time.Parse("2006-01-02", mt.To); err != nil {
return ErrMissingOrInvalidToDate
}
return nil
}
var ( var (
ErrCannotParseFile = errors.New("cannot parse file") ErrCannotParseFile = errors.New("cannot parse file")
ErrValidateRecord = errors.New("error validating record") ErrValidateRecord = errors.New("error validating record")
ErrRecordNotValid = errors.New("record not valid") ErrRecordNotValid = errors.New("record not valid")
ErrReadingCSVHeader = errors.New("error reading CSV header") ErrReadingCSVHeader = errors.New("error reading CSV header")
ErrReadingCSVRow = errors.New("error reading CSV row") ErrReadingCSVRow = errors.New("error reading CSV row")
ErrMissingOrInvalidDateField = errors.New("missing or invalid date field") ErrMissingOrInvalidDate = errors.New("missing or invalid date")
ErrMissingOrInvalidCityField = errors.New("missing or invalid city field") ErrMissingOrInvalidFromDate = errors.New("missing or invalid from date")
ErrMissingOrInvalidMaxTemp = errors.New("missing or invalid max temp field") ErrMissingOrInvalidToDate = errors.New("missing or invalid to date")
ErrMissingOrInvalidMinTemp = errors.New("missing or invalid min temp field") ErrMissingOrInvalidLocation = errors.New("missing or invalid location")
ErrMissingOrInvalidRainfall = errors.New("missing or invalid rainfall field") ErrMissingOrInvalidMaxTemp = errors.New("missing or invalid max temp")
ErrMissingOrInvalidCloudiness = errors.New("missing or invalid cloudiness field") ErrMissingOrInvalidMinTemp = errors.New("missing or invalid min temp")
ErrMissingOrInvalidRainfall = errors.New("missing or invalid rainfall")
ErrMissingOrInvalidCloudiness = errors.New("missing or invalid cloudiness")
ErrMaxTempOutOfRange = errors.New("max temp out of range (must be <= 60°C)") ErrMaxTempOutOfRange = errors.New("max temp out of range (must be <= 60°C)")
ErrMinTempOutOfRange = errors.New("min temp out of range (must be >= -20°C)") ErrMinTempOutOfRange = errors.New("min temp out of range (must be >= -20°C)")
ErrRainfallOutOfRange = errors.New("rainfall out of range (must be 0-500 mm)") ErrRainfallOutOfRange = errors.New("rainfall out of range (must be 0-500 mm)")

View File

@ -4,6 +4,7 @@ import (
"encoding/csv" "encoding/csv"
"fmt" "fmt"
"io" "io"
"servicea/internal/app"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -69,7 +70,7 @@ func (c *CSV) Parse(r io.Reader) ([]MeteoData, []RejectedMeteoData, error) {
rowValue := strings.Join(row, ";") rowValue := strings.Join(row, ";")
record := make(H) record := make(app.H)
for i, value := range row { for i, value := range row {
if i < len(header) { if i < len(header) {
record[header[i]] = value record[header[i]] = value
@ -99,17 +100,17 @@ func (c *CSV) Parse(r io.Reader) ([]MeteoData, []RejectedMeteoData, error) {
return meteoDataList, rejectedDataList, nil return meteoDataList, rejectedDataList, nil
} }
func normalize(record H) (*MeteoData, error) { func normalize(record app.H) (*MeteoData, error) {
meteoData := &MeteoData{} meteoData := &MeteoData{}
var err error var err error
meteoData.Timestamp, err = parseDate(record, "Fecha", ErrMissingOrInvalidDateField) meteoData.Timestamp, err = parseDate(record, "Fecha", ErrMissingOrInvalidDate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
meteoData.Location, err = parseString(record, "Ciudad", ErrMissingOrInvalidCityField) meteoData.Location, err = parseString(record, "Ciudad", ErrMissingOrInvalidLocation)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -137,7 +138,7 @@ func normalize(record H) (*MeteoData, error) {
return meteoData, nil return meteoData, nil
} }
func parseDate(record H, key string, errMissing error) (time.Time, error) { func parseDate(record app.H, key string, errMissing error) (time.Time, error) {
if str, ok := record[key].(string); ok && str != "" { if str, ok := record[key].(string); ok && str != "" {
t, err := time.Parse("2006/01/02", str) t, err := time.Parse("2006/01/02", str)
if err != nil { if err != nil {
@ -148,14 +149,14 @@ func parseDate(record H, key string, errMissing error) (time.Time, error) {
return time.Time{}, errMissing return time.Time{}, errMissing
} }
func parseString(record H, key string, errMissing error) (string, error) { func parseString(record app.H, key string, errMissing error) (string, error) {
if str, ok := record[key].(string); ok && str != "" { if str, ok := record[key].(string); ok && str != "" {
return str, nil return str, nil
} }
return "", errMissing return "", errMissing
} }
func parseFloatField(record H, key string, errMissing error) (float32, error) { func parseFloatField(record app.H, key string, errMissing error) (float32, error) {
if str, ok := record[key].(string); ok && str != "" { if str, ok := record[key].(string); ok && str != "" {
str = strings.Replace(str, ",", ".", 1) str = strings.Replace(str, ",", ".", 1)
f, err := strconv.ParseFloat(str, 32) f, err := strconv.ParseFloat(str, 32)
@ -167,7 +168,7 @@ func parseFloatField(record H, key string, errMissing error) (float32, error) {
return 0, errMissing return 0, errMissing
} }
func parseIntField(record H, key string, errMissing error) (int, error) { func parseIntField(record app.H, key string, errMissing error) (int, error) {
if str, ok := record[key].(string); ok && str != "" { if str, ok := record[key].(string); ok && str != "" {
str = strings.TrimSpace(str) str = strings.TrimSpace(str)
i, err := strconv.Atoi(str) i, err := strconv.Atoi(str)

View File

@ -4,15 +4,18 @@ import (
"bytes" "bytes"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
"servicea/internal/app"
"servicea/internal/domains"
"strconv"
"time" "time"
) )
type Handler struct { type Handler struct {
domains.BaseHandler
*Service *Service
} }
@ -68,13 +71,48 @@ func (h *Handler) IngestCSV(w http.ResponseWriter, r *http.Request) {
"elapsed_ms", fileStats.ElapsedMS, "elapsed_ms", fileStats.ElapsedMS,
"file_checksum", fileStats.FileChecksum, "file_checksum", fileStats.FileChecksum,
) )
h.ToJSON(w, http.StatusOK, app.H{"stats": fileStats})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(H{
"stats": fileStats,
})
} }
func (h *Handler) IngestExcel(w http.ResponseWriter, r *http.Request) { func (h *Handler) IngestExcel(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello from excel") fmt.Fprintf(w, "hello from excel")
} }
func (h *Handler) GetMeteoData(w http.ResponseWriter, r *http.Request) {
queryParams := r.URL.Query()
pageInt := int(1)
limitInt := int(10)
page := queryParams.Get("page")
limit := queryParams.Get("limit")
if page != "" {
p, err := strconv.Atoi(page)
if err == nil {
pageInt = p
}
}
if limit != "" {
l, err := strconv.Atoi(limit)
if err == nil {
limitInt = l
} else {
limitInt = 10
}
}
params := GetMeteoData{
Location: queryParams.Get("city"),
From: queryParams.Get("from"),
To: queryParams.Get("to"),
Page: pageInt,
Limit: limitInt,
}
if err := params.Validate(); err != nil {
h.ToJSON(w, http.StatusBadRequest, app.H{"error": err.Error()})
}
slog.Info("params", "params", params)
}

View File

@ -5,4 +5,6 @@ import "net/http"
func RegisterRoutes(mux *http.ServeMux, handler *Handler) { func RegisterRoutes(mux *http.ServeMux, handler *Handler) {
mux.HandleFunc("POST /ingest/csv", handler.IngestCSV) mux.HandleFunc("POST /ingest/csv", handler.IngestCSV)
mux.HandleFunc("POST /ingest/excel", handler.IngestExcel) mux.HandleFunc("POST /ingest/excel", handler.IngestExcel)
mux.HandleFunc("GET /data", handler.GetMeteoData)
} }

View File

@ -48,3 +48,7 @@ func (s *Service) UpdateElapsedMS(ctx context.Context, batchID, elapsedMS int) e
return nil return nil
} }
func (s *Service) GetMeteoData(params GetMeteoData) ([]MeteoData, error) {
return []MeteoData{}, nil
}