feat(csvimport): validated CSV account parser
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
package csvimport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Row struct {
|
||||||
|
SrcLogin string
|
||||||
|
SrcPass string
|
||||||
|
DstLogin string
|
||||||
|
DstPass string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(r io.Reader) ([]Row, error) {
|
||||||
|
cr := csv.NewReader(r)
|
||||||
|
cr.FieldsPerRecord = -1 // проверяем сами
|
||||||
|
cr.TrimLeadingSpace = true
|
||||||
|
|
||||||
|
var rows []Row
|
||||||
|
seen := map[string]bool{}
|
||||||
|
line := 0
|
||||||
|
for {
|
||||||
|
rec, err := cr.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
line++
|
||||||
|
if len(rec) == 1 && strings.TrimSpace(rec[0]) == "" {
|
||||||
|
continue // пустая строка
|
||||||
|
}
|
||||||
|
if len(rec) != 4 {
|
||||||
|
return nil, fmt.Errorf("line %d: expected 4 columns, got %d", line, len(rec))
|
||||||
|
}
|
||||||
|
for i := range rec {
|
||||||
|
rec[i] = strings.TrimSpace(rec[i])
|
||||||
|
if rec[i] == "" {
|
||||||
|
return nil, fmt.Errorf("line %d: column %d is empty", line, i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seen[rec[0]] {
|
||||||
|
return nil, fmt.Errorf("line %d: duplicate src_login %q", line, rec[0])
|
||||||
|
}
|
||||||
|
seen[rec[0]] = true
|
||||||
|
rows = append(rows, Row{SrcLogin: rec[0], SrcPass: rec[1], DstLogin: rec[2], DstPass: rec[3]})
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return nil, fmt.Errorf("no rows parsed")
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package csvimport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseOK(t *testing.T) {
|
||||||
|
rows, err := Parse(strings.NewReader("a@x,p1,a@y,p2\nb@x,p3,b@y,p4\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse: %v", err)
|
||||||
|
}
|
||||||
|
if len(rows) != 2 || rows[0].SrcLogin != "a@x" || rows[1].DstPass != "p4" {
|
||||||
|
t.Fatalf("bad rows: %+v", rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRejectsBadColumns(t *testing.T) {
|
||||||
|
if _, err := Parse(strings.NewReader("a,b,c\n")); err == nil {
|
||||||
|
t.Fatal("3 columns must error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRejectsDuplicateSrc(t *testing.T) {
|
||||||
|
if _, err := Parse(strings.NewReader("a@x,p,a@y,p\na@x,q,c@y,q\n")); err == nil {
|
||||||
|
t.Fatal("duplicate src_login must error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRejectsEmptyField(t *testing.T) {
|
||||||
|
if _, err := Parse(strings.NewReader("a@x,,a@y,p\n")); err == nil {
|
||||||
|
t.Fatal("empty password must error")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user