mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
Adding required to fixed_width filter, adding export of the header and trailer, tests
This commit is contained in:
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -94,7 +94,6 @@ const (
|
||||
FIXED_WIDTH = "fixed_width"
|
||||
XML_PROFILE_PREFIX = "*xml:"
|
||||
CDRE = "cdre"
|
||||
COST_DETAILS = "cost_details"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
Reference in New Issue
Block a user