Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.

Commit 3b9e999

Browse files
authored
Merge pull request #1 from obalunenko/develop
Implement validator
2 parents 4ad467e + cec2a11 commit 3b9e999

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+19639
-0
lines changed

.github/workflows/go.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Go
2+
on: [push]
3+
jobs:
4+
5+
vet-test:
6+
name: Vet and test
7+
runs-on: ubuntu-latest
8+
steps:
9+
- name: Set up Go 1.15
10+
uses: actions/setup-go@v1
11+
with:
12+
go-version: 1.15
13+
id: go
14+
15+
- name: Check out code into the Go module directory
16+
uses: actions/checkout@v1
17+
18+
- name: Get dependencies
19+
run: go mod verify
20+
21+
- name: Vet
22+
run: go vet $(go list ./...)
23+
24+
- name: Test
25+
run: go test -race -v $(go list ./...)

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@
1313

1414
# Dependency directories (remove the comment below to include it)
1515
# vendor/
16+
17+
.idea/
18+
.vscode/

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/obalunenko/csvvalidator
2+
3+
go 1.15
4+
5+
require github.com/stretchr/testify v1.7.0

go.sum

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
6+
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
7+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
9+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
10+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
11+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

validation.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Package csvvalidator provide validation of csv rows according to set up rules.
2+
package csvvalidator
3+
4+
import (
5+
"errors"
6+
"fmt"
7+
"strings"
8+
)
9+
10+
// Column represents csv column.
11+
type Column struct {
12+
Number uint
13+
Name string
14+
}
15+
16+
// NewColumn creates new Column with passed column number,
17+
// name of column is optional.
18+
func NewColumn(number uint, name ...string) Column {
19+
var resName string
20+
if len(name) != 0 {
21+
resName = strings.Join(name, " ")
22+
}
23+
return Column{
24+
Number: number,
25+
Name: resName,
26+
}
27+
28+
}
29+
30+
func (c *Column) String() string {
31+
if c.Name == "" {
32+
return fmt.Sprintf("%d", c.Number)
33+
}
34+
return fmt.Sprintf("%d:%s", c.Number, c.Name)
35+
}
36+
37+
// Rule represent column row validation rule.
38+
type Rule struct {
39+
MaxLength uint // if 0 - no limit
40+
MinLength uint // if 0 - no limit
41+
RestrictedChars []string
42+
}
43+
44+
var (
45+
// RuleNotEmpty defines a rule for field that require it to be not empty.
46+
RuleNotEmpty = Rule{
47+
MaxLength: 0,
48+
MinLength: 1,
49+
RestrictedChars: nil,
50+
}
51+
// RuleShouldBeEmpty defines a rule for field that require it to be empty.
52+
RuleShouldBeEmpty = Rule{
53+
MaxLength: 0,
54+
MinLength: 0,
55+
RestrictedChars: nil,
56+
}
57+
)
58+
59+
// ValidationRules represent rules for columns validation,
60+
// counting of columns in row starts from 0.
61+
type ValidationRules map[Column]Rule
62+
63+
// Row represent csv row:
64+
// - total columns number for row
65+
// - validation rules for each column.
66+
type Row struct {
67+
ColumnsTotalNum uint
68+
ColumnsRules ValidationRules
69+
}
70+
71+
// ValidateRow validates passed row.
72+
func (row Row) ValidateRow(rec []string) error {
73+
if row.ColumnsTotalNum == 0 {
74+
return errors.New("invalid columns total number specified in rules")
75+
}
76+
77+
if uint(len(rec)) != row.ColumnsTotalNum {
78+
return fmt.Errorf("invalid row [row:[%v]; columns num:[%d]; should be: [%d]]",
79+
rec, len(rec), row.ColumnsTotalNum)
80+
}
81+
82+
return row.ColumnsRules.ValidateRow(rec)
83+
}
84+
85+
// ValidateRow validates passed row,
86+
// counting of columns in row starts from 0.
87+
func (rules ValidationRules) ValidateRow(rec []string) error {
88+
if len(rules) == 0 {
89+
return errors.New("validation rules not specified")
90+
}
91+
if len(rec) == 0 {
92+
return errors.New("empty row")
93+
}
94+
95+
for col, rule := range rules {
96+
if err := validateColumn(rec[col.Number], rule); err != nil {
97+
return fmt.Errorf("invalid column [row:%s; column:%s]: %w", rec,col.String(), err)
98+
}
99+
}
100+
101+
return nil
102+
}
103+
104+
func validateColumn(data string, rule Rule) error {
105+
if rule.MinLength != 0 && data == "" {
106+
return errors.New("should not be empty")
107+
}
108+
109+
if rule.MinLength == rule.MaxLength {
110+
if uint(len(data)) != rule.MinLength {
111+
return fmt.Errorf("invalid length [column:[%s]; has len:[%d]; should be:[%d]]",
112+
data,len(data),rule.MinLength)
113+
}
114+
}
115+
116+
if uint(len(data)) < rule.MinLength {
117+
return fmt.Errorf("invalid length [column:[%s]; has len:[%d]; should be at less[%d]]",
118+
data, len(data),rule.MinLength)
119+
}
120+
121+
if uint(len(data)) > rule.MaxLength && rule.MaxLength != 0 {
122+
return fmt.Errorf("invalid length [column:[%s]; has len:[%d]; should be at less[%d]]",
123+
data, len(data),rule.MaxLength)
124+
}
125+
126+
if rule.RestrictedChars != nil {
127+
if strings.ContainsAny(data, strings.Join(rule.RestrictedChars, "")) {
128+
return fmt.Errorf("contains restricted characters [column:[%s]]", data)
129+
}
130+
}
131+
132+
return nil
133+
}

