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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions extensions/omniv21/fileformat/edi/seg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package edi

import (
"github.com/jf-tech/go-corelib/maths"
)

const (
segTypeSeg = "segment"
segTypeGroup = "segment_group"
)

const (
fqdnDelim = "/"
rootSegName = "#root"
)

type elem struct {
Name string `json:"name,omitempty"`
Index int `json:"index,omitempty"`
ComponentIndex *int `json:"component_index,omitempty"`
EmptyIfMissing bool `json:"empty_if_missing,omitempty"`
}

type segDecl struct {
Name string `json:"name,omitempty"`
Type *string `json:"type,omitempty"`
IsTarget bool `json:"is_target,omitempty"`
Min *int `json:"min,omitempty"`
Max *int `json:"max,omitempty"`
Elems []elem `json:"elements,omitempty"`
Children []*segDecl `json:"child_segments,omitempty"`
fqdn string // internal computed field
}

func (d *segDecl) isGroup() bool {
return d.Type != nil && *d.Type == segTypeGroup
}

func (d *segDecl) minOccurs() int {
switch d.Min {
case nil:
// for majority cases, segments have min=1, max=1, so default nil to 1
return 1
default:
return *d.Min
}
}

func (d *segDecl) maxOccurs() int {
switch {
case d.Max == nil:
// for majority cases, segments have min=1, max=1, so default nil to 1
return 1
case *d.Max < 0:
// typically schema writer uses -1 to indicate infinite; practically max int is good enough :)
return maths.MaxIntValue
default:
return *d.Max
}
}

func (d *segDecl) matchSegName(segName string) bool {
switch d.isGroup() {
case true:
// Group (or so called loop) itself doesn't have a segment name in EDI file (we do assign a
// name to it for xpath query reference, but that name isn't a segment name per se). A
// group/loop's first non-group child, recursively if necessary, can be used as the group's
// identifying segment name, per EDI standard. Meaning if a group's first non-group child's
// segment exists in an EDI file, then this group has an instance in the file. If the first
// non-group child's segment isn't found, then we (the standard) assume the group is skipped.
// The explanation can be found:
// - https://www.gxs.co.uk/wp-content/uploads/tutorial_ansi.pdf (around page 9), quote
// "...loop is optional, but if any segment in the loop is used, the first segment
// within the loop becomes mandatory..."
// - https://github.com/smooks/smooks-edi-cartridge/blob/54f97e89156114e13e1acd3b3c46fe9a4234918c/edi-sax/src/main/java/org/smooks/edi/edisax/model/internal/SegmentGroup.java#L68
return len(d.Children) > 0 && d.Children[0].matchSegName(segName)
default:
return d.Name == segName
}
}
89 changes: 89 additions & 0 deletions extensions/omniv21/fileformat/edi/seg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package edi

import (
"testing"

"github.com/jf-tech/go-corelib/maths"
"github.com/jf-tech/go-corelib/strs"
"github.com/jf-tech/go-corelib/testlib"
"github.com/stretchr/testify/assert"
)

func TestSegDeclIsGroup(t *testing.T) {
assert.False(t, (&segDecl{}).isGroup())
assert.False(t, (&segDecl{Type: strs.StrPtr(segTypeSeg)}).isGroup())
assert.False(t, (&segDecl{Type: strs.StrPtr("something")}).isGroup())
assert.True(t, (&segDecl{Type: strs.StrPtr(segTypeGroup)}).isGroup())
}

func TestSegDeclMinMaxOccurs(t *testing.T) {
for _, test := range []struct {
name string
min *int
max *int
expectedMin int
expectedMax int
}{
{
name: "default",
min: nil,
max: nil,
expectedMin: 1,
expectedMax: 1,
},
{
name: "min/max=0",
min: testlib.IntPtr(0),
max: testlib.IntPtr(0),
expectedMin: 0,
expectedMax: 0,
},
{
name: "max unlimited",
min: nil,
max: testlib.IntPtr(-1),
expectedMin: 1,
expectedMax: maths.MaxIntValue,
},
{
name: "min/max finite",
min: testlib.IntPtr(3),
max: testlib.IntPtr(5),
expectedMin: 3,
expectedMax: 5,
},
} {
t.Run(test.name, func(t *testing.T) {
s := &segDecl{Min: test.min, Max: test.max}
assert.Equal(t, test.expectedMin, s.minOccurs())
assert.Equal(t, test.expectedMax, s.maxOccurs())
})
}
}

func TestSegDeclMatchSegName(t *testing.T) {
/*
ISA
GS
ST
B10
GE
*/
b10 := &segDecl{Name: "B10"}
st := &segDecl{Name: "ST", Type: strs.StrPtr(segTypeGroup), Children: []*segDecl{b10}}
gs := &segDecl{Name: "GS", Type: strs.StrPtr(segTypeGroup), Children: []*segDecl{st}}
ge := &segDecl{Name: "GE"}
isa := &segDecl{Name: "ISA", Children: []*segDecl{gs, ge}}

assert.True(t, isa.matchSegName("ISA"))
assert.False(t, isa.matchSegName("GS"))
assert.False(t, isa.matchSegName("ST"))
assert.False(t, isa.matchSegName("B10"))

assert.False(t, gs.matchSegName("GS"))
assert.False(t, gs.matchSegName("ST"))
assert.True(t, gs.matchSegName("B10"))

assert.False(t, st.matchSegName("ST"))
assert.True(t, st.matchSegName("B10"))
}
4 changes: 2 additions & 2 deletions extensions/omniv21/samples/csv/1_weather_data_csv.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
"name": "dateTimeToRFC3339",
"args": [
{ "xpath": "DATE" },
{ "const": "", "keep_empty_or_null": true, "_comment": "input timezone" },
{ "const": "", "keep_empty_or_null": true, "_comment": "output timezone" }
{ "const": "", "_comment": "input timezone" },
{ "const": "", "_comment": "output timezone" }
]
}},
"high_temperature_fahrenheit": { "xpath": "HIGH_TEMP_C", "template": "template_c_to_f" },
Expand Down