diff --git a/apier/cdre.go b/apier/cdre.go
index 36ac6898b..110e04bc0 100644
--- a/apier/cdre.go
+++ b/apier/cdre.go
@@ -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
}
diff --git a/cdre/cdrexporter.go b/cdre/cdrexporter.go
index 19431f9eb..1d35a1a4d 100644
--- a/cdre/cdrexporter.go
+++ b/cdre/cdrexporter.go
@@ -23,6 +23,6 @@ import (
)
type CdrWriter interface {
- Write(cdr *utils.StoredCdr) string
+ WriteCdr(cdr *utils.StoredCdr) string
Close()
}
diff --git a/cdre/csv.go b/cdre/csv.go
index 3c7aecc47..531b087d3 100644
--- a/cdre/csv.go
+++ b/cdre/csv.go
@@ -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)
diff --git a/cdre/csv_test.go b/cdre/csv_test.go
index 6d664daa9..aa724a648 100644
--- a/cdre/csv_test.go
+++ b/cdre/csv_test.go
@@ -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())
diff --git a/cdre/fixedwidth.go b/cdre/fixedwidth.go
index 5f4172653..aec69d750 100644
--- a/cdre/fixedwidth.go
+++ b/cdre/fixedwidth.go
@@ -19,11 +19,136 @@ along with this program. If not, see
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(" 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(" 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())
+ }
+}
diff --git a/cdre/fixedwidth_test.go b/cdre/fixedwidth_test.go
index b9507a456..7533729b2 100644
--- a/cdre/fixedwidth_test.go
+++ b/cdre/fixedwidth_test.go
@@ -19,68 +19,65 @@ along with this program. If not, see
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)
}
}
diff --git a/cdre/libfixedwidth.go b/cdre/libfixedwidth.go
index e698d38f7..5dff7b191 100644
--- a/cdre/libfixedwidth.go
+++ b/cdre/libfixedwidth.go
@@ -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"`
-}
-*/
diff --git a/cdre/libfixedwidth_test.go b/cdre/libfixedwidth_test.go
new file mode 100644
index 000000000..ce1a136df
--- /dev/null
+++ b/cdre/libfixedwidth_test.go
@@ -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
+*/
+
+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")
+ }
+}
diff --git a/config/helpers.go b/config/helpers.go
index 0e6f97085..b43d0eabc 100644
--- a/config/helpers.go
+++ b/config/helpers.go
@@ -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
diff --git a/config/helpers_test.go b/config/helpers_test.go
index 83d8b784f..0c9081b4e 100644
--- a/config/helpers_test.go
+++ b/config/helpers_test.go
@@ -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"},
diff --git a/config/xmlconfig.go b/config/xmlconfig.go
index a0b54dd1b..ebe96f285 100644
--- a/config/xmlconfig.go
+++ b/config/xmlconfig.go
@@ -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
diff --git a/config/xmlconfig_test.go b/config/xmlconfig_test.go
index c22ef6af6..3c488eca3 100644
--- a/config/xmlconfig_test.go
+++ b/config/xmlconfig_test.go
@@ -34,7 +34,7 @@ func TestParseXmlConfig(t *testing.T) {
-
+
@@ -45,7 +45,7 @@ func TestParseXmlConfig(t *testing.T) {
-
+
@@ -61,7 +61,7 @@ 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 2f572e37d..139be2182 100644
--- a/utils/consts.go
+++ b/utils/consts.go
@@ -94,6 +94,7 @@ const (
FIXED_WIDTH = "fixed_width"
XML_PROFILE_PREFIX = "*xml:"
CDRE = "cdre"
+ COST_DETAILS = "cost_details"
)
var (
diff --git a/utils/rsrfield.go b/utils/rsrfield.go
index 4c5c1db22..baa28efe1 100644
--- a/utils/rsrfield.go
+++ b/utils/rsrfield.go
@@ -18,6 +18,41 @@ along with this program. If not, see
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
diff --git a/utils/rsrfield_test.go b/utils/rsrfield_test.go
new file mode 100644
index 000000000..b556887f0
--- /dev/null
+++ b/utils/rsrfield_test.go
@@ -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
+*/
+
+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)
+ }
+}