Skip to content

Commit f003f99

Browse files
authored
Adding csv2 decl validation (#174)
1 parent 2a466fc commit f003f99

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package csv
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/jf-tech/go-corelib/caches"
7+
"github.com/jf-tech/go-corelib/strs"
8+
)
9+
10+
type validateCtx struct {
11+
seenTarget bool
12+
}
13+
14+
func (ctx *validateCtx) validateFileDecl(fileDecl *FileDecl) error {
15+
for _, decl := range fileDecl.Records {
16+
if err := ctx.validateRecordDecl(decl.Name, decl); err != nil {
17+
return err
18+
}
19+
}
20+
if !ctx.seenTarget && len(fileDecl.Records) > 0 {
21+
// for easy of use and convenience, if no is_target=true record is specified, then
22+
// the first one will be automatically designated as target record.
23+
fileDecl.Records[0].IsTarget = true
24+
}
25+
return nil
26+
}
27+
28+
func (ctx *validateCtx) validateRecordDecl(fqdn string, decl *RecordDecl) (err error) {
29+
decl.fqdn = fqdn
30+
if decl.Header != nil {
31+
if decl.headerRegexp, err = caches.GetRegex(*decl.Header); err != nil {
32+
return fmt.Errorf(
33+
"record/record_group '%s' has an invalid 'header' regexp '%s': %s",
34+
fqdn, *decl.Header, err.Error())
35+
}
36+
}
37+
if decl.Footer != nil {
38+
if decl.footerRegexp, err = caches.GetRegex(*decl.Footer); err != nil {
39+
return fmt.Errorf(
40+
"record/record_group '%s' has an invalid 'footer' regexp '%s': %s",
41+
fqdn, *decl.Footer, err.Error())
42+
}
43+
}
44+
if decl.Group() {
45+
if len(decl.Columns) > 0 {
46+
return fmt.Errorf("record_group '%s' must not have any columns", fqdn)
47+
}
48+
if len(decl.Children) <= 0 {
49+
return fmt.Errorf(
50+
"record_group '%s' must have at least one child record/record_group", fqdn)
51+
}
52+
}
53+
if decl.Target() {
54+
if ctx.seenTarget {
55+
return fmt.Errorf(
56+
"a second record/record_group ('%s') with 'is_target' = true is not allowed",
57+
fqdn)
58+
}
59+
ctx.seenTarget = true
60+
}
61+
if decl.MinOccurs() > decl.MaxOccurs() {
62+
return fmt.Errorf("record/record_group '%s' has 'min' value %d > 'max' value %d",
63+
fqdn, decl.MinOccurs(), decl.MaxOccurs())
64+
}
65+
for _, c := range decl.Children {
66+
if err = ctx.validateRecordDecl(strs.BuildFQDN2("/", fqdn, c.Name), c); err != nil {
67+
return err
68+
}
69+
}
70+
decl.childRecDecls = toFlatFileRecDecls(decl.Children)
71+
return nil
72+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package csv
2+
3+
import (
4+
"testing"
5+
6+
"github.com/jf-tech/go-corelib/strs"
7+
"github.com/jf-tech/go-corelib/testlib"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestValidateFileDecl_AutoTargetFirstRecord(t *testing.T) {
12+
decl := &FileDecl{
13+
Records: []*RecordDecl{
14+
{Name: "A"},
15+
{Name: "B"},
16+
},
17+
}
18+
assert.False(t, decl.Records[0].Target())
19+
assert.False(t, decl.Records[1].Target())
20+
err := (&validateCtx{}).validateFileDecl(decl)
21+
assert.NoError(t, err)
22+
assert.True(t, decl.Records[0].Target())
23+
assert.False(t, decl.Records[1].Target())
24+
}
25+
26+
func TestValidateFileDecl_InvalidHeaderRegexp(t *testing.T) {
27+
err := (&validateCtx{}).validateFileDecl(&FileDecl{
28+
Records: []*RecordDecl{
29+
{Name: "A", Header: strs.StrPtr("[invalid")},
30+
},
31+
})
32+
assert.Error(t, err)
33+
assert.Equal(t,
34+
"record/record_group 'A' has an invalid 'header' regexp '[invalid': error parsing regexp: missing closing ]: `[invalid`",
35+
err.Error())
36+
}
37+
38+
func TestValidateFileDecl_InvalidFooterRegexp(t *testing.T) {
39+
err := (&validateCtx{}).validateFileDecl(&FileDecl{
40+
Records: []*RecordDecl{
41+
{Name: "A", Footer: strs.StrPtr("[invalid")},
42+
},
43+
})
44+
assert.Error(t, err)
45+
assert.Equal(t,
46+
"record/record_group 'A' has an invalid 'footer' regexp '[invalid': error parsing regexp: missing closing ]: `[invalid`",
47+
err.Error())
48+
}
49+
50+
func TestValidateFileDecl_GroupHasColumns(t *testing.T) {
51+
err := (&validateCtx{}).validateFileDecl(&FileDecl{
52+
Records: []*RecordDecl{
53+
{
54+
Name: "A",
55+
Type: strs.StrPtr(typeGroup),
56+
Columns: []*ColumnDecl{{}},
57+
Children: []*RecordDecl{{}},
58+
},
59+
},
60+
})
61+
assert.Error(t, err)
62+
assert.Equal(t, `record_group 'A' must not have any columns`, err.Error())
63+
}
64+
65+
func TestValidateFileDecl_GroupHasNoChildren(t *testing.T) {
66+
err := (&validateCtx{}).validateFileDecl(&FileDecl{
67+
Records: []*RecordDecl{
68+
{Name: "A", Type: strs.StrPtr(typeGroup), IsTarget: true},
69+
},
70+
})
71+
assert.Error(t, err)
72+
assert.Equal(t,
73+
`record_group 'A' must have at least one child record/record_group`, err.Error())
74+
}
75+
76+
func TestValidateFileDecl_TwoIsTarget(t *testing.T) {
77+
err := (&validateCtx{}).validateFileDecl(&FileDecl{
78+
Records: []*RecordDecl{
79+
{Name: "A", IsTarget: true},
80+
{Name: "B", Type: strs.StrPtr(typeGroup), Children: []*RecordDecl{
81+
{Name: "C", IsTarget: true},
82+
}},
83+
},
84+
})
85+
assert.Error(t, err)
86+
assert.Equal(t,
87+
`a second record/record_group ('B/C') with 'is_target' = true is not allowed`,
88+
err.Error())
89+
}
90+
91+
func TestValidateFileDecl_MinGreaterThanMax(t *testing.T) {
92+
err := (&validateCtx{}).validateFileDecl(&FileDecl{
93+
Records: []*RecordDecl{
94+
{Name: "A", Children: []*RecordDecl{
95+
{Name: "B", Min: testlib.IntPtr(2), Max: testlib.IntPtr(1)}}},
96+
},
97+
})
98+
assert.Error(t, err)
99+
assert.Equal(t, `record/record_group 'A/B' has 'min' value 2 > 'max' value 1`, err.Error())
100+
}
101+
102+
func TestValidateFileDecl_Success(t *testing.T) {
103+
col1 := &ColumnDecl{Name: "c1"}
104+
col2 := &ColumnDecl{Name: "c2"}
105+
col3 := &ColumnDecl{Name: "c3"}
106+
fd := &FileDecl{
107+
Records: []*RecordDecl{
108+
{
109+
Name: "A",
110+
Header: strs.StrPtr("^A_BEGIN$"),
111+
Footer: strs.StrPtr("^A_END$"),
112+
Children: []*RecordDecl{
113+
{
114+
Name: "B", IsTarget: true,
115+
Columns: []*ColumnDecl{col1, col2, col3},
116+
},
117+
},
118+
},
119+
},
120+
}
121+
err := (&validateCtx{}).validateFileDecl(fd)
122+
assert.NoError(t, err)
123+
assert.Equal(t, "A", fd.Records[0].fqdn)
124+
assert.True(t, fd.Records[0].matchHeader([]byte("A_BEGIN")))
125+
assert.True(t, fd.Records[0].matchFooter([]byte("A_END")))
126+
assert.Equal(t, 1, len(fd.Records[0].childRecDecls))
127+
assert.Same(t, fd.Records[0].Children[0], fd.Records[0].childRecDecls[0].(*RecordDecl))
128+
assert.Equal(t, "A/B", fd.Records[0].Children[0].fqdn)
129+
assert.Equal(t, []*ColumnDecl{col1, col2, col3}, fd.Records[0].Children[0].Columns)
130+
}

0 commit comments

Comments
 (0)