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