Skip to content

Commit 287f030

Browse files
Merge pull request #1 from pascal-blokur/feat/reverse_headers
Feat/reverse headers
2 parents bd22e11 + f8c2bae commit 287f030

4 files changed

Lines changed: 138 additions & 7 deletions

File tree

.github/workflows/go.yaml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Go
2+
3+
on:
4+
push:
5+
branches: [ "*" ]
6+
pull_request:
7+
8+
9+
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout repo
14+
uses: actions/checkout@v5
15+
with:
16+
fetch-depth: 0 # needed because of the new-from-rev in golangci
17+
18+
- name: Set up Go
19+
uses: actions/setup-go@v6
20+
with:
21+
go-version: "1.25.x"
22+
23+
- name: golangci-lint
24+
uses: golangci/golangci-lint-action@v8
25+
with:
26+
args: --timeout 5m0s
27+
28+
- id: govulncheck
29+
uses: golang/govulncheck-action@v1
30+
31+
test:
32+
runs-on: ubuntu-latest
33+
steps:
34+
- name: Checkout repo
35+
uses: actions/checkout@v5
36+
37+
- name: Set up Go
38+
uses: actions/setup-go@v6
39+
with:
40+
go-version: "1.25.x"
41+
42+
- name: Running Unit Tests
43+
run: go test ./...

errors.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package csvheaders
2+
3+
// ReplacerError is an error that can be returned when using the Replacer.
4+
type ReplacerError string
5+
6+
// Error implements the error interface.
7+
func (e ReplacerError) Error() string {
8+
return string(e)
9+
}
10+
11+
// ErrDuplicateHeader is returned when the header map registers the same header twice.
12+
const ErrDuplicateHeader = ReplacerError("duplicate header")

replacer.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@ func NewReplacer(reader *csv.Reader, inToOut map[string]string) *Replacer {
2121
return &Replacer{Reader: reader, inToOut: inToOut}
2222
}
2323

24+
// NewReverseReplacer returns a replacer using mappings from struct definition to file contents.
25+
// If there is a duplicate value in the headers mapping, this returns an error.
26+
func NewReverseReplacer(reader *csv.Reader, outToIn map[string]string) (*Replacer, error) {
27+
r := &Replacer{Reader: reader, inToOut: make(map[string]string)}
28+
29+
for out, in := range outToIn {
30+
if h, alreadyRegistered := r.inToOut[in]; alreadyRegistered {
31+
return nil, fmt.Errorf("struct tag %s registered with CSV headers %s and %s: %w", in, out, h, ErrDuplicateHeader)
32+
}
33+
34+
r.inToOut[in] = out
35+
}
36+
37+
return r, nil
38+
}
39+
2440
// Read a record from the reader.
2541
func (r *Replacer) Read() ([]string, error) {
2642
record, err := r.Reader.Read()

replacer_test.go

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package csvheaders_test
33
import (
44
"bytes"
55
"encoding/csv"
6+
"errors"
67
"slices"
78
"testing"
89

@@ -14,6 +15,7 @@ func TestReplacer(t *testing.T) {
1415

1516
t.Run("Read", testReplacerRead)
1617
t.Run("ReadAll", testReplacerReadAll)
18+
t.Run("Reverse", testReplacerReverse)
1719
}
1820

1921
func testReplacerRead(t *testing.T) {
@@ -55,21 +57,20 @@ field 1,Charles,Monica
5557
func testReplacerReadAll(t *testing.T) {
5658
t.Parallel()
5759

58-
var rawRows = []byte(`recording artist,title,isrc
59-
THE BEATLES,LET IT BE,US1239875
60-
recording artist,my band,FR789230987
60+
var rawRows = []byte(`recording artist,title
61+
THE BEATLES,LET IT BE
62+
recording artist,my band
6163
`)
6264

6365
want := [][]string{
64-
{"ARTIST", "TITLE", "ISRC"},
65-
{"THE BEATLES", "LET IT BE", "US1239875"},
66-
{"recording artist", "my band", "FR789230987"},
66+
{"ARTIST", "TITLE"},
67+
{"THE BEATLES", "LET IT BE"},
68+
{"recording artist", "my band"},
6769
}
6870

6971
headerReplacements := map[string]string{
7072
"recording artist": "ARTIST",
7173
"title": "TITLE",
72-
"isrc": "ISRC",
7374
}
7475

7576
r := csvheaders.NewReplacer(csv.NewReader(bytes.NewBuffer(rawRows)), headerReplacements)
@@ -79,6 +80,65 @@ recording artist,my band,FR789230987
7980
t.Fatal(err)
8081
}
8182

83+
compareResults(t, want, got)
84+
}
85+
86+
func testReplacerReverse(t *testing.T) {
87+
t.Parallel()
88+
89+
var rawRows = []byte(`field 1,field 2,field 10
90+
value 1,field 2,value 10
91+
`)
92+
93+
testCases := map[string]struct {
94+
headerMapping map[string]string
95+
expectedError error
96+
want [][]string
97+
}{
98+
"duplicated header": {
99+
headerMapping: map[string]string{
100+
"field 1": "FIELD",
101+
"field 2": "FIELD",
102+
},
103+
expectedError: csvheaders.ErrDuplicateHeader,
104+
want: nil,
105+
},
106+
"nil headers": {
107+
headerMapping: nil,
108+
expectedError: nil,
109+
want: [][]string{{"field 1", "field 2", "field 10"}, {"value 1", "field 2", "value 10"}},
110+
},
111+
"no header conflict": {
112+
headerMapping: map[string]string{"Monday": "field 10"},
113+
expectedError: nil,
114+
want: [][]string{{"field 1", "field 2", "Monday"}, {"value 1", "field 2", "value 10"}},
115+
},
116+
}
117+
118+
for name, testCase := range testCases {
119+
t.Run(name, func(t *testing.T) {
120+
t.Parallel()
121+
122+
r, err := csvheaders.NewReverseReplacer(csv.NewReader(bytes.NewBuffer(rawRows)), testCase.headerMapping)
123+
if testCase.expectedError != nil {
124+
if !errors.Is(err, testCase.expectedError) {
125+
t.Fatalf("expected %s, got %s", testCase.expectedError, err)
126+
}
127+
128+
return
129+
}
130+
131+
got, err := r.ReadAll()
132+
if err != nil {
133+
t.Fatalf("unexpected error while reading: %s", err)
134+
}
135+
136+
compareResults(t, testCase.want, got)
137+
})
138+
}
139+
}
140+
141+
func compareResults(t *testing.T, want, got [][]string) {
82142
if len(got) != len(want) {
83143
t.Fatalf("got %d records, want %d", len(got), len(want))
84144
}

0 commit comments

Comments
 (0)