Adding required to fixed_width filter, adding export of the header and trailer, tests

This commit is contained in:
DanB
2014-03-24 16:50:57 +01:00
parent def252d153
commit 881db9c1c4
7 changed files with 259 additions and 40 deletions

View File

@@ -21,31 +21,47 @@ package cdre
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/cgrates/cgrates/config"
"github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/utils"
"io"
"strconv"
"strings"
"time"
)
const (
COST_DETAILS = "cost_details"
FILLER = "filler"
CONSTANT = "constant"
CDRFIELD = "cdrfield"
METATAG = "metatag"
CONCATENATED_CDRFIELD = "concatenated_cdrfield"
META_EXPORTID = "export_id"
META_TIMENOW = "time_now"
META_FIRSTCDRTIME = "first_cdr_time"
META_LASTCDRTIME = "last_cdr_time"
META_NRCDRS = "cdrs_number"
META_DURCDRS = "cdrs_duration"
META_COSTCDRS = "cdrs_cost"
)
var err error
type FixedWidthCdrWriter struct {
logDb engine.LogStorage // Used to extract cost_details if these are requested
writer io.Writer
exportTemplate *config.CgrXmlCdreFwCfg
exportId string // Unique identifier or this export
exportFileName string // If defined it will overwrite the file name
roundDecimals int
header, content, trailer *bytes.Buffer
firstCdrTime, lastCdrTime time.Time
numberOfRecords int
totalDuration time.Duration
totalCost float64
}
// Return Json marshaled callCost attached to
@@ -71,7 +87,7 @@ func (fww *FixedWidthCdrWriter) cdrFieldValue(cdr *utils.StoredCdr, cfgHdr, layo
}
var cdrVal string
switch rsrField.Id {
case utils.COST_DETAILS: // Special case when we need to further extract cost_details out of logDb
case COST_DETAILS: // Special case when we need to further extract cost_details out of logDb
if cdrVal, err = fww.getCdrCostDetails(cdr.CgrId, cdr.MediationRunId); err != nil {
return "", err
}
@@ -87,18 +103,97 @@ func (fww *FixedWidthCdrWriter) cdrFieldValue(cdr *utils.StoredCdr, cfgHdr, layo
return rsrField.ParseValue(cdrVal), nil
}
func (fww *FixedWidthCdrWriter) metaHandler(tag, layout string) (string, error) {
switch tag {
case META_EXPORTID:
return fww.exportId, nil
case META_TIMENOW:
return time.Now().Format(layout), nil
case META_FIRSTCDRTIME:
return fww.firstCdrTime.Format(layout), nil
case META_LASTCDRTIME:
return fww.lastCdrTime.Format(layout), nil
case META_NRCDRS:
return strconv.Itoa(fww.numberOfRecords), nil
case META_DURCDRS:
return strconv.FormatFloat(fww.totalDuration.Seconds(), 'f', -1, 64), nil
case META_COSTCDRS:
return strconv.FormatFloat(utils.Round(fww.totalCost, fww.roundDecimals, utils.ROUNDING_MIDDLE), 'f', -1, 64), nil
default:
return "", errors.New("Unsupported METATAG")
}
return "", nil
}
// Writes the header into it's buffer
func (fww *FixedWidthCdrWriter) ComposeHeader() error {
header := ""
for _, cfgFld := range fww.exportTemplate.Header.Fields {
var outVal string
switch cfgFld.Type {
case FILLER, CONSTANT:
outVal = cfgFld.Value
case METATAG:
outVal, err = fww.metaHandler(cfgFld.Value, cfgFld.Layout)
default:
return fmt.Errorf("Unsupported field type: %s", cfgFld.Type)
}
if err != nil {
engine.Logger.Err(fmt.Sprintf("<CdreFw> Cannot export CDR header, error: %s", err.Error()))
return err
}
if fmtOut, err := FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
engine.Logger.Err(fmt.Sprintf("<CdreFw> Cannot export CDR header, error: %s", err.Error()))
return err
} else {
header += fmtOut
}
}
if len(header) == 0 { // No header data, most likely no configuration fields defined
return nil
}
header += "\n" // Done with cdr, postpend new line char
fww.header.WriteString(header)
return nil
}
// Writes the trailer into it's buffer
func (fww *FixedWidthCdrWriter) ComposeTrailer() error {
trailer := ""
for _, cfgFld := range fww.exportTemplate.Trailer.Fields {
var outVal string
switch cfgFld.Type {
case FILLER, CONSTANT:
outVal = cfgFld.Value
case METATAG:
outVal, err = fww.metaHandler(cfgFld.Value, cfgFld.Layout)
default:
return fmt.Errorf("Unsupported field type: %s", cfgFld.Type)
}
if err != nil {
engine.Logger.Err(fmt.Sprintf("<CdreFw> Cannot export CDR trailer, error: %s", err.Error()))
return err
}
if fmtOut, err := FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
engine.Logger.Err(fmt.Sprintf("<CdreFw> Cannot export CDR trailer, error: %s", err.Error()))
return err
} else {
trailer += fmtOut
}
}
if len(trailer) == 0 { // No header data, most likely no configuration fields defined
return nil
}
trailer += "\n" // Done with cdr, postpend new line char
fww.trailer.WriteString(trailer)
return nil
}
// Write individual cdr into content buffer, build stats
func (fww *FixedWidthCdrWriter) WriteCdr(cdr *utils.StoredCdr) error {
if cdr == nil || len(cdr.CgrId) == 0 { // We do not export empty CDRs
return nil
}
var err error
cdrRow := ""
for _, cfgFld := range fww.exportTemplate.Content.Fields {
@@ -118,11 +213,11 @@ func (fww *FixedWidthCdrWriter) WriteCdr(cdr *utils.StoredCdr) error {
}
}
if err != nil {
engine.Logger.Err(fmt.Sprintf("<CdreFW> Cannot export CDR with cgrid: %s and runid: %s, error: %s", cdr.CgrId, cdr.MediationRunId))
engine.Logger.Err(fmt.Sprintf("<CdreFw> Cannot export CDR with cgrid: %s and runid: %s, error: %s", cdr.CgrId, cdr.MediationRunId, err.Error()))
return err
}
if fmtOut, err := FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding); err != nil {
engine.Logger.Err(fmt.Sprintf("<CdreFW> Cannot export CDR with cgrid: %s and runid: %s, error: %s", cdr.CgrId, cdr.MediationRunId))
if fmtOut, err := FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
engine.Logger.Err(fmt.Sprintf("<CdreFw> Cannot export CDR with cgrid: %s and runid: %s, error: %s", cdr.CgrId, cdr.MediationRunId, err.Error()))
return err
} else {
cdrRow += fmtOut
@@ -142,12 +237,18 @@ func (fww *FixedWidthCdrWriter) WriteCdr(cdr *utils.StoredCdr) error {
}
fww.numberOfRecords += 1
fww.totalDuration += cdr.Duration
fww.totalCost += cdr.Cost
fww.totalCost = utils.Round(fww.totalCost, fww.roundDecimals, utils.ROUNDING_MIDDLE)
return nil
}
func (fww *FixedWidthCdrWriter) Close() {
fww.ComposeHeader()
fww.ComposeTrailer()
if fww.exportTemplate.Header != nil {
fww.ComposeHeader()
}
if fww.exportTemplate.Trailer != nil {
fww.ComposeTrailer()
}
for _, buf := range []*bytes.Buffer{fww.header, fww.content, fww.trailer} {
fww.writer.Write(buf.Bytes())
}

View File

@@ -22,16 +22,28 @@ import (
"bytes"
"github.com/cgrates/cgrates/config"
"github.com/cgrates/cgrates/utils"
"math"
"testing"
"time"
)
var hdrCfgFlds = []*config.CgrXmlCfgCdrField{
&config.CgrXmlCfgCdrField{Name: "RecordType", Type: CONSTANT, Value: "10", Width: 2},
&config.CgrXmlCfgCdrField{Name: "Filler1", Type: FILLER, Width: 3, Padding: "right"},
&config.CgrXmlCfgCdrField{Name: "NetworkProviderCode", Type: CONSTANT, Value: "VOI", Width: 3},
&config.CgrXmlCfgCdrField{Name: "FileSeqNr", Type: METATAG, Value: "export_id", Width: 5, Strip: "right", Padding: "zeroleft"},
&config.CgrXmlCfgCdrField{Name: "CutOffTime", Type: METATAG, Value: "last_cdr_time", Width: 12, Layout: "020106150400"},
&config.CgrXmlCfgCdrField{Name: "FileCreationfTime", Type: METATAG, Value: "time_now", Width: 12, Layout: "020106150400"},
&config.CgrXmlCfgCdrField{Name: "FileSpecificationVersion", Type: CONSTANT, Value: "01", Width: 2},
&config.CgrXmlCfgCdrField{Name: "Filler2", Type: FILLER, Width: 105, Padding: "right"},
}
var contentCfgFlds = []*config.CgrXmlCfgCdrField{
&config.CgrXmlCfgCdrField{Name: "RecordType", Type: CONSTANT, Value: "20", Width: 2},
&config.CgrXmlCfgCdrField{Name: "SIPTrunkID", Type: CDRFIELD, Value: utils.ACCOUNT, Width: 12, Strip: "left", Padding: "right"},
&config.CgrXmlCfgCdrField{Name: "ConnectionNumber", Type: CDRFIELD, Value: utils.SUBJECT, Width: 5, Strip: "right", Padding: "right"},
&config.CgrXmlCfgCdrField{Name: "ANumber", Type: CDRFIELD, Value: "cli", Width: 15, Strip: "xright", Padding: "right"},
&config.CgrXmlCfgCdrField{Name: "CalledNumber", Type: CDRFIELD, Value: "destination", Width: 24, Strip: "xright", Padding: "right"},
&config.CgrXmlCfgCdrField{Name: "CalledNumber", Type: CDRFIELD, Value: utils.DESTINATION, Width: 24, Strip: "xright", Padding: "right"},
&config.CgrXmlCfgCdrField{Name: "ServiceType", Type: CONSTANT, Value: "02", Width: 2},
&config.CgrXmlCfgCdrField{Name: "ServiceIdentification", Type: CONSTANT, Value: "11", Width: 4, Padding: "right"},
&config.CgrXmlCfgCdrField{Name: "StartChargingDateTime", Type: CDRFIELD, Value: utils.SETUP_TIME, Width: 12, Strip: "right", Padding: "right", Layout: "020106150400"},
@@ -49,10 +61,25 @@ var contentCfgFlds = []*config.CgrXmlCfgCdrField{
&config.CgrXmlCfgCdrField{Name: "CallMaskingIndicator", Type: CDRFIELD, Value: "calledmask", Width: 1, Strip: "right", Padding: "right"},
}
var trailerCfgFlds = []*config.CgrXmlCfgCdrField{
&config.CgrXmlCfgCdrField{Name: "RecordType", Type: CONSTANT, Value: "90", Width: 2},
&config.CgrXmlCfgCdrField{Name: "Filler1", Type: FILLER, Width: 3, Padding: "right"},
&config.CgrXmlCfgCdrField{Name: "NetworkProviderCode", Type: CONSTANT, Value: "VOI", Width: 3},
&config.CgrXmlCfgCdrField{Name: "FileSeqNr", Type: METATAG, Value: META_EXPORTID, Width: 5, Strip: "right", Padding: "zeroleft"},
&config.CgrXmlCfgCdrField{Name: "TotalNrRecords", Type: METATAG, Value: META_NRCDRS, Width: 6, Padding: "zeroleft"},
&config.CgrXmlCfgCdrField{Name: "TotalDurRecords", Type: METATAG, Value: META_DURCDRS, Width: 8, Padding: "zeroleft"},
&config.CgrXmlCfgCdrField{Name: "EarliestCDRTime", Type: METATAG, Value: META_FIRSTCDRTIME, Width: 12, Layout: "020106150400"},
&config.CgrXmlCfgCdrField{Name: "LatestCDRTime", Type: METATAG, Value: META_LASTCDRTIME, Width: 12, Layout: "020106150400"},
&config.CgrXmlCfgCdrField{Name: "Filler2", Type: FILLER, Width: 93, Padding: "right"},
}
// Write one CDR and test it's results only for content buffer
func TestWriteCdr(t *testing.T) {
wrBuf := &bytes.Buffer{}
exportTpl := &config.CgrXmlCdreFwCfg{Content: &config.CgrXmlCfgCdrContent{Fields: contentCfgFlds}}
exportTpl := &config.CgrXmlCdreFwCfg{Header: &config.CgrXmlCfgCdrHeader{Fields: hdrCfgFlds},
Content: &config.CgrXmlCfgCdrContent{Fields: contentCfgFlds},
Trailer: &config.CgrXmlCfgCdrTrailer{Fields: trailerCfgFlds},
}
fwWriter := FixedWidthCdrWriter{writer: wrBuf, exportTemplate: exportTpl, roundDecimals: 4, header: &bytes.Buffer{}, content: &bytes.Buffer{}, trailer: &bytes.Buffer{}}
cdr := &utils.StoredCdr{CgrId: utils.FSCgrId("dsafdsaf"), AccId: "dsafdsaf", CdrHost: "192.168.1.1", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org",
TOR: "call", Account: "1001", Subject: "1001", Destination: "1002",
@@ -64,20 +91,100 @@ func TestWriteCdr(t *testing.T) {
if err := fwWriter.WriteCdr(cdr); err != nil {
t.Error(err)
}
eContentOut := "201001 1001 1002 0211 07111308420010 1 3dsafdsaf 0002.3457 \n"
contentOut := fwWriter.content.String()
if len(contentOut) != 145 {
t.Error("Unexpected content length", len(contentOut))
} else if contentOut != eContentOut {
t.Errorf("Content out different than expected. Have <%s>, expecting: <%s>", contentOut, eContentOut)
}
eOut := "201001 1001 1002 0211 07111308420010 1 3dsafdsaf 0002.3457 \n"
if contentOut != eOut {
t.Errorf("Content out different than expected. Have <%s>, expecting: <%s>", contentOut, eOut)
}
eHeader := "10 VOI0000007111308420024031415390001 \n"
eTrailer := "90 VOI0000000000100000010071113084200071113084200 \n"
outBeforeWrite := ""
if wrBuf.String() != outBeforeWrite {
t.Errorf("Output buffer should be empty before write")
}
fwWriter.Close()
if wrBuf.String() != eOut {
t.Errorf("Output buffer does not contain expected info. Have <%s>, expecting: <%s>", wrBuf.String(), eOut)
allOut := wrBuf.String()
eAllOut := eHeader + eContentOut + eTrailer
if math.Mod(float64(len(allOut)), 145) != 0 {
t.Error("Unexpected export content length", len(allOut))
} else if len(allOut) != len(eAllOut) {
t.Errorf("Output does not match expected length. Have output %q, expecting: %q", allOut, eAllOut)
}
// Test stats
if !fwWriter.firstCdrTime.Equal(cdr.SetupTime) {
t.Error("Unexpected firstCdrTime in stats: ", fwWriter.firstCdrTime)
} else if !fwWriter.lastCdrTime.Equal(cdr.SetupTime) {
t.Error("Unexpected lastCdrTime in stats: ", fwWriter.lastCdrTime)
} else if fwWriter.numberOfRecords != 1 {
t.Error("Unexpected number of records in the stats: ", fwWriter.numberOfRecords)
} else if fwWriter.totalDuration != cdr.Duration {
t.Error("Unexpected total duration in the stats: ", fwWriter.totalDuration)
} else if fwWriter.totalCost != utils.Round(cdr.Cost, fwWriter.roundDecimals, utils.ROUNDING_MIDDLE) {
t.Error("Unexpected total cost in the stats: ", fwWriter.totalCost)
}
}
func TestWriteCdrs(t *testing.T) {
wrBuf := &bytes.Buffer{}
exportTpl := &config.CgrXmlCdreFwCfg{Header: &config.CgrXmlCfgCdrHeader{Fields: hdrCfgFlds},
Content: &config.CgrXmlCfgCdrContent{Fields: contentCfgFlds},
Trailer: &config.CgrXmlCfgCdrTrailer{Fields: trailerCfgFlds},
}
fwWriter := FixedWidthCdrWriter{writer: wrBuf, exportTemplate: exportTpl, roundDecimals: 4, header: &bytes.Buffer{}, content: &bytes.Buffer{}, trailer: &bytes.Buffer{}}
cdr1 := &utils.StoredCdr{CgrId: utils.FSCgrId("aaa1"), AccId: "aaa1", CdrHost: "192.168.1.1", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org",
TOR: "call", Account: "1001", Subject: "1001", Destination: "1010",
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
Duration: time.Duration(10) * time.Second, MediationRunId: utils.DEFAULT_RUNID, Cost: 2.25,
ExtraFields: map[string]string{"productnumber": "12341", "fieldextr2": "valextr2"},
}
cdr2 := &utils.StoredCdr{CgrId: utils.FSCgrId("aaa2"), AccId: "aaa2", CdrHost: "192.168.1.2", ReqType: "prepaid", Direction: "*out", Tenant: "cgrates.org",
TOR: "call", Account: "1002", Subject: "1002", Destination: "1011",
SetupTime: time.Date(2013, 11, 7, 7, 42, 20, 0, time.UTC),
AnswerTime: time.Date(2013, 11, 7, 7, 42, 26, 0, time.UTC),
Duration: time.Duration(5) * time.Minute, MediationRunId: utils.DEFAULT_RUNID, Cost: 1.40001,
ExtraFields: map[string]string{"productnumber": "12342", "fieldextr2": "valextr2"},
}
cdr3 := &utils.StoredCdr{}
cdr4 := &utils.StoredCdr{CgrId: utils.FSCgrId("aaa3"), AccId: "aaa4", CdrHost: "192.168.1.4", ReqType: "postpaid", Direction: "*out", Tenant: "cgrates.org",
TOR: "call", Account: "1004", Subject: "1004", Destination: "1013",
SetupTime: time.Date(2013, 11, 7, 9, 42, 18, 0, time.UTC),
AnswerTime: time.Date(2013, 11, 7, 9, 42, 26, 0, time.UTC),
Duration: time.Duration(20) * time.Second, MediationRunId: utils.DEFAULT_RUNID, Cost: 2.34567,
ExtraFields: map[string]string{"productnumber": "12344", "fieldextr2": "valextr2"},
}
for _, cdr := range []*utils.StoredCdr{cdr1, cdr2, cdr3, cdr4} {
if err := fwWriter.WriteCdr(cdr); err != nil {
t.Error(err)
}
contentOut := fwWriter.content.String()
if math.Mod(float64(len(contentOut)), 145) != 0 { // Rest must be 0 always, so content is always multiple of 145 which is our row fixLength
t.Error("Unexpected content length", len(contentOut))
}
}
if len(wrBuf.String()) != 0 {
t.Errorf("Output buffer should be empty before write")
}
fwWriter.Close()
if len(wrBuf.String()) != 725 {
t.Error("Output buffer does not contain expected info. Expecting len: 725, got: ", len(wrBuf.String()))
}
// Test stats
if !fwWriter.firstCdrTime.Equal(cdr2.SetupTime) {
t.Error("Unexpected firstCdrTime in stats: ", fwWriter.firstCdrTime)
}
if !fwWriter.lastCdrTime.Equal(cdr4.SetupTime) {
t.Error("Unexpected lastCdrTime in stats: ", fwWriter.lastCdrTime)
}
if fwWriter.numberOfRecords != 3 {
t.Error("Unexpected number of records in the stats: ", fwWriter.numberOfRecords)
}
if fwWriter.totalDuration != time.Duration(330)*time.Second {
t.Error("Unexpected total duration in the stats: ", fwWriter.totalDuration)
}
if fwWriter.totalCost != 5.9957 {
t.Error("Unexpected total cost in the stats: ", fwWriter.totalCost)
}
}

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
package cdre
import (
"errors"
"fmt"
)
@@ -29,7 +30,10 @@ import (
// width - the field width
// strip - if present it will specify the strip strategy, when missing strip will not be allowed
// padding - if present it will specify the padding strategy to use, left, right, zeroleft, zeroright
func FmtFieldWidth(source string, width int, strip, padding string) (string, error) {
func FmtFieldWidth(source string, width int, strip, padding string, mandatory bool) (string, error) {
if mandatory && len(source) == 0 {
return "", errors.New("Empty source value")
}
if len(source) == width { // the source is exactly the maximum length
return source, nil
}

View File

@@ -22,8 +22,15 @@ import (
"testing"
)
func TestMandatory(t *testing.T) {
_, err := FmtFieldWidth("", 0, "", "", true)
if err == nil {
t.Errorf("Failed to detect mandatory value")
}
}
func TestMaxLen(t *testing.T) {
result, err := FmtFieldWidth("test", 4, "", "")
result, err := FmtFieldWidth("test", 4, "", "", false)
expected := "test"
if err != nil || result != expected {
t.Errorf("Expected \"test\" was \"%s\"", result)
@@ -31,7 +38,7 @@ func TestMaxLen(t *testing.T) {
}
func TestRPadding(t *testing.T) {
result, err := FmtFieldWidth("test", 8, "", "right")
result, err := FmtFieldWidth("test", 8, "", "right", false)
expected := "test "
if err != nil || result != expected {
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
@@ -39,7 +46,7 @@ func TestRPadding(t *testing.T) {
}
func TestPaddingFiller(t *testing.T) {
result, err := FmtFieldWidth("", 8, "", "right")
result, err := FmtFieldWidth("", 8, "", "right", false)
expected := " "
if err != nil || result != expected {
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
@@ -47,7 +54,7 @@ func TestPaddingFiller(t *testing.T) {
}
func TestLPadding(t *testing.T) {
result, err := FmtFieldWidth("test", 8, "", "left")
result, err := FmtFieldWidth("test", 8, "", "left", false)
expected := " test"
if err != nil || result != expected {
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
@@ -55,7 +62,7 @@ func TestLPadding(t *testing.T) {
}
func TestZeroLPadding(t *testing.T) {
result, err := FmtFieldWidth("test", 8, "", "zeroleft")
result, err := FmtFieldWidth("test", 8, "", "zeroleft", false)
expected := "0000test"
if err != nil || result != expected {
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
@@ -63,7 +70,7 @@ func TestZeroLPadding(t *testing.T) {
}
func TestRStrip(t *testing.T) {
result, err := FmtFieldWidth("test", 2, "right", "")
result, err := FmtFieldWidth("test", 2, "right", "", false)
expected := "te"
if err != nil || result != expected {
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
@@ -71,7 +78,7 @@ func TestRStrip(t *testing.T) {
}
func TestXRStrip(t *testing.T) {
result, err := FmtFieldWidth("test", 3, "xright", "")
result, err := FmtFieldWidth("test", 3, "xright", "", false)
expected := "tex"
if err != nil || result != expected {
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
@@ -79,7 +86,7 @@ func TestXRStrip(t *testing.T) {
}
func TestLStrip(t *testing.T) {
result, err := FmtFieldWidth("test", 2, "left", "")
result, err := FmtFieldWidth("test", 2, "left", "", false)
expected := "st"
if err != nil || result != expected {
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
@@ -87,7 +94,7 @@ func TestLStrip(t *testing.T) {
}
func TestXLStrip(t *testing.T) {
result, err := FmtFieldWidth("test", 3, "xleft", "")
result, err := FmtFieldWidth("test", 3, "xleft", "", false)
expected := "xst"
if err != nil || result != expected {
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
@@ -95,14 +102,14 @@ func TestXLStrip(t *testing.T) {
}
func TestStripNotAllowed(t *testing.T) {
_, err := FmtFieldWidth("test", 3, "", "")
_, err := FmtFieldWidth("test", 3, "", "", false)
if err == nil {
t.Error("Expected error")
}
}
func TestPaddingNotAllowed(t *testing.T) {
_, err := FmtFieldWidth("test", 5, "", "")
_, err := FmtFieldWidth("test", 5, "", "", false)
if err == nil {
t.Error("Expected error")
}

View File

@@ -79,14 +79,15 @@ type CgrXmlCfgCdrTrailer struct {
// CDR field
type CgrXmlCfgCdrField struct {
XMLName xml.Name `xml:"field"`
Name string `xml:"name,attr"`
Type string `xml:"type,attr"`
Value string `xml:"value,attr"`
Width int `xml:"width,attr"` // Field width
Strip string `xml:"strip,attr"` // Strip strategy in case value is bigger than field width <""|left|xleft|right|xright>
Padding string `xml:"padding,attr"` // Padding strategy in case of value is smaller than width <""left|zeroleft|right>
Layout string `xml:"layout,attr"` // Eg. time format layout
XMLName xml.Name `xml:"field"`
Name string `xml:"name,attr"`
Type string `xml:"type,attr"`
Value string `xml:"value,attr"`
Width int `xml:"width,attr"` // Field width
Strip string `xml:"strip,attr"` // Strip strategy in case value is bigger than field width <""|left|xleft|right|xright>
Padding string `xml:"padding,attr"` // Padding strategy in case of value is smaller than width <""left|zeroleft|right>
Layout string `xml:"layout,attr"` // Eg. time format layout
Mandatory bool `xml:"layout,attr"` // If field is mandatory, empty value will be considered as error and CDR will not be exported
}
// Avoid building from raw config string always, so build cache here

View File

@@ -34,8 +34,8 @@ func TestParseXmlConfig(t *testing.T) {
<field name="RecordType" type="constant" value="10" width="2"/>
<field name="Filler1" type="filler" width="3"/>
<field name="NetworkProviderCode" type="constant" value="VOI" width="3"/>
<field name="FileSeqNr" type="metatag" value="exportid" padding="zeroleft" width="5"/>
<field name="CutOffTime" type="metatag" value="time_lastcdr" layout="020106150400" width="12"/>
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5"/>
<field name="CutOffTime" type="metatag" value="last_cdr_time" layout="020106150400" width="12"/>
<field name="FileCreationfTime" type="metatag" value="time_now" layout="020106150400" width="12"/>
<field name="FileSpecificationVersion" type="constant" value="01" width="2"/>
<field name="Filler2" type="filler" width="105"/>
@@ -70,9 +70,9 @@ func TestParseXmlConfig(t *testing.T) {
<field name="RecordType" type="constant" value="90" width="2"/>
<field name="Filler1" type="filler" width="3"/>
<field name="NetworkProviderCode" type="constant" value="VOI" width="3"/>
<field name="FileSeqNr" type="metatag" value="exportid" padding="zeroleft" width="5"/>
<field name="TotalNrRecords" type="metatag" value="nr_cdrs" padding="zeroleft" width="6"/>
<field name="TotalDurRecords" type="metatag" value="dur_cdrs" padding="zeroleft" width="8"/>
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5"/>
<field name="TotalNrRecords" type="metatag" value="cdrs_number" padding="zeroleft" width="6"/>
<field name="TotalDurRecords" type="metatag" value="cdrs_duration" padding="zeroleft" width="8"/>
<field name="EarliestCDRTime" type="metatag" value="first_cdr_time" layout="020106150400" width="12"/>
<field name="LatestCDRTime" type="metatag" value="last_cdr_time" layout="020106150400" width="12"/>
<field name="Filler1" type="filler" width="93"/>

View File

@@ -94,7 +94,6 @@ const (
FIXED_WIDTH = "fixed_width"
XML_PROFILE_PREFIX = "*xml:"
CDRE = "cdre"
COST_DETAILS = "cost_details"
)
var (