validation_example_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package csvvalidator_test
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/obalunenko/csvvalidator"
7+
)
8+
9+
func ExampleValidationRules_ValidateRow() {
10+
// initialise columns
11+
var (
12+
clientNameCol = csvvalidator.NewColumn(0, "Client Name")
13+
transactionDateCol = csvvalidator.NewColumn(1, "Transaction Date")
14+
transactionAmountCol = csvvalidator.NewColumn(2, "Transaction Amount")
15+
debitCreditCodeCol = csvvalidator.NewColumn(3, "Debit Credit Code")
16+
customerIDCol = csvvalidator.NewColumn(4, "Customer ID")
17+
18+
// set total columns number
19+
totalColNum = uint(5)
20+
)
21+
22+
// set up validation rules
23+
var rowRules = csvvalidator.ValidationRules{
24+
clientNameCol: csvvalidator.Rule{
25+
MaxLength: 4,
26+
MinLength: 4,
27+
RestrictedChars: nil,
28+
},
29+
transactionDateCol: csvvalidator.Rule{
30+
MaxLength: 10,
31+
MinLength: 10,
32+
RestrictedChars: nil,
33+
},
34+
transactionAmountCol: csvvalidator.Rule{
35+
MaxLength: 14,
36+
MinLength: 4,
37+
RestrictedChars: nil,
38+
},
39+
debitCreditCodeCol: csvvalidator.Rule{
40+
MaxLength: 1,
41+
MinLength: 1,
42+
RestrictedChars: nil,
43+
},
44+
customerIDCol: csvvalidator.Rule{
45+
MaxLength: 10,
46+
MinLength: 1,
47+
RestrictedChars: nil,
48+
},
49+
}
50+
51+
// create csv row ([]string)
52+
var testDataRow = make([]string, totalColNum)
53+
testDataRow[clientNameCol.Number] = "Test Client Name too long" // exceed maxlength
54+
testDataRow[transactionDateCol.Number] = "2019/05/30"
55+
testDataRow[transactionAmountCol.Number] = "100.45"
56+
testDataRow[debitCreditCodeCol.Number] = "C"
57+
testDataRow[customerIDCol.Number] = "ID"
58+
59+
// validate row
60+
err := rowRules.ValidateRow(testDataRow)
61+
fmt.Print(err)
62+
// Output:
63+
// invalid column [row:[Test Client Name too long 2019/05/30 100.45 C ID]; column:0:Client Name]: invalid length [column:[Test Client Name too long]; has len:[25]; should be:[4]]
64+
}
65+
66+
func ExampleNewColumn() {
67+
clientNameCol := csvvalidator.NewColumn(0, "Client Name")
68+
fmt.Print(clientNameCol.String())
69+
// Output:
70+
// 0:Client Name
71+
}

0 commit comments

Comments
 (0)