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"
)
type H map[string]any
type MeteoData struct {
Timestamp time.Time `csv:"fecha"`
Location string `csv:"ciudad"`
@ -29,18 +27,52 @@ type FileStats struct {
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 (
ErrCannotParseFile = errors.New("cannot parse file")
ErrValidateRecord = errors.New("error validating record")
ErrRecordNotValid = errors.New("record not valid")
ErrReadingCSVHeader = errors.New("error reading CSV header")
ErrReadingCSVRow = errors.New("error reading CSV row")
ErrMissingOrInvalidDateField = errors.New("missing or invalid date field")
ErrMissingOrInvalidCityField = errors.New("missing or invalid city field")
ErrMissingOrInvalidMaxTemp = errors.New("missing or invalid max temp field")
ErrMissingOrInvalidMinTemp = errors.New("missing or invalid min temp field")
ErrMissingOrInvalidRainfall = errors.New("missing or invalid rainfall field")
ErrMissingOrInvalidCloudiness = errors.New("missing or invalid cloudiness field")
ErrMissingOrInvalidDate = errors.New("missing or invalid date")
ErrMissingOrInvalidFromDate = errors.New("missing or invalid from date")
ErrMissingOrInvalidToDate = errors.New("missing or invalid to date")
ErrMissingOrInvalidLocation = errors.New("missing or invalid location")
ErrMissingOrInvalidMaxTemp = errors.New("missing or invalid max temp")
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)")
ErrMinTempOutOfRange = errors.New("min temp out of range (must be >= -20°C)")
ErrRainfallOutOfRange = errors.New("rainfall out of range (must be 0-500 mm)")

View File

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

View File

@ -4,15 +4,18 @@ import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"servicea/internal/app"
"servicea/internal/domains"
"strconv"
"time"
)
type Handler struct {
domains.BaseHandler
*Service
}
@ -68,13 +71,48 @@ func (h *Handler) IngestCSV(w http.ResponseWriter, r *http.Request) {
"elapsed_ms", fileStats.ElapsedMS,
"file_checksum", fileStats.FileChecksum,
)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(H{
"stats": fileStats,
})
h.ToJSON(w, http.StatusOK, app.H{"stats": fileStats})
}
func (h *Handler) IngestExcel(w http.ResponseWriter, r *http.Request) {
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) {
mux.HandleFunc("POST /ingest/csv", handler.IngestCSV)
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
}
func (s *Service) GetMeteoData(params GetMeteoData) ([]MeteoData, error) {
return []MeteoData{}, nil
}