From e8842664cbfc3b092296b2c74e9cd5fdaf6a86ef Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 9 Sep 2024 01:55:59 +0300 Subject: [PATCH 01/31] feat: [LoadFromString] Minimal code for the test to work version 1 --- go.mod | 3 +++ pkg/ex.txt | 10 ++++++++++ pkg/iniparser.go | 37 +++++++++++++++++++++++++++++++++++++ pkg/iniparser_test.go | 26 ++++++++++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 go.mod create mode 100644 pkg/ex.txt create mode 100644 pkg/iniparser.go create mode 100644 pkg/iniparser_test.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8bbc76e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/codescalersinternships/RawanMostafa-inigo + +go 1.23.1 diff --git a/pkg/ex.txt b/pkg/ex.txt new file mode 100644 index 0000000..7cdf01a --- /dev/null +++ b/pkg/ex.txt @@ -0,0 +1,10 @@ +; last modified 1 April 2001 by John Doe +[owner] +name = John Doe +organization = Acme Widgets Inc. + +[database] +; use IP address in case network name resolution is not working +server = 192.0.2.62 +port = 143 +file = "payroll.dat" \ No newline at end of file diff --git a/pkg/iniparser.go b/pkg/iniparser.go new file mode 100644 index 0000000..e54e431 --- /dev/null +++ b/pkg/iniparser.go @@ -0,0 +1,37 @@ +package iniparser + +import ( + "log" + "strings" +) + +type section struct { + map_ map[string]string +} + +type iniParser struct { + sections map[string]section +} + +func InitParser() iniParser { + return iniParser{ + make(map[string]section), + } +} + +func (i *iniParser) LoadFromString(file string) { + + lines := strings.Split(file, "\n") + + for _, line := range lines { + var title string + var section section + line := strings.TrimSpace(line) + if strings.Contains(line, "[") && strings.Contains(line, "]") { + title = strings.Trim(line, "[]") + } + + log.Printf(title, section) + } + +} diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go new file mode 100644 index 0000000..07d832c --- /dev/null +++ b/pkg/iniparser_test.go @@ -0,0 +1,26 @@ +package iniparser + +import ( + "reflect" + "testing" +) + +func TestLoadFromString(t *testing.T) { + s := `; last modified 1 April 2001 by John Doe + [owner] + name = John Doe + organization = Acme Widgets Inc.` + + section1 := section{make(map[string]string)} + section1.map_["name"] = "John Doe" + section1.map_["organization"] = "Acme Widgets Inc." + expected := make(map[string]section) + expected["owner"] = section1 + + parser := InitParser() + parser.LoadFromString(s) + + if !reflect.DeepEqual(parser.sections, expected) { + t.Errorf("Error! wanted:%v \n , got : %v \n", expected, parser.sections) + } +} From 1722150982ff7249e8c0c315729bcc9dfef9fe89 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 9 Sep 2024 11:35:55 +0300 Subject: [PATCH 02/31] feat: Completing the logic of LoadFromString and its test --- pkg/iniparser.go | 23 ++++++++++++++++------- pkg/iniparser_test.go | 30 ++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index e54e431..7e90fc5 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -1,7 +1,6 @@ package iniparser import ( - "log" "strings" ) @@ -23,15 +22,25 @@ func (i *iniParser) LoadFromString(file string) { lines := strings.Split(file, "\n") + var title string + var sec section for _, line := range lines { - var title string - var section section - line := strings.TrimSpace(line) + line = strings.TrimSpace(line) if strings.Contains(line, "[") && strings.Contains(line, "]") { + if title != "" { + i.sections[title] = sec + } title = strings.Trim(line, "[]") + title = strings.TrimSpace(title) + sec = section{map_: make(map[string]string)} + } else if strings.Contains(line, "=") { + parts := strings.Split(line, "=") + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + sec.map_[key] = value } - - log.Printf(title, section) } - + if title != "" { + i.sections[title] = sec + } } diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 07d832c..38c83ba 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -9,13 +9,31 @@ func TestLoadFromString(t *testing.T) { s := `; last modified 1 April 2001 by John Doe [owner] name = John Doe - organization = Acme Widgets Inc.` + organization = Acme Widgets Inc. - section1 := section{make(map[string]string)} - section1.map_["name"] = "John Doe" - section1.map_["organization"] = "Acme Widgets Inc." - expected := make(map[string]section) - expected["owner"] = section1 + [database] + ; use IP address in case network name resolution is not working + server = 192.0.2.62 + port = 143` + + var expected map[string]section + t.Run("populate expected map", func(t *testing.T) { + t.Helper() + expected = map[string]section{ + "owner": { + map_: map[string]string{ + "name": "John Doe", + "organization": "Acme Widgets Inc.", + }, + }, + "database": { + map_: map[string]string{ + "server": "192.0.2.62", + "port": "143", + }, + }, + } + }) parser := InitParser() parser.LoadFromString(s) From 4ebf588fe4be01f5bcefbb10e138b1b8c2bfcbf2 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 9 Sep 2024 12:47:02 +0300 Subject: [PATCH 03/31] feat: LoadFromFile and its test added --- pkg/ex.txt | 10 -------- pkg/iniparser.go | 22 ++++++++++++++---- pkg/iniparser_test.go | 53 +++++++++++++++++++++++++++---------------- pkg/testdata/file.ini | 7 ++++++ 4 files changed, 59 insertions(+), 33 deletions(-) delete mode 100644 pkg/ex.txt create mode 100644 pkg/testdata/file.ini diff --git a/pkg/ex.txt b/pkg/ex.txt deleted file mode 100644 index 7cdf01a..0000000 --- a/pkg/ex.txt +++ /dev/null @@ -1,10 +0,0 @@ -; last modified 1 April 2001 by John Doe -[owner] -name = John Doe -organization = Acme Widgets Inc. - -[database] -; use IP address in case network name resolution is not working -server = 192.0.2.62 -port = 143 -file = "payroll.dat" \ No newline at end of file diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 7e90fc5..66a1633 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -1,6 +1,8 @@ package iniparser import ( + "log" + "os" "strings" ) @@ -18,10 +20,7 @@ func InitParser() iniParser { } } -func (i *iniParser) LoadFromString(file string) { - - lines := strings.Split(file, "\n") - +func (i *iniParser) populateINI(lines []string) { var title string var sec section for _, line := range lines { @@ -44,3 +43,18 @@ func (i *iniParser) LoadFromString(file string) { i.sections[title] = sec } } + +func (i *iniParser) LoadFromString(file string) { + + lines := strings.Split(file, "\n") + i.populateINI(lines) +} + +func (i *iniParser) LoadFromFile(path string) { + + data, err := os.ReadFile(path) + if err != nil { + log.Fatal("Error in reading the file") + } + i.LoadFromString(string(data)) +} diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 38c83ba..91e397d 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -5,6 +5,27 @@ import ( "testing" ) +const path = "testdata/file.ini" + +func populateExpected(t *testing.T) map[string]section { + t.Helper() + expected := map[string]section{ + "owner": { + map_: map[string]string{ + "name": "John Doe", + "organization": "Acme Widgets Inc.", + }, + }, + "database": { + map_: map[string]string{ + "server": "192.0.2.62", + "port": "143", + }, + }, + } + return expected +} + func TestLoadFromString(t *testing.T) { s := `; last modified 1 April 2001 by John Doe [owner] @@ -15,28 +36,22 @@ func TestLoadFromString(t *testing.T) { ; use IP address in case network name resolution is not working server = 192.0.2.62 port = 143` + parser := InitParser() + parser.LoadFromString(s) - var expected map[string]section - t.Run("populate expected map", func(t *testing.T) { - t.Helper() - expected = map[string]section{ - "owner": { - map_: map[string]string{ - "name": "John Doe", - "organization": "Acme Widgets Inc.", - }, - }, - "database": { - map_: map[string]string{ - "server": "192.0.2.62", - "port": "143", - }, - }, - } - }) + expected := populateExpected(t) + + if !reflect.DeepEqual(parser.sections, expected) { + t.Errorf("Error! wanted:%v \n , got : %v \n", expected, parser.sections) + } +} + +func TestLoadFromFile(t *testing.T) { + + expected := populateExpected(t) parser := InitParser() - parser.LoadFromString(s) + parser.LoadFromFile(path) if !reflect.DeepEqual(parser.sections, expected) { t.Errorf("Error! wanted:%v \n , got : %v \n", expected, parser.sections) diff --git a/pkg/testdata/file.ini b/pkg/testdata/file.ini new file mode 100644 index 0000000..41b3644 --- /dev/null +++ b/pkg/testdata/file.ini @@ -0,0 +1,7 @@ +[owner] +name = John Doe +organization = Acme Widgets Inc. + +[database] +server = 192.0.2.62 +port = 143 From 630d791453c437d18dcf5d7e5f426c0f7dea9794 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 9 Sep 2024 14:20:42 +0300 Subject: [PATCH 04/31] feat: GetSectionNames added --- pkg/iniparser.go | 8 ++++++++ pkg/iniparser_test.go | 14 ++++++++++++++ pkg/testdata/file.ini | 3 ++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 66a1633..46b9061 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -58,3 +58,11 @@ func (i *iniParser) LoadFromFile(path string) { } i.LoadFromString(string(data)) } + +func (i *iniParser) GetSectionNames() []string { + names := make([]string, 0) + for key := range i.sections { + names = append(names, key) + } + return names +} diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 91e397d..f6f0fd4 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -57,3 +57,17 @@ func TestLoadFromFile(t *testing.T) { t.Errorf("Error! wanted:%v \n , got : %v \n", expected, parser.sections) } } + +func TestGetSectionNames(t *testing.T) { + parser := InitParser() + parser.LoadFromFile(path) + names := parser.GetSectionNames() + + expected := []string{"owner", "database"} + + + if !reflect.DeepEqual(names, expected) { + t.Errorf("Error! wanted:%q \n , got:%q \n", expected, names) + } + +} diff --git a/pkg/testdata/file.ini b/pkg/testdata/file.ini index 41b3644..f49dea2 100644 --- a/pkg/testdata/file.ini +++ b/pkg/testdata/file.ini @@ -1,7 +1,8 @@ +;another sample comment [owner] name = John Doe organization = Acme Widgets Inc. - +;this is a sample comment [database] server = 192.0.2.62 port = 143 From 1d9ff9e1c4b25801f69053f1473c32f8c504f5a0 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 9 Sep 2024 15:34:12 +0300 Subject: [PATCH 05/31] refactor: adding corner case for empty section --- pkg/iniparser.go | 6 +++ pkg/iniparser_test.go | 88 ++++++++++++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 46b9061..f153986 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -24,7 +24,11 @@ func (i *iniParser) populateINI(lines []string) { var title string var sec section for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, ";") { + continue + } if strings.Contains(line, "[") && strings.Contains(line, "]") { if title != "" { i.sections[title] = sec @@ -32,7 +36,9 @@ func (i *iniParser) populateINI(lines []string) { title = strings.Trim(line, "[]") title = strings.TrimSpace(title) sec = section{map_: make(map[string]string)} + } else if strings.Contains(line, "=") { + parts := strings.Split(line, "=") key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index f6f0fd4..3eb8047 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -7,7 +7,7 @@ import ( const path = "testdata/file.ini" -func populateExpected(t *testing.T) map[string]section { +func populateExpectedNormal(t *testing.T) map[string]section { t.Helper() expected := map[string]section{ "owner": { @@ -26,36 +26,76 @@ func populateExpected(t *testing.T) map[string]section { return expected } -func TestLoadFromString(t *testing.T) { - s := `; last modified 1 April 2001 by John Doe - [owner] - name = John Doe - organization = Acme Widgets Inc. - - [database] - ; use IP address in case network name resolution is not working - server = 192.0.2.62 - port = 143` - parser := InitParser() - parser.LoadFromString(s) - - expected := populateExpected(t) +func populateExpectedEmptySection(t *testing.T) map[string]section { + t.Helper() + expected := map[string]section{ + "owner": { + map_: map[string]string{ + "name": "John Doe", + "organization": "Acme Widgets Inc.", + }, + }, + "database": { + map_: map[string]string{}, + }, + } + return expected +} - if !reflect.DeepEqual(parser.sections, expected) { - t.Errorf("Error! wanted:%v \n , got : %v \n", expected, parser.sections) +func assertEquality(t *testing.T, obj1 any, obj2 any) { + t.Helper() + if reflect.TypeOf(obj1) != reflect.TypeOf(obj2) { + t.Errorf("Error! type mismatch, wanted %t got %t", reflect.TypeOf(obj1), reflect.TypeOf(obj2)) + } + if !reflect.DeepEqual(obj1, obj2) { + t.Errorf("Error! values mismatch, wanted %v got %v", obj1, obj2) } } +func TestLoadFromString(t *testing.T) { + t.Run("test normal ini file", func(t *testing.T) { + s := `; last modified 1 April 2001 by John Doe + [owner] + name = John Doe + organization = Acme Widgets Inc. + + [database] + ; use IP address in case network name resolution is not working + server = 192.0.2.62 + port = 143` + parser := InitParser() + parser.LoadFromString(s) + + expected := populateExpectedNormal(t) + + assertEquality(t, expected, parser.sections) + + }) + + t.Run("test empty section ini file", func(t *testing.T) { + s := `; last modified 1 April 2001 by John Doe + [owner] + name = John Doe + organization = Acme Widgets Inc. + + [database]` + parser := InitParser() + parser.LoadFromString(s) + + expected := populateExpectedEmptySection(t) + + assertEquality(t, expected, parser.sections) + }) +} + func TestLoadFromFile(t *testing.T) { - expected := populateExpected(t) + expected := populateExpectedNormal(t) parser := InitParser() parser.LoadFromFile(path) - if !reflect.DeepEqual(parser.sections, expected) { - t.Errorf("Error! wanted:%v \n , got : %v \n", expected, parser.sections) - } + assertEquality(t, expected, parser.sections) } func TestGetSectionNames(t *testing.T) { @@ -65,9 +105,5 @@ func TestGetSectionNames(t *testing.T) { expected := []string{"owner", "database"} - - if !reflect.DeepEqual(names, expected) { - t.Errorf("Error! wanted:%q \n , got:%q \n", expected, names) - } - + assertEquality(t, names, expected) } From 0a7a12e0b0a18301235c8ca82c1409bee3a60e6c Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 9 Sep 2024 16:43:22 +0300 Subject: [PATCH 06/31] feat: Get() and its table test --- pkg/iniparser.go | 19 ++++++++++++++- pkg/iniparser_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index f153986..0030158 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -1,8 +1,10 @@ package iniparser import ( + "fmt" "log" "os" + "reflect" "strings" ) @@ -24,7 +26,7 @@ func (i *iniParser) populateINI(lines []string) { var title string var sec section for _, line := range lines { - + line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, ";") { continue @@ -72,3 +74,18 @@ func (i *iniParser) GetSectionNames() []string { } return names } + +func (i *iniParser) GetSections() map[string]section { + return i.sections +} + +func (i *iniParser) Get(sectionName string, key string) (string, error) { + if reflect.DeepEqual(i.sections[sectionName], section{}) { + return "", fmt.Errorf("section not found") + } + if i.sections[sectionName].map_[key] == "" { + return "", fmt.Errorf("key not found") + } + return i.sections[sectionName].map_[key], nil + +} diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 3eb8047..c0a1c50 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -1,6 +1,7 @@ package iniparser import ( + "fmt" "reflect" "testing" ) @@ -107,3 +108,57 @@ func TestGetSectionNames(t *testing.T) { assertEquality(t, names, expected) } + +func TestGetSections(t *testing.T) { + parser := InitParser() + parser.LoadFromFile(path) + got := parser.GetSections() + + expected := populateExpectedNormal(t) + assertEquality(t, got, expected) +} + +func TestGet(t *testing.T) { + + testcases := []struct { + testcaseName string + sectionName string + key string + expected string + err error + }{ + { + testcaseName: "Normal case: section and key are present", + sectionName: "database", + key: "server", + expected: "192.0.2.62", + err: nil, + }, + { + testcaseName: "corner case: section not found", + sectionName: "user", + key: "server", + err: fmt.Errorf("section not found"), + }, + { + testcaseName: "corner case: key not found", + sectionName: "database", + key: "size", + err: fmt.Errorf("key not found"), + }, + } + for _, testcase := range testcases { + + t.Run(testcase.testcaseName, func(t *testing.T) { + parser := InitParser() + parser.LoadFromFile(path) + got, err := parser.Get(testcase.sectionName, testcase.key) + if testcase.err == nil { + assertEquality(t, got, testcase.expected) + } else { + assertEquality(t, err, testcase.err) + } + }) + } + +} From d88ae9bbe675a06e82d4cd514a70e641b922f1a5 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 10 Sep 2024 11:12:37 +0300 Subject: [PATCH 07/31] feat: Adding github actions --- .github/workflows/main.yaml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/main.yaml diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..2908fe3 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,35 @@ +name: Go CI + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.23.1' + - name: Build + run: go build -v ./... + + + golangci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.60 + - name: Go Format + uses: Jerome1337/gofmt-action@v1.0.5 + with: + gofmt-path: './src' + gofmt-flags: '-l -d' + - name: Test with the Go CLI + run: go test ./pkg \ No newline at end of file From e53f44a0a4e3a777f1b7d6c0e6b7bbcbbdd5db84 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 10 Sep 2024 11:27:48 +0300 Subject: [PATCH 08/31] feat: Adding Set() and its test --- pkg/iniparser.go | 11 +++++++++++ pkg/iniparser_test.go | 46 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 0030158..3db92c4 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -89,3 +89,14 @@ func (i *iniParser) Get(sectionName string, key string) (string, error) { return i.sections[sectionName].map_[key], nil } + +func (i *iniParser) Set(sectionName string, key string, value string) error { + if reflect.DeepEqual(i.sections[sectionName], section{}) { + return fmt.Errorf("section not found") + } + if i.sections[sectionName].map_[key] == "" { + return fmt.Errorf("key not found") + } + i.sections[sectionName].map_[key] = value + return nil +} diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index c0a1c50..9fbd6bc 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -162,3 +162,49 @@ func TestGet(t *testing.T) { } } + +func TestSet(t *testing.T) { + + testcases := []struct { + testcaseName string + sectionName string + key string + value string + err error + }{ + { + testcaseName: "Normal case: section and key are present", + sectionName: "database", + key: "server", + value: "127.0.0.1", + err: nil, + }, + { + testcaseName: "corner case: section not found", + sectionName: "user", + key: "server", + err: fmt.Errorf("section not found"), + }, + { + testcaseName: "corner case: key not found", + sectionName: "database", + key: "size", + err: fmt.Errorf("key not found"), + }, + } + for _, testcase := range testcases { + + t.Run(testcase.testcaseName, func(t *testing.T) { + parser := InitParser() + parser.LoadFromFile(path) + err := parser.Set(testcase.sectionName, testcase.key, testcase.value) + if testcase.err == nil { + value := parser.sections[testcase.sectionName].map_[testcase.key] + assertEquality(t, value, testcase.value) + } else { + assertEquality(t, err, testcase.err) + } + }) + } + +} From 8d412274dd3c4ca33065c71f5ae31d1109ddf069 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 10 Sep 2024 12:40:41 +0300 Subject: [PATCH 09/31] feat: Adding ToString() and its test --- pkg/iniparser.go | 11 ++++++++++ pkg/iniparser_test.go | 50 +++++++++++++++++++++++++++---------------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 3db92c4..c0c52e1 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -100,3 +100,14 @@ func (i *iniParser) Set(sectionName string, key string, value string) error { i.sections[sectionName].map_[key] = value return nil } + +func (i *iniParser) ToString() string { + var result string + for sectionName, section := range i.sections { + result += "[" + sectionName + "]\n" + for key, value := range section.map_ { + result += key + "=" + value + "\n" + } + } + return result +} diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 9fbd6bc..734a402 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -8,6 +8,16 @@ import ( const path = "testdata/file.ini" +const stringINI = `; last modified 1 April 2001 by John Doe +[owner] +name = John Doe +organization = Acme Widgets Inc. + +[database] +; use IP address in case network name resolution is not working +server = 192.0.2.62 +port = 143` + func populateExpectedNormal(t *testing.T) map[string]section { t.Helper() expected := map[string]section{ @@ -55,17 +65,9 @@ func assertEquality(t *testing.T, obj1 any, obj2 any) { func TestLoadFromString(t *testing.T) { t.Run("test normal ini file", func(t *testing.T) { - s := `; last modified 1 April 2001 by John Doe - [owner] - name = John Doe - organization = Acme Widgets Inc. - - [database] - ; use IP address in case network name resolution is not working - server = 192.0.2.62 - port = 143` + parser := InitParser() - parser.LoadFromString(s) + parser.LoadFromString(stringINI) expected := populateExpectedNormal(t) @@ -74,14 +76,14 @@ func TestLoadFromString(t *testing.T) { }) t.Run("test empty section ini file", func(t *testing.T) { - s := `; last modified 1 April 2001 by John Doe + const emptySectionINI = `; last modified 1 April 2001 by John Doe [owner] name = John Doe organization = Acme Widgets Inc. [database]` parser := InitParser() - parser.LoadFromString(s) + parser.LoadFromString(emptySectionINI) expected := populateExpectedEmptySection(t) @@ -106,7 +108,7 @@ func TestGetSectionNames(t *testing.T) { expected := []string{"owner", "database"} - assertEquality(t, names, expected) + assertEquality(t, expected, names) } func TestGetSections(t *testing.T) { @@ -115,7 +117,7 @@ func TestGetSections(t *testing.T) { got := parser.GetSections() expected := populateExpectedNormal(t) - assertEquality(t, got, expected) + assertEquality(t, expected, got) } func TestGet(t *testing.T) { @@ -154,9 +156,9 @@ func TestGet(t *testing.T) { parser.LoadFromFile(path) got, err := parser.Get(testcase.sectionName, testcase.key) if testcase.err == nil { - assertEquality(t, got, testcase.expected) + assertEquality(t, testcase.expected, got) } else { - assertEquality(t, err, testcase.err) + assertEquality(t, testcase.err, err) } }) } @@ -200,11 +202,23 @@ func TestSet(t *testing.T) { err := parser.Set(testcase.sectionName, testcase.key, testcase.value) if testcase.err == nil { value := parser.sections[testcase.sectionName].map_[testcase.key] - assertEquality(t, value, testcase.value) + assertEquality(t, testcase.value, value) } else { - assertEquality(t, err, testcase.err) + assertEquality(t, testcase.err, err) } }) } } + +func TestToString(t *testing.T) { + parser1 := InitParser() + parser2 := InitParser() + + parser1.LoadFromString(stringINI) + got := parser1.ToString() + + parser2.LoadFromString(got) + + assertEquality(t, parser1.sections, parser2.sections) +} From 2fa3ac06a39c49a8d5389697c0615ee993466b63 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 10 Sep 2024 14:28:41 +0300 Subject: [PATCH 10/31] feat: Adding SaveToFile() and its test --- pkg/iniparser.go | 33 ++++++++++++++++++++++++++++++--- pkg/iniparser_test.go | 38 ++++++++++++++++++++++++++++++++++++-- pkg/testdata/file.ini | 3 +++ pkg/testdata/out.ini | 9 +++++++++ 4 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 pkg/testdata/out.ini diff --git a/pkg/iniparser.go b/pkg/iniparser.go index c0c52e1..88bfd3e 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -5,6 +5,7 @@ import ( "log" "os" "reflect" + "sort" "strings" ) @@ -103,11 +104,37 @@ func (i *iniParser) Set(sectionName string, key string, value string) error { func (i *iniParser) ToString() string { var result string - for sectionName, section := range i.sections { + sectionNames := make([]string, 0) + for sectionName := range i.sections { + sectionNames = append(sectionNames, sectionName) + } + sort.Strings(sectionNames) + + for _, sectionName := range sectionNames { + keys := make([]string, 0) result += "[" + sectionName + "]\n" - for key, value := range section.map_ { - result += key + "=" + value + "\n" + for key := range i.sections[sectionName].map_ { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + result += key + " = " + i.sections[sectionName].map_[key] + "\n" } } return result } + +func (i *iniParser) SaveToFile(path string) error { + file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("error in opening the file: %v", err) + } + defer file.Close() + + stringFile := i.ToString() + _, err = file.WriteString(stringFile) + if err != nil { + return fmt.Errorf("error in writing to the file: %v", err) + } + return nil +} diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 734a402..e47fb47 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -2,6 +2,7 @@ package iniparser import ( "fmt" + "os" "reflect" "testing" ) @@ -16,7 +17,10 @@ organization = Acme Widgets Inc. [database] ; use IP address in case network name resolution is not working server = 192.0.2.62 -port = 143` +port = 143 +[section] +key0 = val0 +key1 = val1` func populateExpectedNormal(t *testing.T) map[string]section { t.Helper() @@ -33,6 +37,12 @@ func populateExpectedNormal(t *testing.T) map[string]section { "port": "143", }, }, + "section": { + map_: map[string]string{ + "key0": "val0", + "key1": "val1", + }, + }, } return expected } @@ -63,6 +73,16 @@ func assertEquality(t *testing.T, obj1 any, obj2 any) { } } +func assertFile(t *testing.T, filePath string, expectedData string) { + t.Helper() + data, err := os.ReadFile(filePath) + if err != nil { + t.Errorf("Error! : %v\n", err) + } + assertEquality(t, expectedData, string(data)) + +} + func TestLoadFromString(t *testing.T) { t.Run("test normal ini file", func(t *testing.T) { @@ -106,7 +126,7 @@ func TestGetSectionNames(t *testing.T) { parser.LoadFromFile(path) names := parser.GetSectionNames() - expected := []string{"owner", "database"} + expected := []string{"owner", "database", "section"} assertEquality(t, expected, names) } @@ -222,3 +242,17 @@ func TestToString(t *testing.T) { assertEquality(t, parser1.sections, parser2.sections) } + +func TestSaveToFile(t *testing.T) { + const outPath = "testdata/out.ini" + parser := InitParser() + parser.LoadFromFile(path) + + err := parser.SaveToFile(outPath) + if err != nil { + t.Errorf("Error! %v", err) + } + + stringFile := parser.ToString() + assertFile(t, outPath, stringFile) +} diff --git a/pkg/testdata/file.ini b/pkg/testdata/file.ini index f49dea2..31482bf 100644 --- a/pkg/testdata/file.ini +++ b/pkg/testdata/file.ini @@ -6,3 +6,6 @@ organization = Acme Widgets Inc. [database] server = 192.0.2.62 port = 143 +[section] +key0 = val0 +key1 = val1 \ No newline at end of file diff --git a/pkg/testdata/out.ini b/pkg/testdata/out.ini new file mode 100644 index 0000000..9eb410b --- /dev/null +++ b/pkg/testdata/out.ini @@ -0,0 +1,9 @@ +[database] +port = 143 +server = 192.0.2.62 +[owner] +name = John Doe +organization = Acme Widgets Inc. +[section] +key0 = val0 +key1 = val1 From f8ecb1ae468dae94867b8fe0801ac180452707ee Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 10 Sep 2024 16:10:48 +0300 Subject: [PATCH 11/31] refactor: LoadFromFile() --- pkg/iniparser.go | 10 +++++++--- pkg/iniparser_test.go | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 88bfd3e..7277fca 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -2,7 +2,6 @@ package iniparser import ( "fmt" - "log" "os" "reflect" "sort" @@ -59,13 +58,18 @@ func (i *iniParser) LoadFromString(file string) { i.populateINI(lines) } -func (i *iniParser) LoadFromFile(path string) { +func (i *iniParser) LoadFromFile(path string) error { + + if !strings.HasSuffix(path, ".ini") { + return fmt.Errorf("this is not an ini file") + } data, err := os.ReadFile(path) if err != nil { - log.Fatal("Error in reading the file") + return fmt.Errorf("error in reading the file: %v", err) } i.LoadFromString(string(data)) + return nil } func (i *iniParser) GetSectionNames() []string { diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index e47fb47..63e3815 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -116,14 +116,21 @@ func TestLoadFromFile(t *testing.T) { expected := populateExpectedNormal(t) parser := InitParser() - parser.LoadFromFile(path) + + err := parser.LoadFromFile(path) + if err != nil { + t.Errorf("Error! %v", err) + } assertEquality(t, expected, parser.sections) } func TestGetSectionNames(t *testing.T) { parser := InitParser() - parser.LoadFromFile(path) + err := parser.LoadFromFile(path) + if err != nil { + t.Errorf("Error! %v", err) + } names := parser.GetSectionNames() expected := []string{"owner", "database", "section"} @@ -133,7 +140,11 @@ func TestGetSectionNames(t *testing.T) { func TestGetSections(t *testing.T) { parser := InitParser() - parser.LoadFromFile(path) + err := parser.LoadFromFile(path) + if err != nil { + t.Errorf("Error! %v", err) + } + got := parser.GetSections() expected := populateExpectedNormal(t) @@ -173,7 +184,10 @@ func TestGet(t *testing.T) { t.Run(testcase.testcaseName, func(t *testing.T) { parser := InitParser() - parser.LoadFromFile(path) + err := parser.LoadFromFile(path) + if err != nil { + t.Errorf("Error! %v", err) + } got, err := parser.Get(testcase.sectionName, testcase.key) if testcase.err == nil { assertEquality(t, testcase.expected, got) @@ -218,8 +232,11 @@ func TestSet(t *testing.T) { t.Run(testcase.testcaseName, func(t *testing.T) { parser := InitParser() - parser.LoadFromFile(path) - err := parser.Set(testcase.sectionName, testcase.key, testcase.value) + err := parser.LoadFromFile(path) + if err != nil { + t.Errorf("Error! %v", err) + } + err = parser.Set(testcase.sectionName, testcase.key, testcase.value) if testcase.err == nil { value := parser.sections[testcase.sectionName].map_[testcase.key] assertEquality(t, testcase.value, value) @@ -246,9 +263,12 @@ func TestToString(t *testing.T) { func TestSaveToFile(t *testing.T) { const outPath = "testdata/out.ini" parser := InitParser() - parser.LoadFromFile(path) + err := parser.LoadFromFile(path) + if err != nil { + t.Errorf("Error! %v", err) + } - err := parser.SaveToFile(outPath) + err = parser.SaveToFile(outPath) if err != nil { t.Errorf("Error! %v", err) } From 0898131358b5ae047eb1a6dff61d5782e9a51d6d Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 10 Sep 2024 17:18:57 +0300 Subject: [PATCH 12/31] doc: Adding API documentation --- pkg/iniparser.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 7277fca..7d31927 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -1,3 +1,6 @@ +// Package iniparser implements a parser for ini files +// +// The iniparser package should only be used for for ini files and doesn't support any other types of files package iniparser import ( @@ -8,14 +11,26 @@ import ( "strings" ) +// The section acts as a type representing the value of the iniparser map type section struct { map_ map[string]string } +// The iniParser acts as the data structure storing all of the parsed sections +// It has the following format: +// +// "sectionName": { +// map_: map[string]string{ +// "key0": "value0", +// "key1": "value1", +// }, +// }, type iniParser struct { sections map[string]section } +// InitParser returns an iniParser type object +// InitParser is an essential call to get a parser to be able to access its APIs func InitParser() iniParser { return iniParser{ make(map[string]section), @@ -52,12 +67,22 @@ func (i *iniParser) populateINI(lines []string) { } } +// LoadFromString is a method that takes a string ini configs and parses them into the caller iniParser object +// LoadFromString assumes that: +// 1- There're no global keys, every keys need to be part of a section +// 2- The key value separator is just = +// 3- Comments are only valid at the beginning of the line func (i *iniParser) LoadFromString(file string) { lines := strings.Split(file, "\n") i.populateINI(lines) } +// LoadFromFile is a method that takes a path to an ini file and parses it into the caller iniParser object +// LoadFromFile assumes that: +// 1- There're no global keys, every keys need to be part of a section +// 2- The key value separator is just = +// 3- Comments are only valid at the beginning of the line func (i *iniParser) LoadFromFile(path string) error { if !strings.HasSuffix(path, ".ini") { @@ -72,6 +97,8 @@ func (i *iniParser) LoadFromFile(path string) error { return nil } +// GetSectionNames is a method that returns an array of strings having the names +// of the sections of the caller iniParser object func (i *iniParser) GetSectionNames() []string { names := make([]string, 0) for key := range i.sections { @@ -80,10 +107,19 @@ func (i *iniParser) GetSectionNames() []string { return names } +// GetSections is a method that returns a map[string]section representing +// the data structure of the caller iniParser object func (i *iniParser) GetSections() map[string]section { return i.sections } +// Get is a method that takes a string for the sectionName and a string for the key +// and returns the value of this key and an error +// Error formats for different cases: +// +// If the section name passed isn't found --> "section not found" +// If the key passed isn't found in the passed section --> "key not found" +// else --> nil func (i *iniParser) Get(sectionName string, key string) (string, error) { if reflect.DeepEqual(i.sections[sectionName], section{}) { return "", fmt.Errorf("section not found") @@ -95,6 +131,13 @@ func (i *iniParser) Get(sectionName string, key string) (string, error) { } +// Set is a method that takes a string for the sectionName, a string for the key and a string for the value of this key +// It sets the passed key if found with the passed value and returns an error +// Error formats for different cases: +// +// If the section name passed isn't found --> "section not found" +// If the key passed isn't found in the passed section --> "key not found" +// else --> nil func (i *iniParser) Set(sectionName string, key string, value string) error { if reflect.DeepEqual(i.sections[sectionName], section{}) { return fmt.Errorf("section not found") @@ -106,6 +149,8 @@ func (i *iniParser) Set(sectionName string, key string, value string) error { return nil } +// ToString is a method that returns the parsed ini map of the caller object as one string +// The returned string won't include the comments func (i *iniParser) ToString() string { var result string sectionNames := make([]string, 0) @@ -128,6 +173,13 @@ func (i *iniParser) ToString() string { return result } +// SaveToFile is a method that takes a path to an output file and returns an error +// It saves the parsed ini map of the caller object into a file +// Error formats for different cases: +// +// If the file couldn't be opened --> "error in opening the file:" +// If writing to the file failed --> "error in writing to the file:" +// else --> nil func (i *iniParser) SaveToFile(path string) error { file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { From 5dbd304ccbae29595f1999ede753904d53f5007c Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 10 Sep 2024 18:04:38 +0300 Subject: [PATCH 13/31] doc: end user documentation --- README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3803f1c..d6ae16c 100644 --- a/README.md +++ b/README.md @@ -1 +1,60 @@ -# RawanMostafa-inigo \ No newline at end of file +# INI Config Parser + +This repository implements an ini config parser + +## In this README 👇 + +- [Features](#features) +- [Usage](#usage) + +## Features + - `InitParser()` returns a parser object to call our APIs + - `LoadFromString()` takes a string ini configs and parses them into the caller iniParser object + - `LoadFromFile()` takes an ini file path and parses it into the caller iniParser object + - `GetSectionNames()` returns an array of strings having the names of the sections of the caller iniParser object + - `GetSections()` returns a representing the parsed data structure of the caller iniParser object + - `Get()` takes a sectionName and a key and returns the value of this key and an error if found + - `Set()` takes a sectionName, a key and a value, it sets the passed key with the passed value and returns an error if found + - `ToString()` returns the parsed ini map of the caller object as one string + - `SaveToFile()` takes a path to an output file, saves the parsed ini map of the caller object into a file and returns an error if found + +## Usage + +1. + ```go + import github.com/codescalersinternships/RawanMostafa-inigo + ``` + +2. Initialize the parser first + ```go + parser := InitParser() + ``` + +3. Example usage: + ```go + parser.LoadFromString(s) + ``` + ```go + parser.LoadFromFile(filepath) + ``` + ```go + names := parser.GetSectionNames() + ``` + ```go + sections := parser.GetSections() + ``` + ```go + value, err := parser.Get(sectionName, key) + ``` + ```go + err = parser.Set(sectionName, key, value) + ``` + ```go + s := parser.ToString() + ``` + ```go + err = parser.SaveToFile(outPath) + ``` + + + From 96c7df68201f138fbac50c80c982436abac4e0b1 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Wed, 11 Sep 2024 11:41:44 +0300 Subject: [PATCH 14/31] test: testcases for LoadFromFile added --- pkg/iniparser_test.go | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 63e3815..b9b32fa 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -113,16 +113,45 @@ func TestLoadFromString(t *testing.T) { func TestLoadFromFile(t *testing.T) { - expected := populateExpectedNormal(t) - - parser := InitParser() + expectedFile := populateExpectedNormal(t) - err := parser.LoadFromFile(path) - if err != nil { - t.Errorf("Error! %v", err) + testcases := []struct { + testcaseName string + filePath string + expected map[string]section + err error + }{ + { + testcaseName: "Normal case: ini file is present", + filePath: "testdata/file.ini", + expected: expectedFile, + err: nil, + }, + { + testcaseName: "corner case: not an ini file", + filePath: "testdata/file.txt", + err: fmt.Errorf("this is not an ini file"), + }, + { + testcaseName: "corner case: file not found", + filePath: "testdata/filex.ini", + err: fmt.Errorf("error in reading the file: open testdata/filex.ini: no such file or directory"), + }, } + for _, testcase := range testcases { + + t.Run(testcase.testcaseName, func(t *testing.T) { + parser := InitParser() - assertEquality(t, expected, parser.sections) + err := parser.LoadFromFile(testcase.filePath) + if testcase.err == nil { + assertEquality(t, expectedFile, parser.sections) + } else { + assertEquality(t, testcase.err, err) + } + + }) + } } func TestGetSectionNames(t *testing.T) { From bc7c5d55da485d5fde1d2159bb3f4a8c29b0bac3 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Wed, 11 Sep 2024 15:08:58 +0300 Subject: [PATCH 15/31] fix: make the caller object instead of ptr when needed --- pkg/iniparser.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 7d31927..7331b4e 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -99,7 +99,7 @@ func (i *iniParser) LoadFromFile(path string) error { // GetSectionNames is a method that returns an array of strings having the names // of the sections of the caller iniParser object -func (i *iniParser) GetSectionNames() []string { +func (i iniParser) GetSectionNames() []string { names := make([]string, 0) for key := range i.sections { names = append(names, key) @@ -109,7 +109,7 @@ func (i *iniParser) GetSectionNames() []string { // GetSections is a method that returns a map[string]section representing // the data structure of the caller iniParser object -func (i *iniParser) GetSections() map[string]section { +func (i iniParser) GetSections() map[string]section { return i.sections } @@ -120,7 +120,7 @@ func (i *iniParser) GetSections() map[string]section { // If the section name passed isn't found --> "section not found" // If the key passed isn't found in the passed section --> "key not found" // else --> nil -func (i *iniParser) Get(sectionName string, key string) (string, error) { +func (i iniParser) Get(sectionName string, key string) (string, error) { if reflect.DeepEqual(i.sections[sectionName], section{}) { return "", fmt.Errorf("section not found") } @@ -151,7 +151,7 @@ func (i *iniParser) Set(sectionName string, key string, value string) error { // ToString is a method that returns the parsed ini map of the caller object as one string // The returned string won't include the comments -func (i *iniParser) ToString() string { +func (i iniParser) ToString() string { var result string sectionNames := make([]string, 0) for sectionName := range i.sections { @@ -180,7 +180,7 @@ func (i *iniParser) ToString() string { // If the file couldn't be opened --> "error in opening the file:" // If writing to the file failed --> "error in writing to the file:" // else --> nil -func (i *iniParser) SaveToFile(path string) error { +func (i iniParser) SaveToFile(path string) error { file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("error in opening the file: %v", err) From b1f5ab46a5043cf97acb743f63b54c3f7b398541 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Wed, 11 Sep 2024 15:19:04 +0300 Subject: [PATCH 16/31] refactor : error handling technique --- pkg/iniparser.go | 12 +++++++----- pkg/iniparser_test.go | 37 ++++++++++++++++++------------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 7331b4e..1896fb8 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -4,6 +4,7 @@ package iniparser import ( + "errors" "fmt" "os" "reflect" @@ -86,7 +87,7 @@ func (i *iniParser) LoadFromString(file string) { func (i *iniParser) LoadFromFile(path string) error { if !strings.HasSuffix(path, ".ini") { - return fmt.Errorf("this is not an ini file") + return errors.New("this is not an ini file") } data, err := os.ReadFile(path) @@ -121,11 +122,12 @@ func (i iniParser) GetSections() map[string]section { // If the key passed isn't found in the passed section --> "key not found" // else --> nil func (i iniParser) Get(sectionName string, key string) (string, error) { + if reflect.DeepEqual(i.sections[sectionName], section{}) { - return "", fmt.Errorf("section not found") + return "", errors.New("section not found") } if i.sections[sectionName].map_[key] == "" { - return "", fmt.Errorf("key not found") + return "", errors.New("key not found") } return i.sections[sectionName].map_[key], nil @@ -140,10 +142,10 @@ func (i iniParser) Get(sectionName string, key string) (string, error) { // else --> nil func (i *iniParser) Set(sectionName string, key string, value string) error { if reflect.DeepEqual(i.sections[sectionName], section{}) { - return fmt.Errorf("section not found") + return errors.New("section not found") } if i.sections[sectionName].map_[key] == "" { - return fmt.Errorf("key not found") + return errors.New("key not found") } i.sections[sectionName].map_[key] = value return nil diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index b9b32fa..354c548 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -1,7 +1,6 @@ package iniparser import ( - "fmt" "os" "reflect" "testing" @@ -119,23 +118,23 @@ func TestLoadFromFile(t *testing.T) { testcaseName string filePath string expected map[string]section - err error + err string }{ { testcaseName: "Normal case: ini file is present", filePath: "testdata/file.ini", expected: expectedFile, - err: nil, + err: "", }, { testcaseName: "corner case: not an ini file", filePath: "testdata/file.txt", - err: fmt.Errorf("this is not an ini file"), + err: "this is not an ini file", }, { testcaseName: "corner case: file not found", filePath: "testdata/filex.ini", - err: fmt.Errorf("error in reading the file: open testdata/filex.ini: no such file or directory"), + err: "error in reading the file: open testdata/filex.ini: no such file or directory", }, } for _, testcase := range testcases { @@ -144,10 +143,10 @@ func TestLoadFromFile(t *testing.T) { parser := InitParser() err := parser.LoadFromFile(testcase.filePath) - if testcase.err == nil { + if testcase.err == "" { assertEquality(t, expectedFile, parser.sections) } else { - assertEquality(t, testcase.err, err) + assertEquality(t, testcase.err, err.Error()) } }) @@ -187,26 +186,26 @@ func TestGet(t *testing.T) { sectionName string key string expected string - err error + err string }{ { testcaseName: "Normal case: section and key are present", sectionName: "database", key: "server", expected: "192.0.2.62", - err: nil, + err: "", }, { testcaseName: "corner case: section not found", sectionName: "user", key: "server", - err: fmt.Errorf("section not found"), + err: "section not found", }, { testcaseName: "corner case: key not found", sectionName: "database", key: "size", - err: fmt.Errorf("key not found"), + err: "key not found", }, } for _, testcase := range testcases { @@ -218,10 +217,10 @@ func TestGet(t *testing.T) { t.Errorf("Error! %v", err) } got, err := parser.Get(testcase.sectionName, testcase.key) - if testcase.err == nil { + if testcase.err == "" { assertEquality(t, testcase.expected, got) } else { - assertEquality(t, testcase.err, err) + assertEquality(t, testcase.err, err.Error()) } }) } @@ -235,26 +234,26 @@ func TestSet(t *testing.T) { sectionName string key string value string - err error + err string }{ { testcaseName: "Normal case: section and key are present", sectionName: "database", key: "server", value: "127.0.0.1", - err: nil, + err: "", }, { testcaseName: "corner case: section not found", sectionName: "user", key: "server", - err: fmt.Errorf("section not found"), + err: "section not found", }, { testcaseName: "corner case: key not found", sectionName: "database", key: "size", - err: fmt.Errorf("key not found"), + err: "key not found", }, } for _, testcase := range testcases { @@ -266,11 +265,11 @@ func TestSet(t *testing.T) { t.Errorf("Error! %v", err) } err = parser.Set(testcase.sectionName, testcase.key, testcase.value) - if testcase.err == nil { + if testcase.err == "" { value := parser.sections[testcase.sectionName].map_[testcase.key] assertEquality(t, testcase.value, value) } else { - assertEquality(t, testcase.err, err) + assertEquality(t, testcase.err, err.Error()) } }) } From 760d69bcd9a1dd82b5eb05a0123d7aa3f50cf79b Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Wed, 11 Sep 2024 15:52:49 +0300 Subject: [PATCH 17/31] refactor : error variables added --- pkg/iniparser.go | 43 ++++++++++++++++++++------------ pkg/iniparser_test.go | 57 +++++++++++++++++++++++++++++++------------ 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 1896fb8..290e86b 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -7,11 +7,15 @@ import ( "errors" "fmt" "os" - "reflect" "sort" "strings" ) +var errEmptyParser = errors.New("this parser has no sections") +var errNoKey = errors.New("key not found") +var errNoSection = errors.New("section not found") +var errNotINI = errors.New("this is not an ini file") + // The section acts as a type representing the value of the iniparser map type section struct { map_ map[string]string @@ -87,7 +91,7 @@ func (i *iniParser) LoadFromString(file string) { func (i *iniParser) LoadFromFile(path string) error { if !strings.HasSuffix(path, ".ini") { - return errors.New("this is not an ini file") + return errNotINI } data, err := os.ReadFile(path) @@ -99,19 +103,26 @@ func (i *iniParser) LoadFromFile(path string) error { } // GetSectionNames is a method that returns an array of strings having the names -// of the sections of the caller iniParser object -func (i iniParser) GetSectionNames() []string { +// of the sections of the caller iniParser object and an error in case of empty sections +func (i iniParser) GetSectionNames() ([]string, error) { + if len(i.sections) == 0 { + return make([]string, 0), errEmptyParser + } names := make([]string, 0) for key := range i.sections { names = append(names, key) } - return names + sort.Strings(names) + return names, nil } // GetSections is a method that returns a map[string]section representing -// the data structure of the caller iniParser object -func (i iniParser) GetSections() map[string]section { - return i.sections +// the data structure of the caller iniParser object and an error in case of empty sections +func (i iniParser) GetSections() (map[string]section, error) { + if len(i.sections) == 0 { + return make(map[string]section, 0), errEmptyParser + } + return i.sections, nil } // Get is a method that takes a string for the sectionName and a string for the key @@ -123,11 +134,11 @@ func (i iniParser) GetSections() map[string]section { // else --> nil func (i iniParser) Get(sectionName string, key string) (string, error) { - if reflect.DeepEqual(i.sections[sectionName], section{}) { - return "", errors.New("section not found") + if _, ok := i.sections[sectionName]; !ok { + return "", errNoSection } - if i.sections[sectionName].map_[key] == "" { - return "", errors.New("key not found") + if _, ok := i.sections[sectionName].map_[key]; !ok { + return "", errNoKey } return i.sections[sectionName].map_[key], nil @@ -141,11 +152,11 @@ func (i iniParser) Get(sectionName string, key string) (string, error) { // If the key passed isn't found in the passed section --> "key not found" // else --> nil func (i *iniParser) Set(sectionName string, key string, value string) error { - if reflect.DeepEqual(i.sections[sectionName], section{}) { - return errors.New("section not found") + if _, ok := i.sections[sectionName]; !ok { + return errNoSection } - if i.sections[sectionName].map_[key] == "" { - return errors.New("key not found") + if _, ok := i.sections[sectionName].map_[key]; !ok { + return errNoKey } i.sections[sectionName].map_[key] = value return nil diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 354c548..5ad3b7c 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -154,29 +154,54 @@ func TestLoadFromFile(t *testing.T) { } func TestGetSectionNames(t *testing.T) { - parser := InitParser() - err := parser.LoadFromFile(path) - if err != nil { - t.Errorf("Error! %v", err) - } - names := parser.GetSectionNames() + t.Run("Normal case: sections are not empty", func(t *testing.T) { + + parser := InitParser() + err := parser.LoadFromFile(path) + if err != nil { + t.Errorf("Error! %v", err) + } + names, err := parser.GetSectionNames() - expected := []string{"owner", "database", "section"} + expected := []string{"database", "owner", "section"} - assertEquality(t, expected, names) + assertEquality(t, expected, names) + assertEquality(t, nil, err) + }) + + t.Run("Corner case: sections are empty", func(t *testing.T) { + parser := InitParser() + + _, err := parser.GetSectionNames() + + assertEquality(t, "this parser has no sections", err.Error()) + }) } func TestGetSections(t *testing.T) { - parser := InitParser() - err := parser.LoadFromFile(path) - if err != nil { - t.Errorf("Error! %v", err) - } + t.Run("Normal case: sections are not empty", func(t *testing.T) { + + parser := InitParser() + err := parser.LoadFromFile(path) + if err != nil { + t.Errorf("Error! %v", err) + } + + got, err := parser.GetSections() - got := parser.GetSections() + expected := populateExpectedNormal(t) + assertEquality(t, expected, got) + assertEquality(t, nil, err) + }) + + t.Run("Corner case: sections are empty", func(t *testing.T) { + + parser := InitParser() + + _, err := parser.GetSections() + assertEquality(t, "this parser has no sections", err.Error()) + }) - expected := populateExpectedNormal(t) - assertEquality(t, expected, got) } func TestGet(t *testing.T) { From 2820ee99cf47de6d317eb344868f34f763971f12 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Wed, 11 Sep 2024 15:58:41 +0300 Subject: [PATCH 18/31] fix : make all callers objects not ptrs --- pkg/iniparser.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 290e86b..495a22c 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -42,7 +42,7 @@ func InitParser() iniParser { } } -func (i *iniParser) populateINI(lines []string) { +func (i iniParser) populateINI(lines []string) { var title string var sec section for _, line := range lines { @@ -77,7 +77,7 @@ func (i *iniParser) populateINI(lines []string) { // 1- There're no global keys, every keys need to be part of a section // 2- The key value separator is just = // 3- Comments are only valid at the beginning of the line -func (i *iniParser) LoadFromString(file string) { +func (i iniParser) LoadFromString(file string) { lines := strings.Split(file, "\n") i.populateINI(lines) @@ -88,7 +88,7 @@ func (i *iniParser) LoadFromString(file string) { // 1- There're no global keys, every keys need to be part of a section // 2- The key value separator is just = // 3- Comments are only valid at the beginning of the line -func (i *iniParser) LoadFromFile(path string) error { +func (i iniParser) LoadFromFile(path string) error { if !strings.HasSuffix(path, ".ini") { return errNotINI @@ -151,7 +151,7 @@ func (i iniParser) Get(sectionName string, key string) (string, error) { // If the section name passed isn't found --> "section not found" // If the key passed isn't found in the passed section --> "key not found" // else --> nil -func (i *iniParser) Set(sectionName string, key string, value string) error { +func (i iniParser) Set(sectionName string, key string, value string) error { if _, ok := i.sections[sectionName]; !ok { return errNoSection } From 81265f694dc1c5fa4085e6ff172e36880c90b1fd Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 17 Sep 2024 00:20:48 +0300 Subject: [PATCH 19/31] feat: Implementing Stringer interface to ini parser --- pkg/iniparser.go | 8 +++++--- pkg/iniparser_test.go | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 495a22c..37f8aec 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -162,9 +162,11 @@ func (i iniParser) Set(sectionName string, key string, value string) error { return nil } + // ToString is a method that returns the parsed ini map of the caller object as one string // The returned string won't include the comments -func (i iniParser) ToString() string { +// Also,it tells fmt pkg how to print the object +func (i iniParser) String() string { var result string sectionNames := make([]string, 0) for sectionName := range i.sections { @@ -183,7 +185,7 @@ func (i iniParser) ToString() string { result += key + " = " + i.sections[sectionName].map_[key] + "\n" } } - return result + return fmt.Sprint(result) } // SaveToFile is a method that takes a path to an output file and returns an error @@ -200,7 +202,7 @@ func (i iniParser) SaveToFile(path string) error { } defer file.Close() - stringFile := i.ToString() + stringFile := i.String() _, err = file.WriteString(stringFile) if err != nil { return fmt.Errorf("error in writing to the file: %v", err) diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 5ad3b7c..9316c65 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -1,6 +1,7 @@ package iniparser import ( + "fmt" "os" "reflect" "testing" @@ -301,16 +302,17 @@ func TestSet(t *testing.T) { } -func TestToString(t *testing.T) { +func TestString(t *testing.T) { parser1 := InitParser() parser2 := InitParser() parser1.LoadFromString(stringINI) - got := parser1.ToString() + got := parser1.String() parser2.LoadFromString(got) assertEquality(t, parser1.sections, parser2.sections) + assertEquality(t, fmt.Sprint(parser1), fmt.Sprint(parser2)) } func TestSaveToFile(t *testing.T) { @@ -326,6 +328,6 @@ func TestSaveToFile(t *testing.T) { t.Errorf("Error! %v", err) } - stringFile := parser.ToString() + stringFile := parser.String() assertFile(t, outPath, stringFile) } From fdbcf5a57590aae27869d448a511f0a7cd79c595 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Sun, 22 Sep 2024 12:46:04 +0300 Subject: [PATCH 20/31] fix: Applying review comments for workflow file --- .github/workflows/main.yaml | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 2908fe3..b7f46f0 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -3,18 +3,6 @@ name: Go CI on: [push, pull_request] jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.23.1' - - name: Build - run: go build -v ./... - - golangci: runs-on: ubuntu-latest steps: @@ -31,5 +19,12 @@ jobs: with: gofmt-path: './src' gofmt-flags: '-l -d' + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable - name: Test with the Go CLI - run: go test ./pkg \ No newline at end of file + run: go test -v ./pkg \ No newline at end of file From a3c15af24649a06cccdf10cb662d142b6cef988a Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Sun, 22 Sep 2024 13:49:20 +0300 Subject: [PATCH 21/31] fix: first batch of review comments --- .github/workflows/main.yaml | 2 +- pkg/iniparser.go | 102 ++++++++++++++---------------------- pkg/iniparser_test.go | 53 +++++++------------ 3 files changed, 59 insertions(+), 98 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index b7f46f0..e1dcbde 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,6 +1,6 @@ name: Go CI -on: [push, pull_request] +on: [push] jobs: golangci: diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 37f8aec..5f2c814 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -11,57 +11,47 @@ import ( "strings" ) -var errEmptyParser = errors.New("this parser has no sections") -var errNoKey = errors.New("key not found") -var errNoSection = errors.New("section not found") -var errNotINI = errors.New("this is not an ini file") +var ErrNoKey = errors.New("key not found") +var ErrNoSection = errors.New("section not found") +var ErrNotINI = errors.New("this is not an ini file") // The section acts as a type representing the value of the iniparser map type section struct { map_ map[string]string } -// The iniParser acts as the data structure storing all of the parsed sections -// It has the following format: -// -// "sectionName": { -// map_: map[string]string{ -// "key0": "value0", -// "key1": "value1", -// }, -// }, -type iniParser struct { +// The Parser acts as the data structure storing all of the parsed sections +type Parser struct { sections map[string]section } -// InitParser returns an iniParser type object -// InitParser is an essential call to get a parser to be able to access its APIs -func InitParser() iniParser { - return iniParser{ +// NewParser returns a Parser type object +// NewParser is an essential call to get a parser to be able to access its APIs +func NewParser() Parser { + return Parser{ make(map[string]section), } } -func (i iniParser) populateINI(lines []string) { +func (i Parser) populateINI(lines []string) { var title string var sec section for _, line := range lines { line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, ";") { + if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") { continue } - if strings.Contains(line, "[") && strings.Contains(line, "]") { + if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { if title != "" { i.sections[title] = sec } title = strings.Trim(line, "[]") - title = strings.TrimSpace(title) sec = section{map_: make(map[string]string)} } else if strings.Contains(line, "=") { - parts := strings.Split(line, "=") + parts := strings.SplitN(line, "=", 2) key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) sec.map_[key] = value @@ -72,41 +62,41 @@ func (i iniParser) populateINI(lines []string) { } } -// LoadFromString is a method that takes a string ini configs and parses them into the caller iniParser object +// LoadFromString is a method that takes a string ini configs and parses them into the caller Parser object // LoadFromString assumes that: // 1- There're no global keys, every keys need to be part of a section // 2- The key value separator is just = // 3- Comments are only valid at the beginning of the line -func (i iniParser) LoadFromString(file string) { +func (i Parser) LoadFromString(data string) { - lines := strings.Split(file, "\n") + lines := strings.Split(data, "\n") i.populateINI(lines) } -// LoadFromFile is a method that takes a path to an ini file and parses it into the caller iniParser object +// LoadFromFile is a method that takes a path to an ini file and parses it into the caller Parser object // LoadFromFile assumes that: // 1- There're no global keys, every keys need to be part of a section // 2- The key value separator is just = // 3- Comments are only valid at the beginning of the line -func (i iniParser) LoadFromFile(path string) error { +func (i Parser) LoadFromFile(path string) error { if !strings.HasSuffix(path, ".ini") { - return errNotINI + return ErrNotINI } data, err := os.ReadFile(path) if err != nil { - return fmt.Errorf("error in reading the file: %v", err) + return fmt.Errorf("error in reading the file: %w", err) } i.LoadFromString(string(data)) return nil } // GetSectionNames is a method that returns an array of strings having the names -// of the sections of the caller iniParser object and an error in case of empty sections -func (i iniParser) GetSectionNames() ([]string, error) { +// of the sections of the caller Parser object and an error in case of empty sections +func (i Parser) GetSectionNames() ([]string, error) { if len(i.sections) == 0 { - return make([]string, 0), errEmptyParser + return make([]string, 0), nil } names := make([]string, 0) for key := range i.sections { @@ -117,31 +107,20 @@ func (i iniParser) GetSectionNames() ([]string, error) { } // GetSections is a method that returns a map[string]section representing -// the data structure of the caller iniParser object and an error in case of empty sections -func (i iniParser) GetSections() (map[string]section, error) { +// the data structure of the caller Parser object and an error in case of empty sections +func (i Parser) GetSections() (map[string]section, error) { if len(i.sections) == 0 { - return make(map[string]section, 0), errEmptyParser + return make(map[string]section, 0), nil } return i.sections, nil } // Get is a method that takes a string for the sectionName and a string for the key -// and returns the value of this key and an error -// Error formats for different cases: -// -// If the section name passed isn't found --> "section not found" -// If the key passed isn't found in the passed section --> "key not found" -// else --> nil -func (i iniParser) Get(sectionName string, key string) (string, error) { - - if _, ok := i.sections[sectionName]; !ok { - return "", errNoSection - } - if _, ok := i.sections[sectionName].map_[key]; !ok { - return "", errNoKey - } - return i.sections[sectionName].map_[key], nil +// and returns the value of this key and a boolean to indicate if found or not +func (i Parser) Get(sectionName string, key string) (string, bool) { + val, exists := i.sections[sectionName].map_[key] + return val, exists } // Set is a method that takes a string for the sectionName, a string for the key and a string for the value of this key @@ -151,22 +130,21 @@ func (i iniParser) Get(sectionName string, key string) (string, error) { // If the section name passed isn't found --> "section not found" // If the key passed isn't found in the passed section --> "key not found" // else --> nil -func (i iniParser) Set(sectionName string, key string, value string) error { +func (i Parser) Set(sectionName string, key string, value string) error { if _, ok := i.sections[sectionName]; !ok { - return errNoSection + return ErrNoSection } if _, ok := i.sections[sectionName].map_[key]; !ok { - return errNoKey + return ErrNoKey } i.sections[sectionName].map_[key] = value return nil } - -// ToString is a method that returns the parsed ini map of the caller object as one string +// String is a method that returns the parsed ini map of the caller object as one string // The returned string won't include the comments // Also,it tells fmt pkg how to print the object -func (i iniParser) String() string { +func (i Parser) String() string { var result string sectionNames := make([]string, 0) for sectionName := range i.sections { @@ -176,13 +154,13 @@ func (i iniParser) String() string { for _, sectionName := range sectionNames { keys := make([]string, 0) - result += "[" + sectionName + "]\n" + result += fmt.Sprintf("[%s]\n", sectionName) for key := range i.sections[sectionName].map_ { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { - result += key + " = " + i.sections[sectionName].map_[key] + "\n" + result += fmt.Sprintf("%s = %s\n", key, i.sections[sectionName].map_[key]) } } return fmt.Sprint(result) @@ -195,17 +173,17 @@ func (i iniParser) String() string { // If the file couldn't be opened --> "error in opening the file:" // If writing to the file failed --> "error in writing to the file:" // else --> nil -func (i iniParser) SaveToFile(path string) error { +func (i Parser) SaveToFile(path string) error { file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - return fmt.Errorf("error in opening the file: %v", err) + return fmt.Errorf("error in opening the file: %w", err) } defer file.Close() stringFile := i.String() _, err = file.WriteString(stringFile) if err != nil { - return fmt.Errorf("error in writing to the file: %v", err) + return fmt.Errorf("error in writing to the file: %w", err) } return nil } diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 9316c65..46ffa86 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -86,7 +86,7 @@ func assertFile(t *testing.T, filePath string, expectedData string) { func TestLoadFromString(t *testing.T) { t.Run("test normal ini file", func(t *testing.T) { - parser := InitParser() + parser := NewParser() parser.LoadFromString(stringINI) expected := populateExpectedNormal(t) @@ -102,7 +102,7 @@ func TestLoadFromString(t *testing.T) { organization = Acme Widgets Inc. [database]` - parser := InitParser() + parser := NewParser() parser.LoadFromString(emptySectionINI) expected := populateExpectedEmptySection(t) @@ -141,7 +141,7 @@ func TestLoadFromFile(t *testing.T) { for _, testcase := range testcases { t.Run(testcase.testcaseName, func(t *testing.T) { - parser := InitParser() + parser := NewParser() err := parser.LoadFromFile(testcase.filePath) if testcase.err == "" { @@ -157,7 +157,7 @@ func TestLoadFromFile(t *testing.T) { func TestGetSectionNames(t *testing.T) { t.Run("Normal case: sections are not empty", func(t *testing.T) { - parser := InitParser() + parser := NewParser() err := parser.LoadFromFile(path) if err != nil { t.Errorf("Error! %v", err) @@ -170,19 +170,12 @@ func TestGetSectionNames(t *testing.T) { assertEquality(t, nil, err) }) - t.Run("Corner case: sections are empty", func(t *testing.T) { - parser := InitParser() - - _, err := parser.GetSectionNames() - - assertEquality(t, "this parser has no sections", err.Error()) - }) } func TestGetSections(t *testing.T) { t.Run("Normal case: sections are not empty", func(t *testing.T) { - parser := InitParser() + parser := NewParser() err := parser.LoadFromFile(path) if err != nil { t.Errorf("Error! %v", err) @@ -195,14 +188,6 @@ func TestGetSections(t *testing.T) { assertEquality(t, nil, err) }) - t.Run("Corner case: sections are empty", func(t *testing.T) { - - parser := InitParser() - - _, err := parser.GetSections() - assertEquality(t, "this parser has no sections", err.Error()) - }) - } func TestGet(t *testing.T) { @@ -212,42 +197,40 @@ func TestGet(t *testing.T) { sectionName string key string expected string - err string + found bool }{ { testcaseName: "Normal case: section and key are present", sectionName: "database", key: "server", expected: "192.0.2.62", - err: "", + found: true, }, { testcaseName: "corner case: section not found", sectionName: "user", key: "server", - err: "section not found", + found: false, }, { testcaseName: "corner case: key not found", sectionName: "database", key: "size", - err: "key not found", + found: false, }, } for _, testcase := range testcases { t.Run(testcase.testcaseName, func(t *testing.T) { - parser := InitParser() + parser := NewParser() err := parser.LoadFromFile(path) if err != nil { t.Errorf("Error! %v", err) } - got, err := parser.Get(testcase.sectionName, testcase.key) - if testcase.err == "" { - assertEquality(t, testcase.expected, got) - } else { - assertEquality(t, testcase.err, err.Error()) - } + got, found := parser.Get(testcase.sectionName, testcase.key) + assertEquality(t, testcase.expected, got) + assertEquality(t, testcase.found, found) + }) } @@ -285,7 +268,7 @@ func TestSet(t *testing.T) { for _, testcase := range testcases { t.Run(testcase.testcaseName, func(t *testing.T) { - parser := InitParser() + parser := NewParser() err := parser.LoadFromFile(path) if err != nil { t.Errorf("Error! %v", err) @@ -303,8 +286,8 @@ func TestSet(t *testing.T) { } func TestString(t *testing.T) { - parser1 := InitParser() - parser2 := InitParser() + parser1 := NewParser() + parser2 := NewParser() parser1.LoadFromString(stringINI) got := parser1.String() @@ -317,7 +300,7 @@ func TestString(t *testing.T) { func TestSaveToFile(t *testing.T) { const outPath = "testdata/out.ini" - parser := InitParser() + parser := NewParser() err := parser.LoadFromFile(path) if err != nil { t.Errorf("Error! %v", err) From b35523df37faff7026b75507e814a262a145bd92 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 24 Sep 2024 10:39:06 +0300 Subject: [PATCH 22/31] fix: Applying review comments: removing section struct and renaming PopulateINI method --- pkg/iniparser.go | 33 +++++++++++++---------------- pkg/iniparser_test.go | 48 +++++++++++++++++-------------------------- 2 files changed, 33 insertions(+), 48 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 5f2c814..c1d8ee4 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -15,27 +15,22 @@ var ErrNoKey = errors.New("key not found") var ErrNoSection = errors.New("section not found") var ErrNotINI = errors.New("this is not an ini file") -// The section acts as a type representing the value of the iniparser map -type section struct { - map_ map[string]string -} - // The Parser acts as the data structure storing all of the parsed sections type Parser struct { - sections map[string]section + sections map[string]map[string]string } // NewParser returns a Parser type object // NewParser is an essential call to get a parser to be able to access its APIs func NewParser() Parser { return Parser{ - make(map[string]section), + make(map[string]map[string]string), } } -func (i Parser) populateINI(lines []string) { +func (i Parser) parse(lines []string) { var title string - var sec section + var sec map[string]string for _, line := range lines { line = strings.TrimSpace(line) @@ -47,14 +42,14 @@ func (i Parser) populateINI(lines []string) { i.sections[title] = sec } title = strings.Trim(line, "[]") - sec = section{map_: make(map[string]string)} + sec = make(map[string]string) } else if strings.Contains(line, "=") { parts := strings.SplitN(line, "=", 2) key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) - sec.map_[key] = value + sec[key] = value } } if title != "" { @@ -70,7 +65,7 @@ func (i Parser) populateINI(lines []string) { func (i Parser) LoadFromString(data string) { lines := strings.Split(data, "\n") - i.populateINI(lines) + i.parse(lines) } // LoadFromFile is a method that takes a path to an ini file and parses it into the caller Parser object @@ -108,9 +103,9 @@ func (i Parser) GetSectionNames() ([]string, error) { // GetSections is a method that returns a map[string]section representing // the data structure of the caller Parser object and an error in case of empty sections -func (i Parser) GetSections() (map[string]section, error) { +func (i Parser) GetSections() (map[string]map[string]string, error) { if len(i.sections) == 0 { - return make(map[string]section, 0), nil + return make(map[string]map[string]string, 0), nil } return i.sections, nil } @@ -119,7 +114,7 @@ func (i Parser) GetSections() (map[string]section, error) { // and returns the value of this key and a boolean to indicate if found or not func (i Parser) Get(sectionName string, key string) (string, bool) { - val, exists := i.sections[sectionName].map_[key] + val, exists := i.sections[sectionName][key] return val, exists } @@ -134,10 +129,10 @@ func (i Parser) Set(sectionName string, key string, value string) error { if _, ok := i.sections[sectionName]; !ok { return ErrNoSection } - if _, ok := i.sections[sectionName].map_[key]; !ok { + if _, ok := i.sections[sectionName][key]; !ok { return ErrNoKey } - i.sections[sectionName].map_[key] = value + i.sections[sectionName][key] = value return nil } @@ -155,12 +150,12 @@ func (i Parser) String() string { for _, sectionName := range sectionNames { keys := make([]string, 0) result += fmt.Sprintf("[%s]\n", sectionName) - for key := range i.sections[sectionName].map_ { + for key := range i.sections[sectionName] { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { - result += fmt.Sprintf("%s = %s\n", key, i.sections[sectionName].map_[key]) + result += fmt.Sprintf("%s = %s\n", key, i.sections[sectionName][key]) } } return fmt.Sprint(result) diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 46ffa86..d4626bc 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -22,43 +22,33 @@ port = 143 key0 = val0 key1 = val1` -func populateExpectedNormal(t *testing.T) map[string]section { +func populateExpectedNormal(t *testing.T) map[string]map[string]string { t.Helper() - expected := map[string]section{ - "owner": { - map_: map[string]string{ - "name": "John Doe", - "organization": "Acme Widgets Inc.", - }, + expected := map[string]map[string]string{ + "owner": map[string]string{ + "name": "John Doe", + "organization": "Acme Widgets Inc.", }, - "database": { - map_: map[string]string{ - "server": "192.0.2.62", - "port": "143", - }, + "database": map[string]string{ + "server": "192.0.2.62", + "port": "143", }, - "section": { - map_: map[string]string{ - "key0": "val0", - "key1": "val1", - }, + "section": map[string]string{ + "key0": "val0", + "key1": "val1", }, } return expected } -func populateExpectedEmptySection(t *testing.T) map[string]section { +func populateExpectedEmptySection(t *testing.T) map[string]map[string]string { t.Helper() - expected := map[string]section{ - "owner": { - map_: map[string]string{ - "name": "John Doe", - "organization": "Acme Widgets Inc.", - }, - }, - "database": { - map_: map[string]string{}, + expected := map[string]map[string]string{ + "owner": map[string]string{ + "name": "John Doe", + "organization": "Acme Widgets Inc.", }, + "database": map[string]string{}, } return expected } @@ -118,7 +108,7 @@ func TestLoadFromFile(t *testing.T) { testcases := []struct { testcaseName string filePath string - expected map[string]section + expected map[string]map[string]string err string }{ { @@ -275,7 +265,7 @@ func TestSet(t *testing.T) { } err = parser.Set(testcase.sectionName, testcase.key, testcase.value) if testcase.err == "" { - value := parser.sections[testcase.sectionName].map_[testcase.key] + value := parser.sections[testcase.sectionName][testcase.key] assertEquality(t, testcase.value, value) } else { assertEquality(t, testcase.err, err.Error()) From a5977850580452a43b2523aee301a4da47673c64 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 24 Sep 2024 11:09:29 +0300 Subject: [PATCH 23/31] fix: Applying review comments: handling global key case and its tests --- pkg/iniparser.go | 15 +++++++++++---- pkg/iniparser_test.go | 23 ++++++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index c1d8ee4..9c4461c 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -14,6 +14,7 @@ import ( var ErrNoKey = errors.New("key not found") var ErrNoSection = errors.New("section not found") var ErrNotINI = errors.New("this is not an ini file") +var ErrGlobalKey = errors.New("global keys aren't supported") // The Parser acts as the data structure storing all of the parsed sections type Parser struct { @@ -28,9 +29,10 @@ func NewParser() Parser { } } -func (i Parser) parse(lines []string) { +func (i Parser) parse(lines []string) error { var title string var sec map[string]string + inSection := false for _, line := range lines { line = strings.TrimSpace(line) @@ -38,6 +40,7 @@ func (i Parser) parse(lines []string) { continue } if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { + inSection = true if title != "" { i.sections[title] = sec } @@ -45,7 +48,9 @@ func (i Parser) parse(lines []string) { sec = make(map[string]string) } else if strings.Contains(line, "=") { - + if !inSection { + return ErrGlobalKey + } parts := strings.SplitN(line, "=", 2) key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) @@ -55,6 +60,7 @@ func (i Parser) parse(lines []string) { if title != "" { i.sections[title] = sec } + return nil } // LoadFromString is a method that takes a string ini configs and parses them into the caller Parser object @@ -62,10 +68,11 @@ func (i Parser) parse(lines []string) { // 1- There're no global keys, every keys need to be part of a section // 2- The key value separator is just = // 3- Comments are only valid at the beginning of the line -func (i Parser) LoadFromString(data string) { +func (i Parser) LoadFromString(data string) (err error) { lines := strings.Split(data, "\n") - i.parse(lines) + err = i.parse(lines) + return } // LoadFromFile is a method that takes a path to an ini file and parses it into the caller Parser object diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index d4626bc..55264b8 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -25,15 +25,15 @@ key1 = val1` func populateExpectedNormal(t *testing.T) map[string]map[string]string { t.Helper() expected := map[string]map[string]string{ - "owner": map[string]string{ + "owner": { "name": "John Doe", "organization": "Acme Widgets Inc.", }, - "database": map[string]string{ + "database": { "server": "192.0.2.62", "port": "143", }, - "section": map[string]string{ + "section": { "key0": "val0", "key1": "val1", }, @@ -44,11 +44,11 @@ func populateExpectedNormal(t *testing.T) map[string]map[string]string { func populateExpectedEmptySection(t *testing.T) map[string]map[string]string { t.Helper() expected := map[string]map[string]string{ - "owner": map[string]string{ + "owner": { "name": "John Doe", "organization": "Acme Widgets Inc.", }, - "database": map[string]string{}, + "database": {}, } return expected } @@ -99,6 +99,19 @@ func TestLoadFromString(t *testing.T) { assertEquality(t, expected, parser.sections) }) + + t.Run("test global key found", func(t *testing.T) { + const IniFile = `; last modified 1 April 2001 by John Doe + name = John Doe + [owner] + organization = Acme Widgets Inc. + + [database]` + + parser := NewParser() + err := parser.LoadFromString(IniFile) + assertEquality(t, err, ErrGlobalKey) + }) } func TestLoadFromFile(t *testing.T) { From bba6d2775daa8597b7e213d0d88af5ce1effa1e1 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 24 Sep 2024 11:16:57 +0300 Subject: [PATCH 24/31] fix: Applying review comments: using strings.Builder to concatenate strings instead of += --- pkg/iniparser.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 9c4461c..d24c884 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -147,25 +147,25 @@ func (i Parser) Set(sectionName string, key string, value string) error { // The returned string won't include the comments // Also,it tells fmt pkg how to print the object func (i Parser) String() string { - var result string sectionNames := make([]string, 0) for sectionName := range i.sections { sectionNames = append(sectionNames, sectionName) } sort.Strings(sectionNames) + var b strings.Builder for _, sectionName := range sectionNames { keys := make([]string, 0) - result += fmt.Sprintf("[%s]\n", sectionName) + b.WriteString(fmt.Sprintf("[%s]\n", sectionName)) for key := range i.sections[sectionName] { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { - result += fmt.Sprintf("%s = %s\n", key, i.sections[sectionName][key]) + b.WriteString(fmt.Sprintf("%s = %s\n", key, i.sections[sectionName][key])) } } - return fmt.Sprint(result) + return b.String() } // SaveToFile is a method that takes a path to an output file and returns an error From 1362a24603ef978d210de2998e86bde42a54048c Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 24 Sep 2024 11:21:02 +0300 Subject: [PATCH 25/31] fix: Applying review comments: Adding the length of the array to make() function --- pkg/iniparser.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index d24c884..06b3602 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -147,7 +147,7 @@ func (i Parser) Set(sectionName string, key string, value string) error { // The returned string won't include the comments // Also,it tells fmt pkg how to print the object func (i Parser) String() string { - sectionNames := make([]string, 0) + sectionNames := make([]string, 0, len(i.sections)) for sectionName := range i.sections { sectionNames = append(sectionNames, sectionName) } @@ -155,7 +155,7 @@ func (i Parser) String() string { var b strings.Builder for _, sectionName := range sectionNames { - keys := make([]string, 0) + keys := make([]string, 0, len(i.sections[sectionName])) b.WriteString(fmt.Sprintf("[%s]\n", sectionName)) for key := range i.sections[sectionName] { keys = append(keys, key) From ad396fac97d1bf18bd47041cb31c2b1169d56c63 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 24 Sep 2024 11:36:05 +0300 Subject: [PATCH 26/31] fix: Applying review comments: using errors.Is to assert on errors instead of their strings --- pkg/iniparser.go | 3 ++- pkg/iniparser_test.go | 29 ++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 06b3602..5a022d2 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -15,6 +15,7 @@ var ErrNoKey = errors.New("key not found") var ErrNoSection = errors.New("section not found") var ErrNotINI = errors.New("this is not an ini file") var ErrGlobalKey = errors.New("global keys aren't supported") +var ErrFileNotExist = errors.New("file doesn't exist") // The Parser acts as the data structure storing all of the parsed sections type Parser struct { @@ -88,7 +89,7 @@ func (i Parser) LoadFromFile(path string) error { data, err := os.ReadFile(path) if err != nil { - return fmt.Errorf("error in reading the file: %w", err) + return ErrFileNotExist } i.LoadFromString(string(data)) return nil diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 55264b8..2fc48db 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -1,6 +1,7 @@ package iniparser import ( + "errors" "fmt" "os" "reflect" @@ -107,7 +108,7 @@ func TestLoadFromString(t *testing.T) { organization = Acme Widgets Inc. [database]` - + parser := NewParser() err := parser.LoadFromString(IniFile) assertEquality(t, err, ErrGlobalKey) @@ -122,23 +123,23 @@ func TestLoadFromFile(t *testing.T) { testcaseName string filePath string expected map[string]map[string]string - err string + err error }{ { testcaseName: "Normal case: ini file is present", filePath: "testdata/file.ini", expected: expectedFile, - err: "", + err: nil, }, { testcaseName: "corner case: not an ini file", filePath: "testdata/file.txt", - err: "this is not an ini file", + err: ErrNotINI, }, { testcaseName: "corner case: file not found", filePath: "testdata/filex.ini", - err: "error in reading the file: open testdata/filex.ini: no such file or directory", + err: ErrFileNotExist, }, } for _, testcase := range testcases { @@ -147,11 +148,10 @@ func TestLoadFromFile(t *testing.T) { parser := NewParser() err := parser.LoadFromFile(testcase.filePath) - if testcase.err == "" { + if testcase.err == nil { assertEquality(t, expectedFile, parser.sections) - } else { - assertEquality(t, testcase.err, err.Error()) } + assertEquality(t, errors.Is(err, testcase.err), true) }) } @@ -246,26 +246,26 @@ func TestSet(t *testing.T) { sectionName string key string value string - err string + err error }{ { testcaseName: "Normal case: section and key are present", sectionName: "database", key: "server", value: "127.0.0.1", - err: "", + err: nil, }, { testcaseName: "corner case: section not found", sectionName: "user", key: "server", - err: "section not found", + err: ErrNoSection, }, { testcaseName: "corner case: key not found", sectionName: "database", key: "size", - err: "key not found", + err: ErrNoKey, }, } for _, testcase := range testcases { @@ -277,12 +277,11 @@ func TestSet(t *testing.T) { t.Errorf("Error! %v", err) } err = parser.Set(testcase.sectionName, testcase.key, testcase.value) - if testcase.err == "" { + if testcase.err == nil { value := parser.sections[testcase.sectionName][testcase.key] assertEquality(t, testcase.value, value) - } else { - assertEquality(t, testcase.err, err.Error()) } + assertEquality(t, errors.Is(err, testcase.err), true) }) } From eb69b31705532f55a95e4ecf4d29078252183332 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 24 Sep 2024 11:59:49 +0300 Subject: [PATCH 27/31] fix: Applying review comments: adding testcases for empty sections to return empty and not error --- pkg/iniparser_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 2fc48db..362be04 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -173,6 +173,21 @@ func TestGetSectionNames(t *testing.T) { assertEquality(t, nil, err) }) + t.Run("Corner case: no sections", func(t *testing.T) { + + parser := NewParser() + err := parser.LoadFromString("") + if err != nil { + t.Errorf("Error! %v", err) + } + names, err := parser.GetSectionNames() + + expected := make([]string, 0) + + assertEquality(t, expected, names) + assertEquality(t, nil, err) + }) + } func TestGetSections(t *testing.T) { @@ -191,6 +206,21 @@ func TestGetSections(t *testing.T) { assertEquality(t, nil, err) }) + t.Run("Corner case: no sections", func(t *testing.T) { + + parser := NewParser() + err := parser.LoadFromString("") + if err != nil { + t.Errorf("Error! %v", err) + } + names, err := parser.GetSections() + + expected := make(map[string]map[string]string, 0) + + assertEquality(t, expected, names) + assertEquality(t, nil, err) + }) + } func TestGet(t *testing.T) { From 2691c5025b6165d2e185d43b809c526ae7057ce9 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 24 Sep 2024 12:15:34 +0300 Subject: [PATCH 28/31] fix: Applying review comments: using temp file for testing SaveToFile method --- pkg/iniparser_test.go | 12 +++++++++--- pkg/testdata/out.ini | 9 --------- 2 files changed, 9 insertions(+), 12 deletions(-) delete mode 100644 pkg/testdata/out.ini diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 362be04..6d1bac3 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -331,18 +331,24 @@ func TestString(t *testing.T) { } func TestSaveToFile(t *testing.T) { - const outPath = "testdata/out.ini" + parser := NewParser() err := parser.LoadFromFile(path) if err != nil { t.Errorf("Error! %v", err) } - err = parser.SaveToFile(outPath) + file, err := os.CreateTemp("", "out.ini") + if err != nil { + t.Errorf("Error! %v", err) + } + defer os.Remove(file.Name()) + + err = parser.SaveToFile(file.Name()) if err != nil { t.Errorf("Error! %v", err) } stringFile := parser.String() - assertFile(t, outPath, stringFile) + assertFile(t, file.Name(), stringFile) } diff --git a/pkg/testdata/out.ini b/pkg/testdata/out.ini deleted file mode 100644 index 9eb410b..0000000 --- a/pkg/testdata/out.ini +++ /dev/null @@ -1,9 +0,0 @@ -[database] -port = 143 -server = 192.0.2.62 -[owner] -name = John Doe -organization = Acme Widgets Inc. -[section] -key0 = val0 -key1 = val1 From 32327422a4d71c45fba1782905a1fb73ff157ae9 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 24 Sep 2024 12:20:15 +0300 Subject: [PATCH 29/31] fix: liniting issues, error returned from LoadFromStrings isn't checked --- pkg/iniparser.go | 5 ++++- pkg/iniparser_test.go | 24 ++++++++++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/pkg/iniparser.go b/pkg/iniparser.go index 5a022d2..9af34d6 100644 --- a/pkg/iniparser.go +++ b/pkg/iniparser.go @@ -91,7 +91,10 @@ func (i Parser) LoadFromFile(path string) error { if err != nil { return ErrFileNotExist } - i.LoadFromString(string(data)) + err = i.LoadFromString(string(data)) + if err != nil { + return err + } return nil } diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 6d1bac3..4a07dcc 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -78,7 +78,10 @@ func TestLoadFromString(t *testing.T) { t.Run("test normal ini file", func(t *testing.T) { parser := NewParser() - parser.LoadFromString(stringINI) + err := parser.LoadFromString(stringINI) + if err != nil { + t.Error(err) + } expected := populateExpectedNormal(t) @@ -94,7 +97,10 @@ func TestLoadFromString(t *testing.T) { [database]` parser := NewParser() - parser.LoadFromString(emptySectionINI) + err := parser.LoadFromString(emptySectionINI) + if err != nil { + t.Error(err) + } expected := populateExpectedEmptySection(t) @@ -178,7 +184,7 @@ func TestGetSectionNames(t *testing.T) { parser := NewParser() err := parser.LoadFromString("") if err != nil { - t.Errorf("Error! %v", err) + t.Error(err) } names, err := parser.GetSectionNames() @@ -211,7 +217,7 @@ func TestGetSections(t *testing.T) { parser := NewParser() err := parser.LoadFromString("") if err != nil { - t.Errorf("Error! %v", err) + t.Error(err) } names, err := parser.GetSections() @@ -321,10 +327,16 @@ func TestString(t *testing.T) { parser1 := NewParser() parser2 := NewParser() - parser1.LoadFromString(stringINI) + err := parser1.LoadFromString(stringINI) + if err != nil { + t.Error(err) + } got := parser1.String() - parser2.LoadFromString(got) + err = parser2.LoadFromString(got) + if err != nil { + t.Error(err) + } assertEquality(t, parser1.sections, parser2.sections) assertEquality(t, fmt.Sprint(parser1), fmt.Sprint(parser2)) From 9096c2a9616208fa4ed40fa0a948214c71dddfb4 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 24 Sep 2024 12:26:54 +0300 Subject: [PATCH 30/31] test: added testcase for malformed sections --- pkg/iniparser_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index 4a07dcc..a497e80 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -54,6 +54,16 @@ func populateExpectedEmptySection(t *testing.T) map[string]map[string]string { return expected } +func populateExpectedMalformed(t *testing.T) map[string]map[string]string { + t.Helper() + expected := map[string]map[string]string{ + "owner": { + "organization": "Acme Widgets Inc.", + }, + } + return expected +} + func assertEquality(t *testing.T, obj1 any, obj2 any) { t.Helper() if reflect.TypeOf(obj1) != reflect.TypeOf(obj2) { @@ -119,6 +129,25 @@ func TestLoadFromString(t *testing.T) { err := parser.LoadFromString(IniFile) assertEquality(t, err, ErrGlobalKey) }) + + t.Run("test malformed section", func(t *testing.T) { + const malformedINI = `; last modified 1 April 2001 by John Doe + [owner] + name John Doe + organization = Acme Widgets Inc. + [title + ` + + parser := NewParser() + err := parser.LoadFromString(malformedINI) + if err != nil { + t.Error(err) + } + + expected := populateExpectedMalformed(t) + + assertEquality(t, expected, parser.sections) + }) } func TestLoadFromFile(t *testing.T) { From 2b7743ae198b264985882cd0cf97e599a870aabb Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 24 Sep 2024 12:30:08 +0300 Subject: [PATCH 31/31] test: testcase added for duplicate sections --- pkg/iniparser_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pkg/iniparser_test.go b/pkg/iniparser_test.go index a497e80..f420d08 100644 --- a/pkg/iniparser_test.go +++ b/pkg/iniparser_test.go @@ -64,6 +64,17 @@ func populateExpectedMalformed(t *testing.T) map[string]map[string]string { return expected } +func populateExpectedDuplicate(t *testing.T) map[string]map[string]string { + t.Helper() + expected := map[string]map[string]string{ + "owner": { + "name": "John Doe", + "organization": "Acme Widgets Inc.", + }, + } + return expected +} + func assertEquality(t *testing.T, obj1 any, obj2 any) { t.Helper() if reflect.TypeOf(obj1) != reflect.TypeOf(obj2) { @@ -148,6 +159,27 @@ func TestLoadFromString(t *testing.T) { assertEquality(t, expected, parser.sections) }) + + t.Run("test duplicate section", func(t *testing.T) { + const duplicateSectionINI = `; last modified 1 April 2001 by John Doe + [owner] + name = John Doe + organization = Acme Widgets Inc. + + [owner] + name = John Doe + organization = Acme Widgets Inc.` + + parser := NewParser() + err := parser.LoadFromString(duplicateSectionINI) + if err != nil { + t.Error(err) + } + + expected := populateExpectedDuplicate(t) + + assertEquality(t, expected, parser.sections) + }) } func TestLoadFromFile(t *testing.T) {