From e46b71be51333e56ffc233e0523901431d0f9442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Thu, 23 Oct 2025 03:41:02 +0200 Subject: [PATCH] draft ingest csv handler --- README.md | 10 +++- service_a/internal/domains/meteo/domain.go | 33 ++++++----- service_a/internal/domains/meteo/handlers.go | 62 +++++++++++++++++++- 3 files changed, 88 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 5ba1a7f..cf96f78 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,15 @@ Prueba técnica para el puesto de desarrollador Go/C++ Compilar todos los servicios e iniciar los contenedores Docker. -`docker compose --env-file up --build` +```bash +docker compose --env-file up --build` +``` + +Hacer petición POST con fichero a `/ingest/csv` + +```bash +curl -X POST http://localhost:8080/ingest/csv -F "file=@meteo.csv" +``` ## Decisiones técnica diff --git a/service_a/internal/domains/meteo/domain.go b/service_a/internal/domains/meteo/domain.go index 9f12793..8a3941b 100644 --- a/service_a/internal/domains/meteo/domain.go +++ b/service_a/internal/domains/meteo/domain.go @@ -29,19 +29,22 @@ type FileStats struct { } 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") - 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)") - ErrCloudinessOutOfRange = errors.New("cloudiness out of range (must be 0-100%)") + 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") + 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)") + ErrCloudinessOutOfRange = errors.New("cloudiness out of range (must be 0-100%)") + ErrParsingForm = errors.New("error parsing form") + ErrRetrievingFile = errors.New("error retrieving file") + ErrReadingFile = errors.New("error reading file") ) diff --git a/service_a/internal/domains/meteo/handlers.go b/service_a/internal/domains/meteo/handlers.go index 70862a1..1400be3 100644 --- a/service_a/internal/domains/meteo/handlers.go +++ b/service_a/internal/domains/meteo/handlers.go @@ -1,8 +1,15 @@ package meteo import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" "fmt" + "io" + "log/slog" "net/http" + "time" ) type Handler struct{} @@ -12,7 +19,60 @@ func NewHandler() *Handler { } func (h *Handler) IngestCSV(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "hello from csv") + err := r.ParseMultipartForm(10 << 20) + if err != nil { + http.Error(w, ErrParsingForm.Error(), http.StatusBadRequest) + return + } + + file, header, err := r.FormFile("file") + if err != nil { + http.Error(w, ErrRetrievingFile.Error(), http.StatusBadRequest) + return + } + defer file.Close() + + content, err := io.ReadAll(file) + if err != nil { + http.Error(w, ErrReadingFile.Error(), http.StatusInternalServerError) + return + } + + hash := sha256.Sum256(content) + checksum := hex.EncodeToString(hash[:]) + + fileStats := &FileStats{ + FileChecksum: checksum, + } + + start := time.Now() + + csvParser := &CSV{} + inserted, rejected, err := csvParser.Parse(bytes.NewReader(content), fileStats) + if err != nil { + slog.Error(ErrCannotParseFile.Error(), + "filename", header.Filename, + "error", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + fileStats.ElapsedMS = int(time.Since(start).Milliseconds()) + + slog.Info("CSV file processed", + "filename", header.Filename, + "rows_inserted", fileStats.RowsInserted, + "rows_rejected", fileStats.RowsRejected, + "elapsed_ms", fileStats.ElapsedMS, + "file_checksum", fileStats.FileChecksum, + "inserted_data", inserted, + "rejected_data", rejected, + ) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(H{ + "stats": fileStats, + }) } func (h *Handler) IngestExcel(w http.ResponseWriter, r *http.Request) {