Make Attribute Profiles storable in MySQL and Postgres

This commit is contained in:
arberkatellari
2025-11-05 10:56:57 +02:00
committed by Dan Christian Bogos
parent eda80242eb
commit 38a02535f0
17 changed files with 600 additions and 54 deletions

View File

@@ -109,11 +109,11 @@ func TestAttributesIT(t *testing.T) {
case utils.MetaMongo:
attrConfigDIR = "attributes_mongo"
case utils.MetaRedis:
t.SkipNow()
attrConfigDIR = "attributes_redis"
case utils.MetaMySQL:
attrConfigDIR = "attributes_mysql"
case utils.MetaPostgres:
t.SkipNow()
attrConfigDIR = "attributes_postgres"
default:
t.Fatal("Unknown Database type")
}

View File

@@ -132,6 +132,7 @@ const CGRATES_CFG_JSON = `
"*action_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"},
"*versions": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"},
"*charger_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"},
"*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"},
// compatible db types: <*internal|*redis|*mongo>
"*actions": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"},
@@ -143,7 +144,6 @@ const CGRATES_CFG_JSON = `
"*thresholds": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"},
"*filters": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"},
"*route_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"},
"*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"},
"*rate_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"},
"*load_ids": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"},
"*resource_filter_indexes" : {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate": false, "dbConn": "*default"},

View File

@@ -1026,6 +1026,14 @@ func (cfg *CGRConfig) checkConfigSanity() error {
// DataDB sanity checks
hasOneInternalDB := false // used to reutrn error in case more then 1 internaldb is found
allDBsItems := []string{
utils.MetaAccounts,
utils.MetaIPProfiles,
utils.MetaIPAllocations,
utils.MetaActionProfiles,
utils.MetaChargerProfiles,
utils.MetaAttributeProfiles,
}
for _, dbcfg := range cfg.dbCfg.DBConns {
if dbcfg.Type == utils.MetaInternal {
if hasOneInternalDB {
@@ -1060,9 +1068,7 @@ func (cfg *CGRConfig) checkConfigSanity() error {
dataDBTypes := []string{utils.MetaInternal, utils.MetaRedis, utils.MetaMongo,
utils.Internal, utils.Redis, utils.Mongo}
if item != utils.MetaAccounts && item != utils.MetaIPProfiles &&
item != utils.MetaIPAllocations && item != utils.MetaActionProfiles &&
item != utils.MetaChargerProfiles {
if !slices.Contains(allDBsItems, item) {
if item == utils.MetaCDRs {
if !slices.Contains(storDBTypes, cfg.dbCfg.DBConns[val.DBConn].Type) {
return fmt.Errorf("<%s> db item can only be of types <%v>, got <%s>", item,

View File

@@ -36,7 +36,8 @@
}
},
"items": {
"*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}
"*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"},
"*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}
}
},
"loaders": [

View File

@@ -0,0 +1,148 @@
{
// CGRateS Configuration file
//
"general": {
"reply_timeout": "50s",
},
"logger": {
"level": 7
},
"listen": {
"rpc_json": ":2012",
"rpc_gob": ":2013",
"http": ":2080",
},
"db": {
"db_conns": {
"*default": {
"db_type": "redis",
"db_host": "127.0.0.1",
"db_port": 6379,
"db_name": "10",
"db_user": "cgrates"
},
"StorDB": {
"db_type": "postgres",
"db_host": "127.0.0.1",
"db_port": 5432,
"db_name": "cgrates",
"db_user": "cgrates",
"db_password": "CGRateS.org"
}
},
"items": {
"*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"},
"*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}
}
},
"loaders": [
{
"id": "*default",
"enabled": true,
"tenant": "cgrates.org",
"lockfile_path": ".cgr.lck",
"tp_in_dir": "/usr/share/cgrates/tariffplans/testit",
"tp_out_dir": "",
},
],
"cdrs": {
"enabled": true,
"chargers_conns":["*internal"],
},
"attributes": {
"enabled": true,
"stats_conns": ["*localhost"],
"resources_conns": ["*localhost"],
"accounts_conns": ["*localhost"]
},
"chargers": {
"enabled": true,
"attributes_conns": ["*internal"],
},
"resources": {
"enabled": true,
"store_interval": "1s",
"thresholds_conns": ["*internal"]
},
"stats": {
"enabled": true,
"store_interval": "1s",
"thresholds_conns": ["*internal"],
},
"thresholds": {
"enabled": true,
"store_interval": "1s",
},
"routes": {
"enabled": true,
"prefix_indexed_fields":["*req.Destination"],
"stats_conns": ["*internal"],
"resources_conns": ["*internal"],
"rates_conns": ["*internal"],
},
"sessions": {
"enabled": true,
"routes_conns": ["*internal"],
"resources_conns": ["*internal"],
"attributes_conns": ["*internal"],
"rates_conns": ["*internal"],
"cdrs_conns": ["*internal"],
"chargers_conns": ["*internal"],
},
"migrator":{
"users_filters":["Account"],
},
"admins": {
"enabled": true,
},
"rates": {
"enabled": true
},
"actions": {
"enabled": true,
"accounts_conns": ["*localhost"]
},
"accounts": {
"enabled": true
},
"filters": {
"stats_conns": ["*internal"],
"resources_conns": ["*internal"],
"accounts_conns": ["*internal"],
},
}

View File

@@ -0,0 +1,147 @@
{
// CGRateS Configuration file
//
"general": {
"reply_timeout": "50s",
},
"logger": {
"level": 7
},
"listen": {
"rpc_json": ":2012",
"rpc_gob": ":2013",
"http": ":2080",
},
"db": {
"db_conns": {
"*default": {
"db_type": "redis",
"db_host": "127.0.0.1",
"db_port": 6379,
"db_name": "10",
"db_user": "cgrates"
},
"StorDB": {
"db_type": "mysql",
"db_host": "127.0.0.1",
"db_port": 3306,
"db_name": "cgrates",
"db_user": "cgrates",
"db_password": "CGRateS.org"
}
},
"items": {
"*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}
}
},
"loaders": [
{
"id": "*default",
"enabled": true,
"tenant": "cgrates.org",
"lockfile_path": ".cgr.lck",
"tp_in_dir": "/usr/share/cgrates/tariffplans/testit",
"tp_out_dir": "",
},
],
"cdrs": {
"enabled": true,
"chargers_conns":["*internal"],
},
"attributes": {
"enabled": true,
"stats_conns": ["*localhost"],
"resources_conns": ["*localhost"],
"accounts_conns": ["*localhost"]
},
"chargers": {
"enabled": true,
"attributes_conns": ["*internal"],
},
"resources": {
"enabled": true,
"store_interval": "1s",
"thresholds_conns": ["*internal"]
},
"stats": {
"enabled": true,
"store_interval": "1s",
"thresholds_conns": ["*internal"],
},
"thresholds": {
"enabled": true,
"store_interval": "1s",
},
"routes": {
"enabled": true,
"prefix_indexed_fields":["*req.Destination"],
"stats_conns": ["*internal"],
"resources_conns": ["*internal"],
"rates_conns": ["*internal"],
},
"sessions": {
"enabled": true,
"routes_conns": ["*internal"],
"resources_conns": ["*internal"],
"attributes_conns": ["*internal"],
"rates_conns": ["*internal"],
"cdrs_conns": ["*internal"],
"chargers_conns": ["*internal"],
},
"migrator":{
"users_filters":["Account"],
},
"admins": {
"enabled": true,
},
"rates": {
"enabled": true
},
"actions": {
"enabled": true,
"accounts_conns": ["*localhost"]
},
"accounts": {
"enabled": true
},
"filters": {
"stats_conns": ["*internal"],
"resources_conns": ["*internal"],
"accounts_conns": ["*internal"],
},
}

View File

@@ -25,12 +25,14 @@
}
},
"items": {
"*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}
"*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"},
"*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false,
"replicate":false, "dbConn": "StorDB"}
}
},
"attributes": {
"enabled": true,
"enabled": true,
"prefix_indexed_fields": ["*req.Destination"],
"exists_indexed_fields": ["*opts.*usage"],
"notexists_indexed_fields": ["*req.ToR"],

View File

@@ -0,0 +1,45 @@
{
// CGRateS Configuration file
// will be used in apis/attributes_it_test.go
"logger": {
"level": 7
},
"db": {
"db_conns": {
"*default": {
"db_type": "redis",
"db_host": "127.0.0.1",
"db_port": 6379,
"db_name": "10",
"db_user": "cgrates"
},
"StorDB": { // The id of the DB connection
"db_type": "postgres", // db type: <internal|redis|mysql|mongo|postgres>
"db_host": "127.0.0.1",
"db_port": 5432, // db port to reach the database
"db_name": "cgrates", // the host to connect to
"db_user": "cgrates",
"db_password": "CGRateS.org" // password to use when connecting to the database
}
},
"items": {
"*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"},
"*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false,
"replicate":false, "dbConn": "StorDB"}
}
},
"attributes": {
"enabled": true,
"prefix_indexed_fields": ["*req.Destination"],
"exists_indexed_fields": ["*opts.*usage"],
"notexists_indexed_fields": ["*req.ToR"],
},
"admins": {
"enabled": true,
}
}

View File

@@ -0,0 +1,43 @@
{
// CGRateS Configuration file
// will be used in apis/attributes_it_test.go
"logger": {
"level": 7
},
"db": {
"db_conns": {
"*default": {
"db_type": "redis",
"db_host": "127.0.0.1",
"db_port": 6379,
"db_name": "10",
"db_user": "cgrates"
},
"StorDB": {
"db_type": "mysql",
"db_host": "127.0.0.1",
"db_port": 3306,
"db_name": "cgrates",
"db_user": "cgrates",
"db_password": "CGRateS.org"
}
},
"items": {
"*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}
}
},
"attributes": {
"enabled": true,
"prefix_indexed_fields": ["*req.Destination"],
"exists_indexed_fields": ["*opts.*usage"],
"notexists_indexed_fields": ["*req.ToR"],
},
"admins": {
"enabled": true,
}
}

View File

@@ -55,4 +55,15 @@ CREATE TABLE charger_profiles (
PRIMARY KEY (`pk`),
UNIQUE KEY unique_tenant_id (`tenant`, `id`)
);
CREATE UNIQUE INDEX charger_profiles_idx ON charger_profiles (`id`);
CREATE UNIQUE INDEX charger_profiles_idx ON charger_profiles (`id`);
DROP TABLE IF EXISTS attribute_profiles;
CREATE TABLE attribute_profiles (
`pk` int(11) NOT NULL AUTO_INCREMENT,
`tenant` VARCHAR(40) NOT NULL,
`id` VARCHAR(64) NOT NULL,
`attribute_profile` JSON NOT NULL,
PRIMARY KEY (`pk`),
UNIQUE KEY unique_tenant_id (`tenant`, `id`)
);
CREATE UNIQUE INDEX attribute_profiles_idx ON attribute_profiles (`id`);

View File

@@ -54,3 +54,14 @@ CREATE TABLE charger_profiles (
UNIQUE (tenant, id)
);
CREATE UNIQUE INDEX charger_profiles_idx ON charger_profiles ("id");
DROP TABLE IF EXISTS attribute_profiles;
CREATE TABLE attribute_profiles (
pk SERIAL PRIMARY KEY,
tenant VARCHAR(40) NOT NULL,
id VARCHAR(64) NOT NULL,
attribute_profile JSONB NOT NULL,
UNIQUE (tenant, id)
);
CREATE UNIQUE INDEX attribute_profiles_idx ON attribute_profiles ("id");

View File

@@ -424,7 +424,7 @@ type ActionProfileJSONMdl struct {
}
func (ActionProfileJSONMdl) TableName() string {
return utils.TBLActionProfilesJSON
return utils.TBLActionProfiles
}
type ChargerProfileMdl struct {
@@ -435,5 +435,16 @@ type ChargerProfileMdl struct {
}
func (ChargerProfileMdl) TableName() string {
return utils.TBLChargerProfilesJSON
return utils.TBLChargerProfiles
}
type AttributeProfileMdl struct {
PK uint `gorm:"primary_key"`
Tenant string `index:"0" re:".*"`
ID string `index:"1" re:".*"`
AttributeProfile utils.JSONB `gorm:"type:jsonb" index:"2" re:".*"`
}
func (AttributeProfileMdl) TableName() string {
return utils.TBLAttributeProfiles
}

View File

@@ -105,9 +105,11 @@ func (sqls *SQLStorage) GetKeysForPrefix(ctx *context.Context, prefix string) (k
case utils.IPAllocationsPrefix:
keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLIPAllocations, tntID)
case utils.ActionProfilePrefix:
keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLActionProfilesJSON, tntID)
keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLActionProfiles, tntID)
case utils.ChargerProfilePrefix:
keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLChargerProfilesJSON, tntID)
keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLChargerProfiles, tntID)
case utils.AttributeProfilePrefix:
keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLAttributeProfiles, tntID)
default:
err = fmt.Errorf("unsupported prefix in GetKeysForPrefix: %q", prefix)
}
@@ -616,6 +618,51 @@ func (sqls *SQLStorage) RemoveChargerProfileDrv(_ *context.Context, tenant, id s
return nil
}
func (sqls *SQLStorage) GetAttributeProfileDrv(ctx *context.Context, tenant, id string) (ap *utils.AttributeProfile, err error) {
var result []*AttributeProfileMdl
if err := sqls.db.Model(&AttributeProfileMdl{}).Where(&AttributeProfileMdl{Tenant: tenant,
ID: id}).Find(&result).Error; err != nil {
return nil, err
}
if len(result) == 0 {
return nil, utils.ErrNotFound
}
return utils.MapStringInterfaceToAttributeProfile(result[0].AttributeProfile)
}
func (sqls *SQLStorage) SetAttributeProfileDrv(ctx *context.Context, ap *utils.AttributeProfile) (err error) {
tx := sqls.db.Begin()
mdl := &AttributeProfileMdl{
Tenant: ap.Tenant,
ID: ap.ID,
AttributeProfile: ap.AsMapStringInterface(),
}
if err = tx.Model(&AttributeProfileMdl{}).Where(
AttributeProfileMdl{Tenant: mdl.Tenant, ID: mdl.ID}).Delete(
AttributeProfileMdl{}).Error; err != nil {
tx.Rollback()
return
}
if err = tx.Save(mdl).Error; err != nil {
tx.Rollback()
return
}
tx.Commit()
return
}
func (sqls *SQLStorage) RemoveAttributeProfileDrv(ctx *context.Context, tenant, id string) (err error) {
tx := sqls.db.Begin()
if err = tx.Model(&AttributeProfileMdl{}).Where(&AttributeProfileMdl{Tenant: tenant, ID: id}).
Delete(&AttributeProfileMdl{}).Error; err != nil {
tx.Rollback()
return err
}
tx.Commit()
return
}
// AddLoadHistory DataDB method not implemented yet
func (sqls *SQLStorage) AddLoadHistory(ldInst *utils.LoadInstance,
loadHistSize int, transactionID string) error {
@@ -838,21 +885,6 @@ func (sqls *SQLStorage) RemoveRouteProfileDrv(ctx *context.Context, tenant, id s
return utils.ErrNotImplemented
}
// DataDB method not implemented yet
func (sqls *SQLStorage) GetAttributeProfileDrv(ctx *context.Context, tenant, id string) (r *utils.AttributeProfile, err error) {
return nil, utils.ErrNotImplemented
}
// DataDB method not implemented yet
func (sqls *SQLStorage) SetAttributeProfileDrv(ctx *context.Context, r *utils.AttributeProfile) (err error) {
return utils.ErrNotImplemented
}
// DataDB method not implemented yet
func (sqls *SQLStorage) RemoveAttributeProfileDrv(ctx *context.Context, tenant, id string) (err error) {
return utils.ErrNotImplemented
}
// GetStorageType returns the storage type that is being used
func (sqls *SQLStorage) GetStorageType() string {
return utils.MetaMySQL

View File

@@ -67,13 +67,13 @@ func TestAttributeSIT(t *testing.T) {
case utils.MetaInternal:
alsPrfConfigDIR = "attr_test_internal"
case utils.MetaRedis:
t.SkipNow()
alsPrfConfigDIR = "attr_test_redis"
case utils.MetaMySQL:
alsPrfConfigDIR = "attr_test_mysql"
case utils.MetaMongo:
alsPrfConfigDIR = "attr_test_mongo"
case utils.MetaPostgres:
t.SkipNow()
alsPrfConfigDIR = "attr_test_postgres"
default:
t.Fatal("Unknown Database type")
}

View File

@@ -415,3 +415,66 @@ func (ext *APIAttributeProfile) AsAttributeProfile() (attr *AttributeProfile, er
attr.Weights = ext.Weights
return
}
// AsMapStringInterface converts AttributeProfile struct to map[string]any
func (ap *AttributeProfile) AsMapStringInterface() map[string]any {
if ap == nil {
return nil
}
return map[string]any{
Tenant: ap.Tenant,
ID: ap.ID,
FilterIDs: ap.FilterIDs,
Weights: ap.Weights,
Blockers: ap.Blockers,
Attributes: ap.Attributes,
}
}
// MapStringInterfaceToAttributeProfile converts map[string]any to AttributeProfile struct
func MapStringInterfaceToAttributeProfile(m map[string]any) (ap *AttributeProfile, err error) {
ap = &AttributeProfile{}
if v, ok := m[Tenant].(string); ok {
ap.Tenant = v
}
if v, ok := m[ID].(string); ok {
ap.ID = v
}
ap.FilterIDs = InterfaceToStringSlice(m[FilterIDs])
ap.Weights = InterfaceToDynamicWeights(m[Weights])
ap.Blockers = InterfaceToDynamicBlockers(m[Blockers])
ap.Attributes = InterfaceToAttributes(m[Attributes])
return
}
// InterfaceToAttributes converts interface{} to []*Attribute
func InterfaceToAttributes(v any) []*Attribute {
if v == nil {
return nil
}
attrs, ok := v.([]any)
if !ok {
return nil
}
attributes := make([]*Attribute, 0, len(attrs))
for _, attrIface := range attrs {
attrMap, ok := attrIface.(map[string]any)
if !ok {
continue
}
attr := &Attribute{}
attr.FilterIDs = InterfaceToStringSlice(attrMap[FilterIDs])
attr.Blockers = InterfaceToDynamicBlockers(attrMap[Blockers])
if path, ok := attrMap[Path].(string); ok {
attr.Path = path
}
if typ, ok := attrMap[Type].(string); ok {
attr.Type = typ
}
if valueIface, ok := attrMap[Value]; ok {
attr.Value = InterfaceToRSRParsers(valueIface)
}
attributes = append(attributes, attr)
}
return attributes
}

View File

@@ -1897,29 +1897,30 @@ const (
// Table Name
const (
TBLTPResources = "tp_resources"
TBLTPStats = "tp_stats"
TBLTPRankings = "tp_rankings"
TBLTPTrends = "tp_trends"
TBLTPThresholds = "tp_thresholds"
TBLTPFilters = "tp_filters"
SessionCostsTBL = "session_costs"
CDRsTBL = "cdrs"
TBLTPRoutes = "tp_routes"
TBLTPAttributes = "tp_attributes"
TBLTPChargers = "tp_chargers"
TBLVersions = "versions"
TBLAccounts = "accounts"
TBLIPProfiles = "ip_profiles"
TBLIPAllocations = "ip_allocations"
TBLActionProfilesJSON = "action_profiles"
TBLChargerProfilesJSON = "charger_profiles"
OldSMCosts = "sm_costs"
TBLTPDispatchers = "tp_dispatcher_profiles"
TBLTPDispatcherHosts = "tp_dispatcher_hosts"
TBLTPRateProfiles = "tp_rate_profiles"
TBLTPActionProfiles = "tp_action_profiles"
TBLTPAccounts = "tp_accounts"
TBLTPResources = "tp_resources"
TBLTPStats = "tp_stats"
TBLTPRankings = "tp_rankings"
TBLTPTrends = "tp_trends"
TBLTPThresholds = "tp_thresholds"
TBLTPFilters = "tp_filters"
SessionCostsTBL = "session_costs"
CDRsTBL = "cdrs"
TBLTPRoutes = "tp_routes"
TBLTPAttributes = "tp_attributes"
TBLTPChargers = "tp_chargers"
TBLVersions = "versions"
TBLAccounts = "accounts"
TBLIPProfiles = "ip_profiles"
TBLIPAllocations = "ip_allocations"
TBLActionProfiles = "action_profiles"
TBLChargerProfiles = "charger_profiles"
TBLAttributeProfiles = "attribute_profiles"
OldSMCosts = "sm_costs"
TBLTPDispatchers = "tp_dispatcher_profiles"
TBLTPDispatcherHosts = "tp_dispatcher_hosts"
TBLTPRateProfiles = "tp_rate_profiles"
TBLTPActionProfiles = "tp_action_profiles"
TBLTPAccounts = "tp_accounts"
)
// Cache Name

View File

@@ -214,6 +214,31 @@ func (prsrs RSRParsers) AsStringSlice() (v []string) {
return
}
// InterfaceToRSRParsers converts interface to RSRParsers (unexported fields not included)
func InterfaceToRSRParsers(v any) RSRParsers {
if v == nil {
return nil
}
parsers := make(RSRParsers, 0)
switch val := v.(type) {
case []any:
for _, item := range val {
if parserMap, ok := item.(map[string]any); ok {
parser := &RSRParser{}
if rules, ok := parserMap[Rules].(string); ok {
parser.Rules = rules
}
if path, ok := parserMap[Path].(string); ok {
parser.Path = path
}
parsers = append(parsers, parser)
}
}
}
return parsers
}
// NewRSRParser builds one RSRParser
func NewRSRParser(parserRules string) (rsrParser *RSRParser, err error) {
if len(parserRules) == 0 {