mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
Basic implementation of fixed_width cdr export content
This commit is contained in:
@@ -68,7 +68,7 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E
|
||||
}
|
||||
csvWriter := cdre.NewCsvCdrWriter(fileOut, self.Config.RoundingDecimals, exportedFields)
|
||||
for _, cdr := range cdrs {
|
||||
if err := csvWriter.Write(cdr); err != nil {
|
||||
if err := csvWriter.WriteCdr(cdr); err != nil {
|
||||
os.Remove(fileName)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -23,6 +23,6 @@ import (
|
||||
)
|
||||
|
||||
type CdrWriter interface {
|
||||
Write(cdr *utils.StoredCdr) string
|
||||
WriteCdr(cdr *utils.StoredCdr) string
|
||||
Close()
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ func NewCsvCdrWriter(writer io.Writer, roundDecimals int, exportedFields []*util
|
||||
return &CsvCdrWriter{csv.NewWriter(writer), roundDecimals, exportedFields}
|
||||
}
|
||||
|
||||
func (csvwr *CsvCdrWriter) Write(cdr *utils.StoredCdr) error {
|
||||
func (csvwr *CsvCdrWriter) WriteCdr(cdr *utils.StoredCdr) error {
|
||||
row := make([]string, len(csvwr.exportedFields))
|
||||
for idx, fld := range csvwr.exportedFields { // Add primary fields
|
||||
for idx, fld := range csvwr.exportedFields {
|
||||
var fldVal string
|
||||
if fld.Id == utils.COST {
|
||||
fldVal = cdr.FormatCost(csvwr.roundDecimals)
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestCsvCdrWriter(t *testing.T) {
|
||||
Duration: time.Duration(10) * time.Second, MediationRunId: utils.DEFAULT_RUNID,
|
||||
ExtraFields: map[string]string{"extra1": "val_extra1", "extra2": "val_extra2", "extra3": "val_extra3"}, Cost: 1.01,
|
||||
}
|
||||
csvCdrWriter.Write(ratedCdr)
|
||||
csvCdrWriter.WriteCdr(ratedCdr)
|
||||
csvCdrWriter.Close()
|
||||
expected := `b18944ef4dc618569f24c27b9872827a242bad0c,default,dsafdsaf,192.168.1.1,rated,*out,cgrates.org,call,1001,1001,1002,2013-11-07 08:42:25 +0000 UTC,2013-11-07 08:42:26 +0000 UTC,10,1.0100,val_extra3,"",val_extra1`
|
||||
result := strings.TrimSpace(writer.String())
|
||||
|
||||
@@ -19,11 +19,136 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
package cdre
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FixedWidthCdrWriter struct{}
|
||||
const (
|
||||
FILLER = "filler"
|
||||
CONSTANT = "constant"
|
||||
CDRFIELD = "cdrfield"
|
||||
CONCATENATED_CDRFIELD = "concatenated_cdrfield"
|
||||
)
|
||||
|
||||
func (fww *FixedWidthCdrWriter) Write(cdr *utils.StoredCdr) error {
|
||||
type FixedWidthCdrWriter struct {
|
||||
logDb engine.LogStorage // Used to extract cost_details if these are requested
|
||||
writer io.Writer
|
||||
exportTemplate *config.CgrXmlCdreFwCfg
|
||||
roundDecimals int
|
||||
header, content, trailer *bytes.Buffer
|
||||
firstCdrTime, lastCdrTime time.Time
|
||||
numberOfRecords int
|
||||
totalDuration time.Duration
|
||||
}
|
||||
|
||||
// Return Json marshaled callCost attached to
|
||||
// Keep it separately so we test only this part in local tests
|
||||
func (fww *FixedWidthCdrWriter) getCdrCostDetails(cgrId, runId string) (string, error) {
|
||||
cc, err := fww.logDb.GetCallCostLog(cgrId, "", runId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if cc == nil {
|
||||
return "", nil
|
||||
}
|
||||
ccJson, _ := json.Marshal(cc)
|
||||
return string(ccJson), nil
|
||||
}
|
||||
|
||||
// Extracts the value specified by cfgHdr out of cdr
|
||||
func (fww *FixedWidthCdrWriter) cdrFieldValue(cdr *utils.StoredCdr, cfgHdr, layout string) (string, error) {
|
||||
rsrField, err := utils.NewRSRField(cfgHdr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if rsrField == nil {
|
||||
return "", nil
|
||||
}
|
||||
var cdrVal string
|
||||
switch rsrField.Id {
|
||||
case utils.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
|
||||
}
|
||||
case utils.COST:
|
||||
cdrVal = cdr.FormatCost(fww.roundDecimals)
|
||||
case utils.SETUP_TIME:
|
||||
cdrVal = cdr.SetupTime.Format(layout)
|
||||
case utils.ANSWER_TIME: // Format time based on layout
|
||||
cdrVal = cdr.AnswerTime.Format(layout)
|
||||
default:
|
||||
cdrVal = cdr.ExportFieldValue(rsrField.Id)
|
||||
}
|
||||
return rsrField.ParseValue(cdrVal), nil
|
||||
}
|
||||
|
||||
// Writes the header into it's buffer
|
||||
func (fww *FixedWidthCdrWriter) ComposeHeader() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Writes the trailer into it's buffer
|
||||
func (fww *FixedWidthCdrWriter) ComposeTrailer() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write individual cdr into content buffer, build stats
|
||||
func (fww *FixedWidthCdrWriter) WriteCdr(cdr *utils.StoredCdr) error {
|
||||
var err error
|
||||
cdrRow := ""
|
||||
for _, cfgFld := range fww.exportTemplate.Content.Fields {
|
||||
var outVal string
|
||||
switch cfgFld.Type {
|
||||
case FILLER, CONSTANT:
|
||||
outVal = cfgFld.Value
|
||||
case CDRFIELD:
|
||||
outVal, err = fww.cdrFieldValue(cdr, cfgFld.Value, cfgFld.Layout)
|
||||
case CONCATENATED_CDRFIELD:
|
||||
for _, fld := range strings.Split(cfgFld.Value, ",") {
|
||||
if fldOut, err := fww.cdrFieldValue(cdr, fld, cfgFld.Layout); err != nil {
|
||||
break // The error will be reported bellow
|
||||
} else {
|
||||
outVal += fldOut
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<CdreFW> Cannot export CDR with cgrid: %s and runid: %s, error: %s", cdr.CgrId, cdr.MediationRunId))
|
||||
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))
|
||||
return err
|
||||
} else {
|
||||
cdrRow += fmtOut
|
||||
}
|
||||
}
|
||||
if len(cdrRow) == 0 { // No CDR data, most likely no configuration fields defined
|
||||
return nil
|
||||
}
|
||||
cdrRow += "\n" // Done with cdr, postpend new line char
|
||||
fww.content.WriteString(cdrRow)
|
||||
// Done with writing content, compute stats here
|
||||
if fww.firstCdrTime.IsZero() || cdr.SetupTime.Before(fww.firstCdrTime) {
|
||||
fww.firstCdrTime = cdr.SetupTime
|
||||
}
|
||||
if cdr.SetupTime.After(fww.lastCdrTime) {
|
||||
fww.lastCdrTime = cdr.SetupTime
|
||||
}
|
||||
fww.numberOfRecords += 1
|
||||
fww.totalDuration += cdr.Duration
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fww *FixedWidthCdrWriter) Close() {
|
||||
fww.ComposeHeader()
|
||||
fww.ComposeTrailer()
|
||||
for _, buf := range []*bytes.Buffer{fww.header, fww.content, fww.trailer} {
|
||||
fww.writer.Write(buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,68 +19,65 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
package cdre
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMaxLen(t *testing.T) {
|
||||
result, err := filterField("test", 4, false, false, false, false)
|
||||
expected := "test"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"test\" was \"%s\"", result)
|
||||
}
|
||||
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: "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"},
|
||||
&config.CgrXmlCfgCdrField{Name: "ChargeableTime", Type: CDRFIELD, Value: utils.DURATION, Width: 6, Strip: "right", Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "DataVolume", Type: FILLER, Width: 6, Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "TaxCode", Type: CONSTANT, Value: "1", Width: 1},
|
||||
&config.CgrXmlCfgCdrField{Name: "OperatorTAPCode", Type: CDRFIELD, Value: "opertapcode", Width: 2, Strip: "right", Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "ProductNumber", Type: CDRFIELD, Value: "productnumber", Width: 5, Strip: "right", Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "NetworkSubtype", Type: CONSTANT, Value: "3", Width: 1},
|
||||
&config.CgrXmlCfgCdrField{Name: "SessionID", Type: CDRFIELD, Value: utils.ACCID, Width: 16, Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "VolumeUP", Type: FILLER, Width: 8, Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "VolumeDown", Type: FILLER, Width: 8, Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "TerminatingOperator", Type: CONCATENATED_CDRFIELD, Value: "tapcode,operatorcode", Width: 5, Strip: "right", Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "EndCharge", Type: CDRFIELD, Value: utils.COST, Width: 9, Padding: "zeroleft"},
|
||||
&config.CgrXmlCfgCdrField{Name: "CallMaskingIndicator", Type: CDRFIELD, Value: "calledmask", Width: 1, Strip: "right", Padding: "right"},
|
||||
}
|
||||
|
||||
func TestRPadding(t *testing.T) {
|
||||
result, err := filterField("test", 8, false, false, false, false)
|
||||
expected := "test "
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLPadding(t *testing.T) {
|
||||
result, err := filterField("test", 8, false, false, true, false)
|
||||
expected := " test"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRStrip(t *testing.T) {
|
||||
result, err := filterField("test", 2, true, false, false, false)
|
||||
expected := "te"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLStrip(t *testing.T) {
|
||||
result, err := filterField("test", 2, true, true, false, false)
|
||||
expected := "st"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripNotAllowed(t *testing.T) {
|
||||
_, err := filterField("test", 2, false, false, false, false)
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLZeroPadding(t *testing.T) {
|
||||
result, err := filterField("12", 8, false, false, true, true)
|
||||
expected := "00000012"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRZeroPadding(t *testing.T) {
|
||||
result, err := filterField("12", 8, false, false, false, true)
|
||||
expected := "12 "
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
// 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}}
|
||||
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",
|
||||
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.34567,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
if err := fwWriter.WriteCdr(cdr); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
contentOut := fwWriter.content.String()
|
||||
if len(contentOut) != 145 {
|
||||
t.Error("Unexpected content length", len(contentOut))
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,52 +20,50 @@ package cdre
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Used as generic function logic for various fields
|
||||
|
||||
// Attributes
|
||||
// source - the base source
|
||||
// maxLen - the maximum field lenght
|
||||
// stripAllowed - whether we allow stripping of chars in case of source bigger than the maximum allowed
|
||||
// lStrip - if true, strip from beginning of the string
|
||||
// lPadding - if true, add chars at the beginning of the string
|
||||
// paddingChar - the character wich will be used to fill the existing
|
||||
func filterField(source string, maxLen int, stripAllowed, lStrip, lPadding, padWithZero bool) (string, error) {
|
||||
if len(source) == maxLen { // the source is exactly the maximum length
|
||||
// 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) {
|
||||
if len(source) == width { // the source is exactly the maximum length
|
||||
return source, nil
|
||||
}
|
||||
if len(source) > maxLen { //the source is bigger than allowed
|
||||
if !stripAllowed {
|
||||
return "", fmt.Errorf("source %s is bigger than the maximum allowed length %d", source, maxLen)
|
||||
if len(source) > width { //the source is bigger than allowed
|
||||
if len(strip) == 0 {
|
||||
return "", fmt.Errorf("Source %s is bigger than the width %d, no strip defied", source, width)
|
||||
}
|
||||
if !lStrip {
|
||||
return source[:maxLen], nil
|
||||
} else {
|
||||
diffIndx := len(source) - maxLen
|
||||
if strip == "right" {
|
||||
return source[:width], nil
|
||||
} else if strip == "xright" {
|
||||
return source[:width-1] + "x", nil // Suffix with x to mark prefix
|
||||
} else if strip == "left" {
|
||||
diffIndx := len(source) - width
|
||||
return source[diffIndx:], nil
|
||||
} else if strip == "xleft" { // Prefix one x to mark stripping
|
||||
diffIndx := len(source) - width
|
||||
return "x" + source[diffIndx+1:], nil
|
||||
}
|
||||
} else { //the source is smaller as the maximum allowed
|
||||
paddingString := "%"
|
||||
if padWithZero {
|
||||
paddingString += "0" // it will not work for rPadding but this is not needed
|
||||
if len(padding) == 0 {
|
||||
return "", fmt.Errorf("Source %s is smaller than the width %d, no padding defined", source, width)
|
||||
}
|
||||
if !lPadding {
|
||||
paddingString += "-"
|
||||
var paddingFmt string
|
||||
switch padding {
|
||||
case "right":
|
||||
paddingFmt = fmt.Sprintf("%%-%ds", width)
|
||||
case "left":
|
||||
paddingFmt = fmt.Sprintf("%%%ds", width)
|
||||
case "zeroleft":
|
||||
paddingFmt = fmt.Sprintf("%%0%ds", width)
|
||||
}
|
||||
if len(paddingFmt) != 0 {
|
||||
return fmt.Sprintf(paddingFmt, source), nil
|
||||
}
|
||||
paddingString += strconv.Itoa(maxLen) + "s"
|
||||
return fmt.Sprintf(paddingString, source), nil
|
||||
}
|
||||
return source, nil
|
||||
}
|
||||
|
||||
/*
|
||||
type XmlCdreConfig struct {
|
||||
XMLName xml.Name `xml:"configuration"`
|
||||
Name string `xml:"name,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
Header XMLFWCdrHeader `xml:"header"`
|
||||
Content XMLFWCdrContent `xml:"content"`
|
||||
Footer XMLFWCdrFooter `xml:"footer"`
|
||||
}
|
||||
*/
|
||||
|
||||
109
cdre/libfixedwidth_test.go
Normal file
109
cdre/libfixedwidth_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package cdre
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMaxLen(t *testing.T) {
|
||||
result, err := FmtFieldWidth("test", 4, "", "")
|
||||
expected := "test"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"test\" was \"%s\"", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRPadding(t *testing.T) {
|
||||
result, err := FmtFieldWidth("test", 8, "", "right")
|
||||
expected := "test "
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingFiller(t *testing.T) {
|
||||
result, err := FmtFieldWidth("", 8, "", "right")
|
||||
expected := " "
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLPadding(t *testing.T) {
|
||||
result, err := FmtFieldWidth("test", 8, "", "left")
|
||||
expected := " test"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroLPadding(t *testing.T) {
|
||||
result, err := FmtFieldWidth("test", 8, "", "zeroleft")
|
||||
expected := "0000test"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRStrip(t *testing.T) {
|
||||
result, err := FmtFieldWidth("test", 2, "right", "")
|
||||
expected := "te"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXRStrip(t *testing.T) {
|
||||
result, err := FmtFieldWidth("test", 3, "xright", "")
|
||||
expected := "tex"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLStrip(t *testing.T) {
|
||||
result, err := FmtFieldWidth("test", 2, "left", "")
|
||||
expected := "st"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXLStrip(t *testing.T) {
|
||||
result, err := FmtFieldWidth("test", 3, "xleft", "")
|
||||
expected := "xst"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripNotAllowed(t *testing.T) {
|
||||
_, err := FmtFieldWidth("test", 3, "", "")
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingNotAllowed(t *testing.T) {
|
||||
_, err := FmtFieldWidth("test", 5, "", "")
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ package config
|
||||
import (
|
||||
"code.google.com/p/goconf/conf"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
@@ -46,22 +45,6 @@ func ConfigSlice(c *conf.ConfigFile, section, valName string) ([]string, error)
|
||||
return cfgValStrs, nil
|
||||
}
|
||||
|
||||
// Used to parse extra fields definition
|
||||
func parseSearchReplaceFromFieldRule(fieldRule string) (string, *utils.ReSearchReplace, error) {
|
||||
// String rule expected in the form ~hdr_name:s/match_rule/replace_rule/
|
||||
getRuleRgxp := regexp.MustCompile(`~(\w+):s\/(.+[^\\])\/(.+[^\\])\/`) // Make sure the separator / is not escaped in the rule
|
||||
allMatches := getRuleRgxp.FindStringSubmatch(fieldRule)
|
||||
if len(allMatches) != 4 { // Second and third groups are of interest to us
|
||||
return "", nil, errors.New("Invalid Search&Replace field rule.")
|
||||
}
|
||||
fieldName := allMatches[1]
|
||||
searchRegexp, err := regexp.Compile(allMatches[2])
|
||||
if err != nil {
|
||||
return fieldName, nil, err
|
||||
}
|
||||
return fieldName, &utils.ReSearchReplace{searchRegexp, allMatches[3]}, nil
|
||||
}
|
||||
|
||||
func ParseRSRFields(configVal string) ([]*utils.RSRField, error) {
|
||||
cfgValStrs := strings.Split(configVal, string(utils.CSV_SEP))
|
||||
if len(cfgValStrs) == 1 && cfgValStrs[0] == "" { // Prevents returning iterable with empty value
|
||||
@@ -73,14 +56,10 @@ func ParseRSRFields(configVal string) ([]*utils.RSRField, error) {
|
||||
return nil, errors.New("Empty values in config slice")
|
||||
|
||||
}
|
||||
if !strings.HasPrefix(cfgValStr, utils.REGEXP_SEP) {
|
||||
rsrFields[idx] = &utils.RSRField{Id: cfgValStr}
|
||||
continue // Nothing to be done for fields without ReSearchReplace rules
|
||||
}
|
||||
if fldId, reSrcRepl, err := parseSearchReplaceFromFieldRule(cfgValStr); err != nil {
|
||||
if rsrField, err := utils.NewRSRField(cfgValStr); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
rsrFields[idx] = &utils.RSRField{fldId, reSrcRepl}
|
||||
rsrFields[idx] = rsrField
|
||||
}
|
||||
}
|
||||
return rsrFields, nil
|
||||
|
||||
@@ -26,35 +26,6 @@ import (
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
func TestParseSearchReplaceFromFieldRule(t *testing.T) {
|
||||
// Normal case
|
||||
fieldRule := `~sip_redirected_to:s/sip:\+49(\d+)@/0$1/`
|
||||
field, regSrchRplc, err := parseSearchReplaceFromFieldRule(fieldRule)
|
||||
if len(field) == 0 || regSrchRplc == nil || err != nil {
|
||||
t.Error("Failed parsing the field rule")
|
||||
} else if !reflect.DeepEqual(regSrchRplc, &utils.ReSearchReplace{regexp.MustCompile(`sip:\+49(\d+)@`), "0$1"}) {
|
||||
t.Error("Unexpected ReSearchReplace parsed")
|
||||
}
|
||||
// Missing ~ prefix
|
||||
fieldRule = `sip_redirected_to:s/sip:\+49(\d+)@/0$1/`
|
||||
if _, _, err := parseSearchReplaceFromFieldRule(fieldRule); err == nil {
|
||||
t.Error("Parse error, field rule does not start with ~")
|
||||
}
|
||||
// Separator escaped
|
||||
fieldRule = `~sip_redirected_to:s\/sip:\+49(\d+)@/0$1/`
|
||||
if _, _, err := parseSearchReplaceFromFieldRule(fieldRule); err == nil {
|
||||
t.Error("Parse error, field rule does not contain correct number of separators")
|
||||
}
|
||||
// One extra separator but escaped
|
||||
fieldRule = `~sip_redirected_to:s/sip:\+49(\d+)\/@/0$1/`
|
||||
field, regSrchRplc, err = parseSearchReplaceFromFieldRule(fieldRule)
|
||||
if len(field) == 0 || regSrchRplc == nil || err != nil {
|
||||
t.Error("Failed parsing the field rule")
|
||||
} else if !reflect.DeepEqual(regSrchRplc, &utils.ReSearchReplace{regexp.MustCompile(`sip:\+49(\d+)\/@`), "0$1"}) {
|
||||
t.Error("Unexpected ReSearchReplace parsed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRSRFields(t *testing.T) {
|
||||
fields := `host,~sip_redirected_to:s/sip:\+49(\d+)@/0$1/,destination`
|
||||
expectParsedFields := []*utils.RSRField{&utils.RSRField{Id: "host"},
|
||||
|
||||
@@ -79,15 +79,14 @@ 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 string `xml:"width,attr"`
|
||||
Strip string `xml:"strip,attr"`
|
||||
Padding string `xml:"padding,attr"`
|
||||
PaddingChar string `xml:"padding_char,attr"`
|
||||
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
|
||||
}
|
||||
|
||||
// Avoid building from raw config string always, so build cache here
|
||||
|
||||
@@ -34,7 +34,7 @@ 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="left" padding_char="0" width="5"/>
|
||||
<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="FileCreationfTime" type="metatag" value="time_now" layout="020106150400" width="12"/>
|
||||
<field name="FileSpecificationVersion" type="constant" value="01" width="2"/>
|
||||
@@ -45,7 +45,7 @@ func TestParseXmlConfig(t *testing.T) {
|
||||
<fields>
|
||||
<field name="RecordType" type="constant" value="20" width="2"/>
|
||||
<field name="SIPTrunkID" type="cdrfield" value="cgrid" width="12"/>
|
||||
<field name="ConnectionNumber" type="cdrfield" value="subject" strip="left" padding="left" padding_char="0" width="5"/>
|
||||
<field name="ConnectionNumber" type="cdrfield" value="subject" strip="left" padding="left" width="5"/>
|
||||
<field name="ANumber" type="cdrfield" value="cli" strip="xright" width="15"/>
|
||||
<field name="CalledNumber" type="cdrfield" value="destination" strip="xright" width="24"/>
|
||||
<field name="ServiceType" type="constant" value="02" width="2"/>
|
||||
@@ -61,7 +61,7 @@ func TestParseXmlConfig(t *testing.T) {
|
||||
<field name="VolumeUP" type="filler" width="8"/>
|
||||
<field name="VolumeDown" type="filler" width="8"/>
|
||||
<field name="TerminatingOperator" type="concatenated_cdrfield" value="tapcode,operatorcode" width="5"/>
|
||||
<field name="EndCharge" type="metatag" value="total_cost" padding="left" padding_char="0" width="9"/>
|
||||
<field name="EndCharge" type="cdrfield" value="cost" padding="zeroleft" width="9"/>
|
||||
<field name="CallMaskingIndicator" type="cdrfield" value="calledmask" width="1"/>
|
||||
</fields>
|
||||
</content>
|
||||
@@ -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="left" padding_char="0" width="5"/>
|
||||
<field name="TotalNrRecords" type="metatag" value="nr_cdrs" padding="left" padding_char="0" width="6"/>
|
||||
<field name="TotalDurRecords" type="metatag" value="dur_cdrs" padding="left" padding_char="0" width="8"/>
|
||||
<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="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,6 +94,7 @@ const (
|
||||
FIXED_WIDTH = "fixed_width"
|
||||
XML_PROFILE_PREFIX = "*xml:"
|
||||
CDRE = "cdre"
|
||||
COST_DETAILS = "cost_details"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -18,6 +18,41 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseSearchReplaceFromFieldRule(fieldRule string) (string, *ReSearchReplace, error) {
|
||||
// String rule expected in the form ~hdr_name:s/match_rule/replace_rule/
|
||||
getRuleRgxp := regexp.MustCompile(`~(\w+):s\/(.+[^\\])\/(.+[^\\])\/`) // Make sure the separator / is not escaped in the rule
|
||||
allMatches := getRuleRgxp.FindStringSubmatch(fieldRule)
|
||||
if len(allMatches) != 4 { // Second and third groups are of interest to us
|
||||
return "", nil, errors.New("Invalid Search&Replace field rule.")
|
||||
}
|
||||
fieldName := allMatches[1]
|
||||
searchRegexp, err := regexp.Compile(allMatches[2])
|
||||
if err != nil {
|
||||
return fieldName, nil, err
|
||||
}
|
||||
return fieldName, &ReSearchReplace{searchRegexp, allMatches[3]}, nil
|
||||
}
|
||||
|
||||
func NewRSRField(fldStr string) (*RSRField, error) {
|
||||
if len(fldStr) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if !strings.HasPrefix(fldStr, REGEXP_SEP) {
|
||||
return &RSRField{Id: fldStr}, nil
|
||||
}
|
||||
if fldId, reSrcRepl, err := ParseSearchReplaceFromFieldRule(fldStr); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &RSRField{fldId, reSrcRepl}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type RSRField struct {
|
||||
Id string // Identifier
|
||||
RSRule *ReSearchReplace // Rule to use when processing field value
|
||||
|
||||
74
utils/rsrfield_test.go
Normal file
74
utils/rsrfield_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseSearchReplaceFromFieldRule(t *testing.T) {
|
||||
// Normal case
|
||||
fieldRule := `~sip_redirected_to:s/sip:\+49(\d+)@/0$1/`
|
||||
field, regSrchRplc, err := ParseSearchReplaceFromFieldRule(fieldRule)
|
||||
if len(field) == 0 || regSrchRplc == nil || err != nil {
|
||||
t.Error("Failed parsing the field rule")
|
||||
} else if !reflect.DeepEqual(regSrchRplc, &ReSearchReplace{regexp.MustCompile(`sip:\+49(\d+)@`), "0$1"}) {
|
||||
t.Error("Unexpected ReSearchReplace parsed")
|
||||
}
|
||||
// Missing ~ prefix
|
||||
fieldRule = `sip_redirected_to:s/sip:\+49(\d+)@/0$1/`
|
||||
if _, _, err := ParseSearchReplaceFromFieldRule(fieldRule); err == nil {
|
||||
t.Error("Parse error, field rule does not start with ~")
|
||||
}
|
||||
// Separator escaped
|
||||
fieldRule = `~sip_redirected_to:s\/sip:\+49(\d+)@/0$1/`
|
||||
if _, _, err := ParseSearchReplaceFromFieldRule(fieldRule); err == nil {
|
||||
t.Error("Parse error, field rule does not contain correct number of separators")
|
||||
}
|
||||
// One extra separator but escaped
|
||||
fieldRule = `~sip_redirected_to:s/sip:\+49(\d+)\/@/0$1/`
|
||||
field, regSrchRplc, err = ParseSearchReplaceFromFieldRule(fieldRule)
|
||||
if len(field) == 0 || regSrchRplc == nil || err != nil {
|
||||
t.Error("Failed parsing the field rule")
|
||||
} else if !reflect.DeepEqual(regSrchRplc, &ReSearchReplace{regexp.MustCompile(`sip:\+49(\d+)\/@`), "0$1"}) {
|
||||
t.Error("Unexpected ReSearchReplace parsed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRSRField(t *testing.T) {
|
||||
expectRSRField := &RSRField{Id: "sip_redirected_to", RSRule: &ReSearchReplace{regexp.MustCompile(`sip:\+49(\d+)@`), "0$1"}}
|
||||
if rsrField, err := NewRSRField(`~sip_redirected_to:s/sip:\+49(\d+)@/0$1/`); err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(rsrField, expectRSRField) {
|
||||
t.Errorf("Unexpected RSRField received: %v", rsrField)
|
||||
}
|
||||
expectRSRField = &RSRField{Id: "sip_redirected_to"}
|
||||
if rsrField, err := NewRSRField(`sip_redirected_to`); err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(rsrField, expectRSRField) {
|
||||
t.Errorf("Unexpected RSRField received: %v", rsrField)
|
||||
}
|
||||
if rsrField, err := NewRSRField(""); err != nil {
|
||||
t.Error(err)
|
||||
} else if rsrField != nil {
|
||||
t.Errorf("Unexpected RSRField received: %v", rsrField)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user