Basic implementation of fixed_width cdr export content

This commit is contained in:
DanB
2014-03-23 21:17:48 +01:00
parent 15f2a2cd82
commit 2bc4e3fb7e
15 changed files with 453 additions and 165 deletions

View File

@@ -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
}

View File

@@ -23,6 +23,6 @@ import (
)
type CdrWriter interface {
Write(cdr *utils.StoredCdr) string
WriteCdr(cdr *utils.StoredCdr) string
Close()
}

View File

@@ -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)

View File

@@ -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())

View File

@@ -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())
}
}

View File

@@ -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)
}
}

View File

@@ -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
View 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")
}
}

View File

@@ -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

View File

@@ -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"},

View File

@@ -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

View File

@@ -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"/>

View File

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

View File

@@ -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
View 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)
}
}