From c7024d7f4eefb8adb18817f9ed0325b9b54fe5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Tue, 28 Oct 2025 21:38:58 +0100 Subject: [PATCH] add base handler and refactor H --- service_a/internal/app/app.go | 3 ++ service_a/internal/domains/handlers.go | 16 +++++++ service_a/internal/domains/meteo/domain.go | 48 +++++++++++++++---- service_a/internal/domains/meteo/file.go | 17 +++---- service_a/internal/domains/meteo/handlers.go | 50 +++++++++++++++++--- service_a/internal/domains/meteo/router.go | 2 + service_a/internal/domains/meteo/service.go | 4 ++ 7 files changed, 118 insertions(+), 22 deletions(-) create mode 100644 service_a/internal/app/app.go create mode 100644 service_a/internal/domains/handlers.go diff --git a/service_a/internal/app/app.go b/service_a/internal/app/app.go new file mode 100644 index 0000000..93a6d9e --- /dev/null +++ b/service_a/internal/app/app.go @@ -0,0 +1,3 @@ +package app + +type H map[string]any diff --git a/service_a/internal/domains/handlers.go b/service_a/internal/domains/handlers.go new file mode 100644 index 0000000..4278011 --- /dev/null +++ b/service_a/internal/domains/handlers.go @@ -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) +} diff --git a/service_a/internal/domains/meteo/domain.go b/service_a/internal/domains/meteo/domain.go index 9ed3f87..e67addf 100644 --- a/service_a/internal/domains/meteo/domain.go +++ b/service_a/internal/domains/meteo/domain.go @@ -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)") diff --git a/service_a/internal/domains/meteo/file.go b/service_a/internal/domains/meteo/file.go index 3af1fab..262a1d2 100644 --- a/service_a/internal/domains/meteo/file.go +++ b/service_a/internal/domains/meteo/file.go @@ -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) diff --git a/service_a/internal/domains/meteo/handlers.go b/service_a/internal/domains/meteo/handlers.go index 49d670e..5336b93 100644 --- a/service_a/internal/domains/meteo/handlers.go +++ b/service_a/internal/domains/meteo/handlers.go @@ -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) +} diff --git a/service_a/internal/domains/meteo/router.go b/service_a/internal/domains/meteo/router.go index 9fa8cf2..f773628 100644 --- a/service_a/internal/domains/meteo/router.go +++ b/service_a/internal/domains/meteo/router.go @@ -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) } diff --git a/service_a/internal/domains/meteo/service.go b/service_a/internal/domains/meteo/service.go index 1758519..016dd05 100644 --- a/service_a/internal/domains/meteo/service.go +++ b/service_a/internal/domains/meteo/service.go @@ -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 +}