diff --git a/cdre/fixedwidth.go b/cdre/fixedwidth.go index aec69d750..6f5b00b1a 100644 --- a/cdre/fixedwidth.go +++ b/cdre/fixedwidth.go @@ -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(" 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(" 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(" 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(" 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(" Cannot export CDR with cgrid: %s and runid: %s, error: %s", cdr.CgrId, cdr.MediationRunId)) + engine.Logger.Err(fmt.Sprintf(" 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(" 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(" 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()) } diff --git a/cdre/fixedwidth_test.go b/cdre/fixedwidth_test.go index 7533729b2..e8fadf963 100644 --- a/cdre/fixedwidth_test.go +++ b/cdre/fixedwidth_test.go @@ -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) } } diff --git a/cdre/libfixedwidth.go b/cdre/libfixedwidth.go index 5dff7b191..ddbef8c60 100644 --- a/cdre/libfixedwidth.go +++ b/cdre/libfixedwidth.go @@ -19,6 +19,7 @@ along with this program. If not, see 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 } diff --git a/cdre/libfixedwidth_test.go b/cdre/libfixedwidth_test.go index ce1a136df..7c932a302 100644 --- a/cdre/libfixedwidth_test.go +++ b/cdre/libfixedwidth_test.go @@ -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") } diff --git a/config/xmlconfig.go b/config/xmlconfig.go index ebe96f285..1ec79ec93 100644 --- a/config/xmlconfig.go +++ b/config/xmlconfig.go @@ -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 diff --git a/config/xmlconfig_test.go b/config/xmlconfig_test.go index 3c488eca3..43de27cd3 100644 --- a/config/xmlconfig_test.go +++ b/config/xmlconfig_test.go @@ -34,8 +34,8 @@ func TestParseXmlConfig(t *testing.T) { - - + + @@ -70,9 +70,9 @@ func TestParseXmlConfig(t *testing.T) { - - - + + + diff --git a/utils/consts.go b/utils/consts.go index 139be2182..2f572e37d 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -94,7 +94,6 @@ const ( FIXED_WIDTH = "fixed_width" XML_PROFILE_PREFIX = "*xml:" CDRE = "cdre" - COST_DETAILS = "cost_details" ) var (