Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/go.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Go

on:
push:
branches: [ "*" ]
pull_request:


jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5
with:
fetch-depth: 0 # needed because of the new-from-rev in golangci

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.25.x"

- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
args: --timeout 5m0s

- id: govulncheck
uses: golang/govulncheck-action@v1

test:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.25.x"

- name: Running Unit Tests
run: go test ./...
12 changes: 12 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package csvheaders

// ReplacerError is an error that can be returned when using the Replacer.
type ReplacerError string

// Error implements the error interface.
func (e ReplacerError) Error() string {
return string(e)
}

// ErrDuplicateHeader is returned when the header map registers the same header twice.
const ErrDuplicateHeader = ReplacerError("duplicate header")
16 changes: 16 additions & 0 deletions replacer.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ func NewReplacer(reader *csv.Reader, inToOut map[string]string) *Replacer {
return &Replacer{Reader: reader, inToOut: inToOut}
}

// NewReverseReplacer returns a replacer using mappings from struct definition to file contents.
// If there is a duplicate value in the headers mapping, this returns an error.
func NewReverseReplacer(reader *csv.Reader, outToIn map[string]string) (*Replacer, error) {
r := &Replacer{Reader: reader, inToOut: make(map[string]string)}

for out, in := range outToIn {
if h, alreadyRegistered := r.inToOut[in]; alreadyRegistered {
return nil, fmt.Errorf("struct tag %s registered with CSV headers %s and %s: %w", in, out, h, ErrDuplicateHeader)
}

r.inToOut[in] = out
}

return r, nil
}

// Read a record from the reader.
func (r *Replacer) Read() ([]string, error) {
record, err := r.Reader.Read()
Expand Down
74 changes: 67 additions & 7 deletions replacer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package csvheaders_test
import (
"bytes"
"encoding/csv"
"errors"
"slices"
"testing"

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

t.Run("Read", testReplacerRead)
t.Run("ReadAll", testReplacerReadAll)
t.Run("Reverse", testReplacerReverse)
}

func testReplacerRead(t *testing.T) {
Expand Down Expand Up @@ -55,21 +57,20 @@ field 1,Charles,Monica
func testReplacerReadAll(t *testing.T) {
t.Parallel()

var rawRows = []byte(`recording artist,title,isrc
THE BEATLES,LET IT BE,US1239875
recording artist,my band,FR789230987
var rawRows = []byte(`recording artist,title
THE BEATLES,LET IT BE
recording artist,my band
`)

want := [][]string{
{"ARTIST", "TITLE", "ISRC"},
{"THE BEATLES", "LET IT BE", "US1239875"},
{"recording artist", "my band", "FR789230987"},
{"ARTIST", "TITLE"},
{"THE BEATLES", "LET IT BE"},
{"recording artist", "my band"},
}

headerReplacements := map[string]string{
"recording artist": "ARTIST",
"title": "TITLE",
"isrc": "ISRC",
}

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

compareResults(t, want, got)
}

func testReplacerReverse(t *testing.T) {
t.Parallel()

var rawRows = []byte(`field 1,field 2,field 10
value 1,field 2,value 10
`)

testCases := map[string]struct {
headerMapping map[string]string
expectedError error
want [][]string
}{
"duplicated header": {
headerMapping: map[string]string{
"field 1": "FIELD",
"field 2": "FIELD",
},
expectedError: csvheaders.ErrDuplicateHeader,
want: nil,
},
"nil headers": {
headerMapping: nil,
expectedError: nil,
want: [][]string{{"field 1", "field 2", "field 10"}, {"value 1", "field 2", "value 10"}},
},
"no header conflict": {
headerMapping: map[string]string{"Monday": "field 10"},
expectedError: nil,
want: [][]string{{"field 1", "field 2", "Monday"}, {"value 1", "field 2", "value 10"}},
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()

r, err := csvheaders.NewReverseReplacer(csv.NewReader(bytes.NewBuffer(rawRows)), testCase.headerMapping)
if testCase.expectedError != nil {
if !errors.Is(err, testCase.expectedError) {
t.Fatalf("expected %s, got %s", testCase.expectedError, err)
}

return
}

got, err := r.ReadAll()
if err != nil {
t.Fatalf("unexpected error while reading: %s", err)
}

compareResults(t, testCase.want, got)
})
}
}

func compareResults(t *testing.T, want, got [][]string) {
if len(got) != len(want) {
t.Fatalf("got %d records, want %d", len(got), len(want))
}
Expand Down
Loading