diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0322656 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "App Test", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}/app" + }, + { + "name": "Config Tests", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}/config" + }, + { + "name": "Launch Specific Test", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}/app/inventory_data_test.go", + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 4a9330d..4545c7a 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,17 @@ Dieses kleine Tool ermöglicht es, erfasstes Equipment mit den THW-Inventurdaten Das Tool lädt man am einfachsten aus der [Releases-Sektion](https://github.com/mvach/thwInventoryMerge/releases) herunter und legt es in ein beliebiges leeres Verzeichnis (das `working_dir`). ## Konfiguration -Die CSV-Dateien mit den erfassten Barcodes der Scanner sowie die (aus THWin exportierte) Inventur-Excel-Datei legt man am besten ebenfalls in das `working_dir`. +Die CSV-Dateien mit den erfassten Barcodes der Scanner sowie die (aus THWin exportierte) Inventur CSV Datei legt man am besten ebenfalls in das `working_dir`. -Zudem erstellt man eine Konfigurationsdatei (`config.json`), die man am einfachsten auch in das `working_dir` legt. +Zudem erstellt man eine Konfigurationsdatei (`config.json`), die man auch in das `working_dir` legt. ## Hier ist eine Beispielkonfiguration: ``` // config.json { - "excel_file_name": "20240101_Bestand.xlsx", - "excel_config": { - "worksheet_name": "N", + "inventory_csv_file_name": "20240101_Bestand_FGr_N.csv", + "inventory_csv_config": { "equipment_id_column_name": "Inventar Nr", "equipment_available_column_name": "Verfügbar" } @@ -31,7 +30,7 @@ C:/ └── DeinUser/ └── MeinArbeitsverzeichnis/ ├── config.json - ├── 20240101_Bestand.xlsx + ├── 20240101_Bestand_FGr_N.csv ├── scanner1.csv ├── scanner2.csv ├── scanner3.csv @@ -54,4 +53,8 @@ C:/ ## Ausführung -Liegen alle Dateien gemeinsam im `working_dir`, kann thwInventoryMerge.exe einfach per Doppelklick ausgeführt werden. +Befinden sich alle Dateien gemeinsam im `working_dir`, kann die Datei `thwInventoryMerge.exe` einfach per Doppelklick ausgeführt werden. + +Nach der Ausführung wird im `working_dir` ein `result`-Verzeichnis erstellt, in dem sich eine Datei namens `result_.csv` befindet. Diese Datei enthält die zusammengeführten Inventurdaten. + +Jede weitere Ausführung erzeugt eine neue Datei `result_.csv`. \ No newline at end of file diff --git a/app/app_suite_test.go b/app/app_suite_test.go new file mode 100644 index 0000000..097ecd9 --- /dev/null +++ b/app/app_suite_test.go @@ -0,0 +1,13 @@ +package app_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "App Suite") +} \ No newline at end of file diff --git a/app/csv_file.go b/app/csv_file.go new file mode 100644 index 0000000..4c612c5 --- /dev/null +++ b/app/csv_file.go @@ -0,0 +1,66 @@ +package app + +import ( + "encoding/csv" + "fmt" + "os" +) + +type CSVContent [][]string + +type CSVFile interface { + Read(filePath string) (CSVContent, error) + + Write(filePath string, content CSVContent) error +} + +type csvFile struct { +} + +func NewCSVFile() CSVFile { + return &csvFile{} +} + +func (c *csvFile) Read(filePath string) (CSVContent, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("failed to open CSV file '%s': %w", filePath, err) + } + defer file.Close() + + reader := csv.NewReader(file) + reader.Comma = ';' + reader.LazyQuotes = true + content, err := reader.ReadAll() + if err != nil { + return nil, fmt.Errorf("failed to read CSV file '%s': %w", filePath, err) + } + + return content, nil +} + +func (c *csvFile) Write(filePath string, content CSVContent) error { + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create CSV file: %w", err) + } + defer file.Close() + + // Write the UTF-8 BOM for Excel on Windows compatibility + _, err = file.Write([]byte{0xEF, 0xBB, 0xBF}) + if err != nil { + return fmt.Errorf("failed to write UTF-8 BOM to csv file: %v", err) + } + + writer := csv.NewWriter(file) + writer.Comma = ';' + + err = writer.WriteAll(content) + if err != nil { + return fmt.Errorf("failed to write into CSV file: %w", err) + } + + writer.Flush() + + return nil +} diff --git a/app/csv_file_test.go b/app/csv_file_test.go new file mode 100644 index 0000000..b7bc94d --- /dev/null +++ b/app/csv_file_test.go @@ -0,0 +1,174 @@ +package app_test + +import ( + "bufio" + "encoding/csv" + "os" + "path/filepath" + "thwInventoryMerge/app" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("CSVFile", func() { + + var ( + filePath string + ) + + BeforeEach(func() { + }) + + AfterEach(func() { + if filePath != "" { + os.Remove(filePath) + } + }) + + var _ = Describe("Read", func() { + Context("when the CSV file is valid", func() { + BeforeEach(func() { + filePath = filepath.Join(os.TempDir(), "valid.csv") + file, err := os.Create(filePath) + Expect(err).NotTo(HaveOccurred()) + defer file.Close() + + writer := csv.NewWriter(file) + writer.Comma = ';' + writer.WriteAll([][]string{ + {"name", "age", "city"}, + {"Alice", "30", "New York"}, + {"Bob", "25", "San Francisco"}, + }) + writer.Flush() + }) + + It("should read the CSV file successfully", func() { + content, err := app.NewCSVFile().Read(filePath) + Expect(err).NotTo(HaveOccurred()) + + Expect(content).To(HaveLen(3)) // header + 2 rows + Expect(content[0]).To(Equal([]string{"name", "age", "city"})) + Expect(content[1]).To(Equal([]string{"Alice", "30", "New York"})) + Expect(content[2]).To(Equal([]string{"Bob", "25", "San Francisco"})) + }) + }) + + Context("when the CSV file starts like the original from thw", func() { + BeforeEach(func() { + csvContent := `"Ebene";"OE";"Art";"FB";"Menge";"Menge Ist";"Verfügbar";"Ausstattung | Hersteller | Typ";"Sachnummer";"Inventar Nr";"Gerätenr.";"Status" +;"OV Speyer";"";"";"";"";"";"1. Technischer Zug/Fachgruppe Notversorgung und Notinstandsetzung";"";"";"";"V" +1;"";"";"";"";"";"";"Geringwertiges Material";"";"";"";"V" +2;"";"Gwm";"";"";"1";"1";" Eiskratzer, handelsüblich";"2540T21171";"---------------";"--------------------";"V"` + + filePath = filepath.Join(os.TempDir(), "valid.csv") + file, err := os.Create(filePath) + Expect(err).NotTo(HaveOccurred()) + defer file.Close() + + writer := bufio.NewWriter(file) + _, err = writer.WriteString(csvContent) + Expect(err).NotTo(HaveOccurred()) + + err = writer.Flush() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should read the CSV file successfully", func() { + _, err := app.NewCSVFile().Read(filePath) + Expect(err).NotTo(HaveOccurred()) + + // Expect(content).To(HaveLen(3)) // header + 2 rows + // Expect(content[0]).To(Equal([]string{"name", "age", "city"})) + // Expect(content[1]).To(Equal([]string{"Alice", "30", "New York"})) + // Expect(content[2]).To(Equal([]string{"Bob", "25", "San Francisco"})) + }) + }) + + + + Context("when the file does not exist", func() { + It("should return an error", func() { + _, err := app.NewCSVFile().Read("nonexistent.csv") + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to open CSV file")) + }) + }) + + Context("when the CSV file is malformed", func() { + BeforeEach(func() { + // Create a temporary CSV file with malformed content + filePath = filepath.Join(os.TempDir(), "malformed.csv") + file, err := os.Create(filePath) + Expect(err).NotTo(HaveOccurred()) + defer file.Close() + + file.WriteString("name;age;city\nAlice;30\nBob;25;San Francisco") // Missing one field in Alice's row + }) + + It("should return an error when reading", func() { + _, err := app.NewCSVFile().Read(filePath) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to read CSV file")) + }) + }) + }) + + var _ = Describe("Write", func() { + var ( + content [][]string + ) + + BeforeEach(func() { + content = [][]string{ + {"name", "age", "city"}, + {"Alice", "30", "New York"}, + {"Bob", "25", "San Francisco"}, + } + }) + + Context("when the CSV file path is valid", func() { + BeforeEach(func() { + filePath = filepath.Join(os.TempDir(), "output.csv") + }) + + It("should write the content to the CSV file successfully", func() { + err := app.NewCSVFile().Write(filePath, content) + Expect(err).NotTo(HaveOccurred()) + + file, err := os.Open(filePath) + Expect(err).NotTo(HaveOccurred()) + defer file.Close() + + scanner := bufio.NewScanner(file) + + // \ufeff is the UTF-8 BOM for Excel on Windows compatibility + expectedLines := []string{ + "\ufeffname;age;city", + "Alice;30;New York", + "Bob;25;San Francisco", + } + + i := 0 + for scanner.Scan() { + Expect(scanner.Text()).To(Equal(expectedLines[i])) + i++ + } + Expect(scanner.Err()).NotTo(HaveOccurred()) + Expect(i).To(Equal(len(expectedLines))) + }) + }) + + Context("when the file path is invalid", func() { + It("should return an error", func() { + invalidPath := "/invalid/output.csv" // Likely to be invalid on most systems + err := app.NewCSVFile().Write(invalidPath, content) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to create CSV file")) + }) + }) + }) +}) diff --git a/app/inventory_data.go b/app/inventory_data.go new file mode 100644 index 0000000..a71c3b9 --- /dev/null +++ b/app/inventory_data.go @@ -0,0 +1,107 @@ +package app + +import ( + "fmt" + "strconv" + "strings" + "thwInventoryMerge/utils" +) + +type csvHeader map[string]int +type csvHeaderReverse map[int]string +type csvContent []map[string]string + +type InventoryData interface { + GetContent() [][]string + + UpdateInventory(recordedInventory RecordedInventoryMap) +} + +type inventoryData struct { + csvHeader csvHeader + csvHeaderReverse csvHeaderReverse + content csvContent + logger utils.Logger +} + +func NewInventoryData(data [][]string, logger utils.Logger) (InventoryData, error) { + + csvHeader := make(csvHeader) + + var content csvContent + for i := 0; i < len(data); i++ { + record := data[i] + + // create header index on first row + if i == 0 { + for i, colName := range record { + csvHeader[colName] = i + } + } + + row := make(map[string]string) + for colName, colIndex := range csvHeader { + if colIndex < len(record) { + row[colName] = record[colIndex] + } + } + content = append(content, row) + } + + csvHeaderReverse := make(csvHeaderReverse) + for key, value := range csvHeader { + csvHeaderReverse[value] = key + } + + return &inventoryData{ + csvHeader: csvHeader, + csvHeaderReverse: csvHeaderReverse, + content: content, + logger: logger, + }, nil +} + +func (c *inventoryData) GetContent() [][]string { + var result [][]string + + for _, row := range c.content { + var resultRow []string + + for i := 0; i < len(c.csvHeaderReverse); i++ { + resultRow = append(resultRow, row[c.csvHeaderReverse[i]]) + } + + result = append(result, resultRow) + } + + return result +} + +func (c *inventoryData) UpdateInventory(recordedInventory RecordedInventoryMap) { + + firstEquipment := true + + for inventory, amount := range recordedInventory { + inventoryFound := false + + for _, row := range c.content { + // ignore case comparison + if strings.EqualFold(row["Inventar Nr"], inventory) { + inventoryFound = true + row["Verfügbar"] = strconv.Itoa(amount) + } + } + + if !inventoryFound { + if firstEquipment { + c.logger.Info("recorded equipment not available in the inventory:") + c.logger.Info("") + c.logger.WarnIndented("equipment : amount") + c.logger.WarnIndented("----------------------") + firstEquipment = false + } + + c.logger.WarnIndented(fmt.Sprintf("%-13s : %5d", inventory, amount)) + } + } +} diff --git a/app/inventory_data_test.go b/app/inventory_data_test.go new file mode 100644 index 0000000..e970e9a --- /dev/null +++ b/app/inventory_data_test.go @@ -0,0 +1,118 @@ +package app_test + +import ( + "thwInventoryMerge/app" + "thwInventoryMerge/utils/utilsfakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("CSVFile", func() { + var _ = Describe("GetContent", func() { + It("returns the csv content", func() { + csvData := [][]string{ + {"Verfügbar", "Ausstattung", "Inventar Nr", "Status"}, + {"4", "Handlampe", "0591-S00001", "V"}, + {"1", "Fuchsschwanz", "", "V"}, + {"1", "Rettungsweste", "0591-S00002", "U"}} + + data, err := app.NewInventoryData(csvData, nil) + Expect(err).ToNot(HaveOccurred()) + + content := data.GetContent() + + Expect(content[0][0]).To(Equal("Verfügbar")) + Expect(content[0][1]).To(Equal("Ausstattung")) + Expect(content[0][2]).To(Equal("Inventar Nr")) + Expect(content[0][3]).To(Equal("Status")) + Expect(content[1][0]).To(Equal("4")) + Expect(content[1][1]).To(Equal("Handlampe")) + Expect(content[1][2]).To(Equal("0591-S00001")) + Expect(content[1][3]).To(Equal("V")) + Expect(content[2][0]).To(Equal("1")) + Expect(content[2][1]).To(Equal("Fuchsschwanz")) + Expect(content[2][2]).To(Equal("")) + Expect(content[2][3]).To(Equal("V")) + Expect(content[3][0]).To(Equal("1")) + Expect(content[3][1]).To(Equal("Rettungsweste")) + Expect(content[3][2]).To(Equal("0591-S00002")) + Expect(content[3][3]).To(Equal("U")) + }) + + It("preserve leading and trailing spaces", func() { + csvData := [][]string{ + {"Verfügbar", "Ausstattung", "Inventar Nr", "Status"}, + {"4", " Handlampe", " 0591-S00001 ", "V "}} + + data, err := app.NewInventoryData(csvData, nil) + Expect(err).ToNot(HaveOccurred()) + + content := data.GetContent() + + Expect(content[0][0]).To(Equal("Verfügbar")) + Expect(content[0][1]).To(Equal("Ausstattung")) + Expect(content[0][2]).To(Equal("Inventar Nr")) + Expect(content[0][3]).To(Equal("Status")) + Expect(content[1][0]).To(Equal("4")) + Expect(content[1][1]).To(Equal(" Handlampe")) + Expect(content[1][2]).To(Equal(" 0591-S00001 ")) + Expect(content[1][3]).To(Equal("V ")) + }) + }) + + var _ = Describe("UpdateInventory", func() { + It("updated the content", func() { + csvData := [][]string{ + {"Verfügbar", "Ausstattung", "Inventar Nr", "Status"}, + {"4", "Handlampe", "0591-S00001", "V"}, + {"1", "Fuchsschwanz", "1234", "V"}, + {"1", "Rettungsweste", "0591-S00002", "U"}} + + data, err := app.NewInventoryData(csvData, &utilsfakes.FakeLogger{}) + Expect(err).ToNot(HaveOccurred()) + + data.UpdateInventory(app.RecordedInventoryMap{ + "0591-S00001": 100, + "1234": 101, + "0591-S00002": 0, + }) + + content := data.GetContent() + + Expect(content[0][0]).To(Equal("Verfügbar")) + Expect(content[0][1]).To(Equal("Ausstattung")) + Expect(content[0][2]).To(Equal("Inventar Nr")) + Expect(content[0][3]).To(Equal("Status")) + Expect(content[1][0]).To(Equal("100")) + Expect(content[1][1]).To(Equal("Handlampe")) + Expect(content[1][2]).To(Equal("0591-S00001")) + Expect(content[1][3]).To(Equal("V")) + Expect(content[2][0]).To(Equal("101")) + Expect(content[2][1]).To(Equal("Fuchsschwanz")) + Expect(content[2][2]).To(Equal("1234")) + Expect(content[2][3]).To(Equal("V")) + Expect(content[3][0]).To(Equal("0")) + Expect(content[3][1]).To(Equal("Rettungsweste")) + Expect(content[3][2]).To(Equal("0591-S00002")) + Expect(content[3][3]).To(Equal("U")) + }) + + It("logs not existing equipment", func() { + logger := &utilsfakes.FakeLogger{} + + csvData := [][]string{ + {"Verfügbar", "Ausstattung", "Inventar Nr", "Status"}} + + data, err := app.NewInventoryData(csvData, logger) + Expect(err).ToNot(HaveOccurred()) + + data.UpdateInventory(app.RecordedInventoryMap{ + "not_existing": 1, + }) + + Expect(logger.WarnIndentedCallCount()).To(Equal(3)) + Expect(logger.WarnIndentedArgsForCall(2)).To(Equal("not_existing : 1")) + }) + }) +}) diff --git a/app/recorded_inventory.go b/app/recorded_inventory.go new file mode 100644 index 0000000..c1fae3b --- /dev/null +++ b/app/recorded_inventory.go @@ -0,0 +1,36 @@ +package app + +import ( + "strings" +) + +type RecordedInventoryMap map[string]int + +type RecordedInventory interface { + AsMap() (RecordedInventoryMap, error) +} + +type recordedInventory struct { + data []CSVContent +} + +func NewRecordedInventory(data []CSVContent) RecordedInventory { + return recordedInventory{ + data: data, + } +} + +func (r recordedInventory) AsMap() (RecordedInventoryMap, error) { + inventoryNumbers := make(RecordedInventoryMap) + + for _, csvContent := range r.data { + + for _, record := range csvContent { + if len(record) > 0 { + inventoryNumbers[strings.ToLower(record[0])]++ + } + } + } + + return inventoryNumbers, nil +} \ No newline at end of file diff --git a/app/recorded_inventory_test.go b/app/recorded_inventory_test.go new file mode 100644 index 0000000..5ebfdf8 --- /dev/null +++ b/app/recorded_inventory_test.go @@ -0,0 +1,57 @@ +package app_test + +import ( + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "thwInventoryMerge/app" +) + +var _ = Describe("RecordedInventory", func() { + var ( + csvFile1 *os.File + csvFile2 *os.File + err error + ) + + BeforeEach(func() { + csvFile1, err = os.CreateTemp("", "csv1.csv") + Expect(err).ToNot(HaveOccurred()) + + csvFile2, err = os.CreateTemp("", "csv2.csv") + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + os.Remove(csvFile1.Name()) + os.Remove(csvFile2.Name()) + }) + + var _ = Describe("GetRecordedInventory", func() { + It("returns the inventory recorded in multiple csv", func() { + recordedInventory := app.NewRecordedInventory( + []app.CSVContent{[][]string{ + {"0001-S001304"}, + {"0509-002494"}, + {"0591-S002360"}, + {"0509-002494"}, + }, [][]string{ + {"0591-002781"}, + {"0591-S002319"}, + {"0591-002781"}, + {"0591-S002319"}, + {"0591-002781"}, + }}) + inventoryMap, err := recordedInventory.AsMap() + Expect(err).ToNot(HaveOccurred()) + Expect(inventoryMap).To(HaveLen(5)) + Expect(inventoryMap).To(HaveKeyWithValue("0001-s001304", 1)) + Expect(inventoryMap).To(HaveKeyWithValue("0509-002494", 2)) + Expect(inventoryMap).To(HaveKeyWithValue("0591-s002360", 1)) + Expect(inventoryMap).To(HaveKeyWithValue("0591-002781", 3)) + Expect(inventoryMap).To(HaveKeyWithValue("0591-s002319", 2)) + }) + }) +}) diff --git a/app/update_excel.go b/app/update_excel.go deleted file mode 100644 index e8c9394..0000000 --- a/app/update_excel.go +++ /dev/null @@ -1,145 +0,0 @@ -package app - -import ( - "encoding/csv" - "fmt" - "os" - "strings" - "thwInventoryMerge/config" - - "github.com/xuri/excelize/v2" -) - -type UpdateExcel interface { - Update() error -} - -type updateExcel struct { - config config.Config -} - -func NewUpdateExcel(config config.Config) UpdateExcel { - return &updateExcel{ - config: config, - } -} - -func (u *updateExcel) Update() error { - recordedInventory, err := u.getRecordedInventory() - if err != nil { - return fmt.Errorf("failed to get recorded inventory: %w", err) - } - - excelFile, err := excelize.OpenFile(u.config.GetAbsoluteExcelFileName()) - if err != nil { - return fmt.Errorf("failed to open the Excel file: %w", err) - } - - rows, err := excelFile.GetRows(u.config.ExcelConfig.WorksheetName) - if err != nil { - return fmt.Errorf("failed to get rows from Excel file: %w", err) - } - - for rowIndex, row := range rows { - if rowIndex == 0 { - err := u.getHeaderIndices(row) - if err != nil { - return fmt.Errorf("failed to get headers from Excel file: %w", err) - } - continue - } - - // Check if the equipment ID exists in the inventory numbers - equipmentID := row[*u.config.ExcelConfig.EquipmentIDColumnIndex] - if count, exists := recordedInventory[strings.ToLower(equipmentID)]; exists { - - // Set the value in the specified cell - cell, err := excelize.CoordinatesToCellName(*u.config.ExcelConfig.EquipmentAvailableColumnIndex+1, rowIndex+1) - if err != nil { - return fmt.Errorf("failed to get cell from coordinates: %w", err) - } - excelFile.SetCellValue( - u.config.ExcelConfig.WorksheetName, - cell, - count, - ) - } - } - - if err := excelFile.Save(); err != nil { - return fmt.Errorf("failed to save the updated Excel file: %w", err) - } else { - fmt.Println("\nUpdated Excel file successfully.") - } - - return nil -} - -func (u *updateExcel) getHeaderIndices(row []string) error { - for j, col := range row { - if strings.EqualFold(col, u.config.ExcelConfig.EquipmentIDColumnName) { - u.config.ExcelConfig.EquipmentIDColumnIndex = &j - } else if strings.EqualFold(col, u.config.ExcelConfig.EquipmentAvailableColumnName) { - u.config.ExcelConfig.EquipmentAvailableColumnIndex = &j - } - } - - if u.config.ExcelConfig.EquipmentIDColumnIndex == nil { - return fmt.Errorf( - "failed to find header %s in first row of worksheet %s", - u.config.ExcelConfig.EquipmentIDColumnName, - u.config.ExcelConfig.WorksheetName, - ) - } - if u.config.ExcelConfig.EquipmentAvailableColumnIndex == nil { - return fmt.Errorf( - "failed to find header %s in first row of worksheet %s", - u.config.ExcelConfig.EquipmentAvailableColumnName, - u.config.ExcelConfig.WorksheetName, - ) - } - - return nil -} - -func (u *updateExcel) getRecordedInventory() (map[string]int, error) { - inventoryNumbers := make(map[string]int) - - csvFiles, err := u.config.GetCSVFiles() - if err != nil { - return nil, fmt.Errorf("failed to get CSV files: %w", err) - } - - for _, file := range csvFiles { - f, err := os.Open(file) - if err != nil { - return nil, fmt.Errorf("failed to open CSV file: %w", err) - } - defer f.Close() - - reader := csv.NewReader(f) - records, err := reader.ReadAll() - if err != nil { - return nil, fmt.Errorf("failed to read CSV file: %w", err) - } - - for _, record := range records { - if len(record) > 0 { - inventoryNumbers[strings.ToLower(record[0])]++ - } - } - } - - u.printInventoryNumbers(inventoryNumbers) - - return inventoryNumbers, nil -} - -func (u *updateExcel) printInventoryNumbers(inventoryNumbers map[string]int) { - fmt.Printf("%-20s %s\n", "Inventory Number", "Quantity") - fmt.Println(strings.Repeat("-", 30)) // Print a separator line - - for number, quantity := range inventoryNumbers { - fmt.Printf("%-20s %d\n", number, quantity) - } -} diff --git a/config/config.go b/config/config.go index 08cbbb6..8f63c51 100644 --- a/config/config.go +++ b/config/config.go @@ -6,21 +6,21 @@ import ( "fmt" "os" "path/filepath" + "thwInventoryMerge/utils" ) type Config struct { WorkingDir string `json:"working_dir"` - ExcelFileName string `json:"excel_file_name"` - ExcelConfig struct { - WorksheetName string `json:"worksheet_name"` + InventoryCSVFileName string `json:"inventory_csv_file_name"` + InventoryCSVConfig struct { EquipmentIDColumnName string `json:"equipment_id_column_name"` - EquipmentIDColumnIndex *int EquipmentAvailableColumnName string `json:"equipment_available_column_name"` - EquipmentAvailableColumnIndex *int - } `json:"excel_config"` + } `json:"inventory_csv_config"` + + logger utils.Logger } -func (c *Config) GetCSVFiles() ([]string, error) { +func (c *Config) GetCSVFilesWithRecordedEquipment() ([]string, error) { var csvFiles []string files, err := os.ReadDir(c.WorkingDir) @@ -28,26 +28,42 @@ func (c *Config) GetCSVFiles() ([]string, error) { return nil, err } + firstEquipment := true + for _, file := range files { - if !file.IsDir() && filepath.Ext(file.Name()) == ".csv" { + if !file.IsDir() && + filepath.Ext(file.Name()) == ".csv" && + filepath.Base(file.Name()) != c.InventoryCSVFileName { + + if firstEquipment { + c.logger.Info("files with recorded equipment:") + c.logger.Info("") + firstEquipment = false + } + + c.logger.InfoIndented(fmt.Sprintf("using '%s'", file.Name())) csvFiles = append(csvFiles, filepath.Join(c.WorkingDir, file.Name())) } } + c.logger.Info("") + return csvFiles, nil } -func (c *Config) GetAbsoluteExcelFileName() string { - return filepath.Join(c.WorkingDir, c.ExcelFileName) +func (c *Config) GetAbsoluteInventoryCSVFileName() string { + return filepath.Join(c.WorkingDir, c.InventoryCSVFileName) } -func LoadConfig(filePath string) (*Config, error) { +func LoadConfig(filePath string, logger utils.Logger) (*Config, error) { data, err := os.ReadFile(filePath) if err != nil { return nil, err } - var config Config + var config = Config{ + logger: logger, + } err = json.Unmarshal(data, &config) if err != nil { @@ -63,17 +79,14 @@ func LoadConfig(filePath string) (*Config, error) { } func (c Config) validate() error { - if c.ExcelFileName == "" { - return errors.New("property excel_file_name is required") - } - if c.ExcelConfig.WorksheetName == "" { - return errors.New("property excel_config.worksheet_name is required") + if c.InventoryCSVFileName == "" { + return errors.New("property inventory_csv_file_name is required") } - if c.ExcelConfig.EquipmentIDColumnName == "" { - return errors.New("property excel_config.equipment_id_column_name is required") + if c.InventoryCSVConfig.EquipmentIDColumnName == "" { + return errors.New("property inventory_csv_config.equipment_id_column_name is required") } - if c.ExcelConfig.EquipmentAvailableColumnName == "" { - return errors.New("property excel_config.equipment_available_column_name is required") + if c.InventoryCSVConfig.EquipmentAvailableColumnName == "" { + return errors.New("property inventory_csv_config.equipment_available_column_name is required") } return nil } diff --git a/config/config_test.go b/config/config_test.go index 3f1de0d..8fcd132 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,12 +1,16 @@ package config_test import ( + "fmt" "os" + "path/filepath" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "thwInventoryMerge/config" + "thwInventoryMerge/utils/utilsfakes" ) var _ = Describe("Config", func() { @@ -25,108 +29,131 @@ var _ = Describe("Config", func() { }) var _ = Describe("LoadConfig", func() { - It("should load the configuration", func() { jsonContent := ` -{ - "working_dir": "foo_working_dir", - "excel_file_name": "foo_excel_file_name", - "excel_config": { - "worksheet_name": "foo_worksheet_name", - "equipment_id_column_name": "foo_equipment_id_column_name", - "equipment_available_column_name": "foo_equipment_available_column_name", - "equipment_available_value": "foo_equipment_available_value" + { + "working_dir": "foo_working_dir", + "inventory_csv_file_name": "foo_inventory_csv_file_name", + "inventory_csv_config": { + "equipment_id_column_name": "foo_equipment_id_column_name", + "equipment_available_column_name": "foo_equipment_available_column_name" + } } -} -` + ` _, err := tempFile.Write([]byte(jsonContent)) Expect(err).ToNot(HaveOccurred()) tempFile.Close() - cfg, err := config.LoadConfig(tempFile.Name()) + cfg, err := config.LoadConfig(tempFile.Name(), nil) Expect(err).ToNot(HaveOccurred()) Expect(cfg.WorkingDir).To(Equal("foo_working_dir")) - Expect(cfg.ExcelFileName).To(Equal("foo_excel_file_name")) - Expect(cfg.ExcelConfig.WorksheetName).To(Equal("foo_worksheet_name")) - Expect(cfg.ExcelConfig.EquipmentIDColumnName).To(Equal("foo_equipment_id_column_name")) - Expect(cfg.ExcelConfig.EquipmentAvailableColumnName).To(Equal("foo_equipment_available_column_name")) + Expect(cfg.InventoryCSVFileName).To(Equal("foo_inventory_csv_file_name")) + Expect(cfg.InventoryCSVConfig.EquipmentIDColumnName).To(Equal("foo_equipment_id_column_name")) + Expect(cfg.InventoryCSVConfig.EquipmentAvailableColumnName).To(Equal("foo_equipment_available_column_name")) }) var _ = Describe("config errors", func() { - It("returns an error if mandatory excel_file_name is missing", func() { + It("returns an error if mandatory inventory_csv_file_name is missing", func() { jsonContent := ` -{ - "working_dir": "foo_working_dir" -} -` + { + "working_dir": "foo_working_dir" + } + ` _, err := tempFile.Write([]byte(jsonContent)) Expect(err).ToNot(HaveOccurred()) tempFile.Close() - cfg, err := config.LoadConfig(tempFile.Name()) + cfg, err := config.LoadConfig(tempFile.Name(), nil) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("failed to validate the config file, property excel_file_name is required")) + Expect(err.Error()).To(Equal("failed to validate the config file, property inventory_csv_file_name is required")) Expect(cfg).To(BeNil()) }) }) - It("returns an error if mandatory excel_config,worksheet_name", func() { + It("returns an error if mandatory inventory_csv_config,equipment_id_column_name is missing", func() { jsonContent := ` -{ - "working_dir": "foo_working_dir", - "excel_file_name": "foo_excel_file_name" -} -` + { + "working_dir": "foo_working_dir", + "inventory_csv_file_name": "foo_inventory_csv_file_name", + "inventory_csv_config": { + "worksheet_name": "foo_worksheet_name" + } + } + ` _, err := tempFile.Write([]byte(jsonContent)) Expect(err).ToNot(HaveOccurred()) tempFile.Close() - cfg, err := config.LoadConfig(tempFile.Name()) + cfg, err := config.LoadConfig(tempFile.Name(), nil) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("failed to validate the config file, property excel_config.worksheet_name is required")) + Expect(err.Error()).To(Equal("failed to validate the config file, property inventory_csv_config.equipment_id_column_name is required")) Expect(cfg).To(BeNil()) }) - It("returns an error if mandatory excel_config,equipment_id_column_name", func() { + It("returns an error if mandatory excel_config,equipment_available_column_name is missing", func() { jsonContent := ` -{ - "working_dir": "foo_working_dir", - "excel_file_name": "foo_excel_file_name", - "excel_config": { - "worksheet_name": "foo_worksheet_name" - } -} -` + { + "working_dir": "foo_working_dir", + "inventory_csv_file_name": "foo_inventory_csv_file_name", + "inventory_csv_config": { + "equipment_id_column_name": "foo_equipment_id_column_name" + } + } + ` _, err := tempFile.Write([]byte(jsonContent)) Expect(err).ToNot(HaveOccurred()) tempFile.Close() - cfg, err := config.LoadConfig(tempFile.Name()) + cfg, err := config.LoadConfig(tempFile.Name(), nil) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("failed to validate the config file, property excel_config.equipment_id_column_name is required")) + Expect(err.Error()).To(Equal("failed to validate the config file, property inventory_csv_config.equipment_available_column_name is required")) Expect(cfg).To(BeNil()) }) - }) - It("returns an error if mandatory excel_config,equipment_available_column_name", func() { - jsonContent := ` + var _ = Describe("GetCSVFilesWithRecordedEquipment", func() { + It("should return the CSV files", func() { + + tempDir, err := os.MkdirTemp("", "test-csv") + Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(tempDir) + + fileNames := []string{"file1.csv", "file2.csv", "inventory_fgr_n.csv", "file3.csv"} + for _, fileName := range fileNames { + filePath := filepath.Join(tempDir, fileName) + file, err := os.Create(filePath) + Expect(err).ToNot(HaveOccurred()) + file.Close() + } + + jsonContent := fmt.Sprintf(` { - "working_dir": "foo_working_dir", - "excel_file_name": "foo_excel_file_name", - "excel_config": { - "worksheet_name": "foo_worksheet_name", - "equipment_id_column_name": "foo_equipment_id_column_name" + "working_dir": "%s", + "inventory_csv_file_name": "inventory_fgr_n.csv", + "inventory_csv_config": { + "equipment_id_column_name": "foo_equipment_id_column_name", + "equipment_available_column_name": "foo_equipment_available_column_name" } } -` - _, err := tempFile.Write([]byte(jsonContent)) - Expect(err).ToNot(HaveOccurred()) - tempFile.Close() +`, strings.ReplaceAll(tempDir, "\\", "\\\\")) - cfg, err := config.LoadConfig(tempFile.Name()) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("failed to validate the config file, property excel_config.equipment_available_column_name is required")) - Expect(cfg).To(BeNil()) + fmt.Println(jsonContent) + _, err = tempFile.Write([]byte(jsonContent)) + Expect(err).ToNot(HaveOccurred()) + tempFile.Close() + + logger := &utilsfakes.FakeLogger{} + + cfg, err := config.LoadConfig(tempFile.Name(), logger) + Expect(err).ToNot(HaveOccurred()) + + files, err := cfg.GetCSVFilesWithRecordedEquipment() + Expect(err).ToNot(HaveOccurred()) + Expect(files).To(HaveLen(3)) + Expect(files).To(ContainElement(filepath.Join(tempDir, "file1.csv"))) + Expect(files).To(ContainElement(filepath.Join(tempDir, "file2.csv"))) + Expect(files).To(ContainElement(filepath.Join(tempDir, "file3.csv"))) + }) }) + }) diff --git a/go.mod b/go.mod index 8a4e3db..8e163df 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module thwInventoryMerge go 1.23.2 require ( + github.com/maxbrunsfeld/counterfeiter/v6 v6.9.0 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 - github.com/xuri/excelize/v2 v2.9.0 ) require ( @@ -13,15 +13,12 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/richardlehane/mscfb v1.0.4 // indirect - github.com/richardlehane/msoleps v1.0.4 // indirect - github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect - github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect - golang.org/x/crypto v0.28.0 // indirect + github.com/kr/pretty v0.2.1 // indirect + golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect - golang.org/x/tools v0.24.0 // indirect + golang.org/x/tools v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3a86251..d713bb6 100644 --- a/go.sum +++ b/go.sum @@ -8,42 +8,39 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/maxbrunsfeld/counterfeiter/v6 v6.9.0 h1:ERhc+PJKEyqWQnKu7/K0frSVGFihYYImqNdqP5r0cN0= +github.com/maxbrunsfeld/counterfeiter/v6 v6.9.0/go.mod h1:tU2wQdIyJ7fib/YXxFR0dgLlFz3yl4p275UfUKmDFjk= github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= -github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= -github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= -github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= -github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= +github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= -github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= -github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= -github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= -github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= -golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 74581f2..bdba4a2 100644 --- a/main.go +++ b/main.go @@ -8,15 +8,20 @@ import ( "path/filepath" "thwInventoryMerge/app" "thwInventoryMerge/config" + "thwInventoryMerge/utils" + "time" ) func main() { + + logger := utils.NewLogger() + var configPath string flag.StringVar(&configPath, "c", "config.json", "the config file path") flag.Parse() - config, err := config.LoadConfig(configPath) + config, err := config.LoadConfig(configPath, logger) if err != nil { log.Fatalf("Failed to load config from path %s: %v", configPath, err) } @@ -29,9 +34,63 @@ func main() { filepath.Join(config.WorkingDir, "config.json") } - err = app.NewUpdateExcel(*config).Update() + csvFiles, err := config.GetCSVFilesWithRecordedEquipment() + if err != nil { + log.Fatalf("Failed to get CSV files: %v", err) + } + + csvFile := app.NewCSVFile() + + var recordedInventoryData []app.CSVContent + for _, file := range csvFiles { + content, err := csvFile.Read(file) + if err != nil { + log.Fatalf("Failed to read CSV file '%s': %v", file, err) + } + recordedInventoryData = append(recordedInventoryData, content) + } + + recordedInventory := app.NewRecordedInventory(recordedInventoryData) + + content, err := csvFile.Read(config.GetAbsoluteInventoryCSVFileName()) + if err != nil { + log.Fatalf("Failed to read CSV file '%s': %v", config.GetAbsoluteInventoryCSVFileName(), err) + } + + inventoryData, err := app.NewInventoryData(content, logger) + if err != nil { + log.Fatalf("Failed to init inventory data: %v", err) + } + + inventoryMap, err := recordedInventory.AsMap() + if err != nil { + log.Fatalf("Failed to convert recorded inventory to map: %v", err) + } + + logger.Info("recorded equipment:") + logger.Info("") + logger.InfoIndented("equipment : amount") + logger.InfoIndented("----------------------") + for key, value := range inventoryMap { + logger.InfoIndented(fmt.Sprintf("%-13s : %5d", key, value)) + } + logger.Info("") + + inventoryData.UpdateInventory(inventoryMap) + + resultDir := filepath.Join(config.WorkingDir, "result") + + err = os.MkdirAll(resultDir, 0755) + if err != nil { + log.Fatalf("Failed to create result directory: %v", err) + } + + err = csvFile.Write( + filepath.Join(resultDir, fmt.Sprintf("result_%s.csv", time.Now().Format("2006-01-02_15-04-05"))), + inventoryData.GetContent(), + ) if err != nil { - log.Fatalf("Failed to update Excel: %v", err) + log.Fatalf("Failed to write result csv: %v", err) } // Keep the terminal open diff --git a/scripts/generate_fakes b/scripts/generate_fakes new file mode 100644 index 0000000..2d32bed --- /dev/null +++ b/scripts/generate_fakes @@ -0,0 +1,21 @@ +#!/bin/bash + +# Get the directory of the script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Navigate to the project root (assuming the script is in the scripts folder) +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_ROOT" + +# Define the pattern to identify fakes folders +FAKE_FOLDER_PATTERN="*fakes" + +# Find and delete all fakes folders +echo "Searching for and deleting fakes folders..." +find . -type d -name "$FAKE_FOLDER_PATTERN" -exec rm -rf {} + + +# Run go generate to regenerate the fakes +echo "Running go generate to regenerate fakes..." +go generate ./... + +echo "Done." \ No newline at end of file diff --git a/tools/tools.go b/tools/tools.go new file mode 100644 index 0000000..03298d0 --- /dev/null +++ b/tools/tools.go @@ -0,0 +1,10 @@ +//go:build tools + +package tools + +import ( + _ "github.com/maxbrunsfeld/counterfeiter/v6" +) + +// This file imports packages that are used when running go generate, or used +// during the development process but not otherwise depended on by built code. \ No newline at end of file diff --git a/utils/generate.go b/utils/generate.go new file mode 100644 index 0000000..ae5f206 --- /dev/null +++ b/utils/generate.go @@ -0,0 +1,5 @@ +package utils + +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate + +func init() {} \ No newline at end of file diff --git a/utils/logger.go b/utils/logger.go new file mode 100644 index 0000000..94c2b37 --- /dev/null +++ b/utils/logger.go @@ -0,0 +1,44 @@ +package utils + +import ( + "log" +) + +//counterfeiter:generate . Logger +type Logger interface { + Info(message string) + + InfoIndented(message string) + + Warn(message string) + + WarnIndented(message string) + + Error(message string) +} + +type logger struct {} + +func NewLogger() Logger { + return logger{} +} + +func (l logger) Info(message string) { + log.Println("[INFO] "+ message) +} + +func (l logger) InfoIndented(message string) { + log.Println("[INFO] "+ message) +} + +func (l logger) Warn(message string) { + log.Println("[WARN] "+ message) +} + +func (l logger) WarnIndented(message string) { + log.Println("[WARN] "+ message) +} + +func (l logger) Error(message string) { + log.Println("[ERROR] "+ message) +} \ No newline at end of file diff --git a/utils/utils_suite_test.go b/utils/utils_suite_test.go new file mode 100644 index 0000000..f5fcf0c --- /dev/null +++ b/utils/utils_suite_test.go @@ -0,0 +1,13 @@ +package utils_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Utils Suite") +} \ No newline at end of file diff --git a/utils/utilsfakes/fake_logger.go b/utils/utilsfakes/fake_logger.go new file mode 100644 index 0000000..33ee5e2 --- /dev/null +++ b/utils/utilsfakes/fake_logger.go @@ -0,0 +1,231 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package utilsfakes + +import ( + "sync" + "thwInventoryMerge/utils" +) + +type FakeLogger struct { + ErrorStub func(string) + errorMutex sync.RWMutex + errorArgsForCall []struct { + arg1 string + } + InfoStub func(string) + infoMutex sync.RWMutex + infoArgsForCall []struct { + arg1 string + } + InfoIndentedStub func(string) + infoIndentedMutex sync.RWMutex + infoIndentedArgsForCall []struct { + arg1 string + } + WarnStub func(string) + warnMutex sync.RWMutex + warnArgsForCall []struct { + arg1 string + } + WarnIndentedStub func(string) + warnIndentedMutex sync.RWMutex + warnIndentedArgsForCall []struct { + arg1 string + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeLogger) Error(arg1 string) { + fake.errorMutex.Lock() + fake.errorArgsForCall = append(fake.errorArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.ErrorStub + fake.recordInvocation("Error", []interface{}{arg1}) + fake.errorMutex.Unlock() + if stub != nil { + fake.ErrorStub(arg1) + } +} + +func (fake *FakeLogger) ErrorCallCount() int { + fake.errorMutex.RLock() + defer fake.errorMutex.RUnlock() + return len(fake.errorArgsForCall) +} + +func (fake *FakeLogger) ErrorCalls(stub func(string)) { + fake.errorMutex.Lock() + defer fake.errorMutex.Unlock() + fake.ErrorStub = stub +} + +func (fake *FakeLogger) ErrorArgsForCall(i int) string { + fake.errorMutex.RLock() + defer fake.errorMutex.RUnlock() + argsForCall := fake.errorArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeLogger) Info(arg1 string) { + fake.infoMutex.Lock() + fake.infoArgsForCall = append(fake.infoArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.InfoStub + fake.recordInvocation("Info", []interface{}{arg1}) + fake.infoMutex.Unlock() + if stub != nil { + fake.InfoStub(arg1) + } +} + +func (fake *FakeLogger) InfoCallCount() int { + fake.infoMutex.RLock() + defer fake.infoMutex.RUnlock() + return len(fake.infoArgsForCall) +} + +func (fake *FakeLogger) InfoCalls(stub func(string)) { + fake.infoMutex.Lock() + defer fake.infoMutex.Unlock() + fake.InfoStub = stub +} + +func (fake *FakeLogger) InfoArgsForCall(i int) string { + fake.infoMutex.RLock() + defer fake.infoMutex.RUnlock() + argsForCall := fake.infoArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeLogger) InfoIndented(arg1 string) { + fake.infoIndentedMutex.Lock() + fake.infoIndentedArgsForCall = append(fake.infoIndentedArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.InfoIndentedStub + fake.recordInvocation("InfoIndented", []interface{}{arg1}) + fake.infoIndentedMutex.Unlock() + if stub != nil { + fake.InfoIndentedStub(arg1) + } +} + +func (fake *FakeLogger) InfoIndentedCallCount() int { + fake.infoIndentedMutex.RLock() + defer fake.infoIndentedMutex.RUnlock() + return len(fake.infoIndentedArgsForCall) +} + +func (fake *FakeLogger) InfoIndentedCalls(stub func(string)) { + fake.infoIndentedMutex.Lock() + defer fake.infoIndentedMutex.Unlock() + fake.InfoIndentedStub = stub +} + +func (fake *FakeLogger) InfoIndentedArgsForCall(i int) string { + fake.infoIndentedMutex.RLock() + defer fake.infoIndentedMutex.RUnlock() + argsForCall := fake.infoIndentedArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeLogger) Warn(arg1 string) { + fake.warnMutex.Lock() + fake.warnArgsForCall = append(fake.warnArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.WarnStub + fake.recordInvocation("Warn", []interface{}{arg1}) + fake.warnMutex.Unlock() + if stub != nil { + fake.WarnStub(arg1) + } +} + +func (fake *FakeLogger) WarnCallCount() int { + fake.warnMutex.RLock() + defer fake.warnMutex.RUnlock() + return len(fake.warnArgsForCall) +} + +func (fake *FakeLogger) WarnCalls(stub func(string)) { + fake.warnMutex.Lock() + defer fake.warnMutex.Unlock() + fake.WarnStub = stub +} + +func (fake *FakeLogger) WarnArgsForCall(i int) string { + fake.warnMutex.RLock() + defer fake.warnMutex.RUnlock() + argsForCall := fake.warnArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeLogger) WarnIndented(arg1 string) { + fake.warnIndentedMutex.Lock() + fake.warnIndentedArgsForCall = append(fake.warnIndentedArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.WarnIndentedStub + fake.recordInvocation("WarnIndented", []interface{}{arg1}) + fake.warnIndentedMutex.Unlock() + if stub != nil { + fake.WarnIndentedStub(arg1) + } +} + +func (fake *FakeLogger) WarnIndentedCallCount() int { + fake.warnIndentedMutex.RLock() + defer fake.warnIndentedMutex.RUnlock() + return len(fake.warnIndentedArgsForCall) +} + +func (fake *FakeLogger) WarnIndentedCalls(stub func(string)) { + fake.warnIndentedMutex.Lock() + defer fake.warnIndentedMutex.Unlock() + fake.WarnIndentedStub = stub +} + +func (fake *FakeLogger) WarnIndentedArgsForCall(i int) string { + fake.warnIndentedMutex.RLock() + defer fake.warnIndentedMutex.RUnlock() + argsForCall := fake.warnIndentedArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeLogger) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.errorMutex.RLock() + defer fake.errorMutex.RUnlock() + fake.infoMutex.RLock() + defer fake.infoMutex.RUnlock() + fake.infoIndentedMutex.RLock() + defer fake.infoIndentedMutex.RUnlock() + fake.warnMutex.RLock() + defer fake.warnMutex.RUnlock() + fake.warnIndentedMutex.RLock() + defer fake.warnIndentedMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeLogger) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ utils.Logger = new(FakeLogger)