From cec463f65802c6bd0682c30dc838a62c9e99d5d4 Mon Sep 17 00:00:00 2001 From: edwardro22 Date: Fri, 15 Dec 2017 08:31:46 +0200 Subject: [PATCH] Updated ArgRSv1ResourceUsage to use CGREvent --- apier/v1/resourcesv1_it_test.go | 137 ++++++++++------------------- console/threshold_process_event.go | 4 +- engine/libstats.go | 1 - engine/resources.go | 30 ++++--- sessionmanager/fssessionmanager.go | 16 ++-- sessionmanager/kamailiosm.go | 24 ++--- utils/apitpdata.go | 4 +- 7 files changed, 91 insertions(+), 125 deletions(-) diff --git a/apier/v1/resourcesv1_it_test.go b/apier/v1/resourcesv1_it_test.go index 5d980b201..18ce1d7a4 100644 --- a/apier/v1/resourcesv1_it_test.go +++ b/apier/v1/resourcesv1_it_test.go @@ -132,9 +132,9 @@ func testV1RsFromFolder(t *testing.T) { func testV1RsGetResourcesForEvent(t *testing.T) { var reply *[]*engine.ResourceProfile - args := &utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", - Event: map[string]interface{}{"Unknown": "unknown"}} + args := &utils.ArgRSv1ResourceUsage{} + args.CGREvent.Tenant="cgrates.org" + args.CGREvent.Event=map[string]interface{}{"Unknown": "unknown" } if err := rlsV1Rpc.Call(utils.ResourceSv1GetResourcesForEvent, args, &reply); err == nil || err.Error() != utils.ErrNotFound.Error() { t.Error(err) } @@ -179,49 +179,41 @@ func testV1RsGetResourcesForEvent(t *testing.T) { func testV1RsTTL0(t *testing.T) { // only matching Resource3 argsRU := utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e21", - Event: map[string]interface{}{ - "Account": "3001", - "Destination": "3002"}, Units: 1, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account": "3001","Destination": "3002"} var reply string if err := rlsV1Rpc.Call(utils.ResourceSv1AllocateResource, argsRU, &reply); err != nil { t.Error(err) } // second allocation should be also allowed argsRU = utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e21", - Event: map[string]interface{}{ - "Account": "3001", - "Destination": "3002"}, Units: 1, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account": "3001","Destination": "3002"} if err := rlsV1Rpc.Call(utils.ResourceSv1AllocateResource, argsRU, &reply); err != nil { t.Error(err) } // too many units should be rejected argsRU = utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e22", - Event: map[string]interface{}{ - "Account": "3001", - "Destination": "3002"}, Units: 2, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account": "3001","Destination": "3002"} if err := rlsV1Rpc.Call(utils.ResourceSv1AllocateResource, argsRU, &reply); err == nil || err.Error() != utils.ErrResourceUnavailable.Error() { t.Error(err) } // make sure no usage was recorded var rs *engine.Resources - args := &utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", - Event: map[string]interface{}{ - "Account": "3001", - "Destination": "3002"}} + args := &utils.ArgRSv1ResourceUsage{} + args.CGREvent.Tenant="cgrates.org" + args.CGREvent.Event=map[string]interface{}{"Account": "3001","Destination": "3002"} if err := rlsV1Rpc.Call(utils.ResourceSv1GetResourcesForEvent, args, &rs); err != nil { t.Error(err) } else if len(*rs) != 1 { @@ -235,12 +227,10 @@ func testV1RsTTL0(t *testing.T) { // release should not give out errors var releaseReply string argsRU = utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e25", // same ID should be accepted by first group since the previous resource should be expired - Event: map[string]interface{}{ - "Account": "3001", - "Destination": "3002"}, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account": "3001","Destination": "3002"} if err := rlsV1Rpc.Call(utils.ResourceSv1ReleaseResource, argsRU, &releaseReply); err != nil { t.Error(err) } @@ -250,14 +240,11 @@ func testV1RsAllocateResource(t *testing.T) { // first event matching Resource1 var reply string argsRU := utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e51", - Event: map[string]interface{}{ - "Account": "1002", - "Subject": "1001", - "Destination": "1002"}, Units: 3, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account":"1002","Subject":"1001","Destination":"1002"} if err := rlsV1Rpc.Call(utils.ResourceSv1AllocateResource, argsRU, &reply); err != nil { t.Error(err) } @@ -267,14 +254,11 @@ func testV1RsAllocateResource(t *testing.T) { } // Second event to test matching of exact limit of first resource argsRU = utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e52", - Event: map[string]interface{}{ - "Account": "1002", - "Subject": "1001", - "Destination": "1002"}, Units: 4, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account":"1002","Subject":"1001","Destination":"1002"} if err := rlsV1Rpc.Call(utils.ResourceSv1AllocateResource, argsRU, &reply); err != nil { t.Error(err) } @@ -284,14 +268,11 @@ func testV1RsAllocateResource(t *testing.T) { } // Third event testing overflow to second resource which still has one resource available argsRU = utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e53", - Event: map[string]interface{}{ - "Account": "dan", - "Subject": "dan", - "Destination": "1002"}, Units: 1, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account":"dan","Subject":"dan","Destination":"1002"} if err := rlsV1Rpc.Call(utils.ResourceSv1AllocateResource, argsRU, &reply); err != nil { t.Error(err) } @@ -301,14 +282,11 @@ func testV1RsAllocateResource(t *testing.T) { } // Test resource unavailable argsRU = utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e54", // same ID should be accepted by first group since the previous resource should be expired - Event: map[string]interface{}{ - "Account": "1002", - "Subject": "1001", - "Destination": "1002"}, Units: 1, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account":"1002","Subject":"1001","Destination":"1002"} if err := rlsV1Rpc.Call(utils.ResourceSv1AllocateResource, argsRU, &reply); err == nil || err.Error() != utils.ErrResourceUnavailable.Error() { t.Error(err) } @@ -316,14 +294,11 @@ func testV1RsAllocateResource(t *testing.T) { time.Sleep(time.Duration(1000) * time.Millisecond) // Give time for allocations on first resource to expire argsRU = utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e55", // same ID should be accepted by first group since the previous resource should be expired - Event: map[string]interface{}{ - "Account": "1002", - "Subject": "1001", - "Destination": "1002"}, Units: 1, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account":"1002","Subject":"1001","Destination":"1002"} if err := rlsV1Rpc.Call(utils.ResourceSv1AllocateResource, argsRU, &reply); err != nil { t.Error(err) } @@ -336,28 +311,23 @@ func testV1RsAllocateResource(t *testing.T) { func testV1RsAllowUsage(t *testing.T) { var allowed bool argsRU := utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e61", - Event: map[string]interface{}{ - "Account": "1002", - "Subject": "1001", - "Destination": "1002"}, Units: 6, } + + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account":"1002","Subject":"1001","Destination":"1002"} if err := rlsV1Rpc.Call(utils.ResourceSv1AllowUsage, argsRU, &allowed); err != nil { t.Error(err) } else if !allowed { // already 3 usages active before allow call, we should have now more than allowed t.Error("resource is not allowed") } argsRU = utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e61", - Event: map[string]interface{}{ - "Account": "1002", - "Subject": "1001", - "Destination": "1002"}, Units: 7, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account":"1002","Subject":"1001","Destination":"1002"} if err := rlsV1Rpc.Call(utils.ResourceSv1AllowUsage, argsRU, &allowed); err != nil { t.Error(err) } else if allowed { // already 3 usages active before allow call, we should have now more than allowed @@ -369,27 +339,21 @@ func testV1RsReleaseResource(t *testing.T) { // relase the only resource active for Resource1 var reply string argsRU := utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e55", // same ID should be accepted by first group since the previous resource should be expired - Event: map[string]interface{}{ - "Account": "1002", - "Subject": "1001", - "Destination": "1002"}, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account":"1002","Subject":"1001","Destination":"1002"} if err := rlsV1Rpc.Call(utils.ResourceSv1ReleaseResource, argsRU, &reply); err != nil { t.Error(err) } // try reserving with full units for Resource1, case which did not work in previous test // only match Resource1 since we don't want for storing of the resource2 bellow argsRU = utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e61", - Event: map[string]interface{}{ - "Account": "1002", - "Subject": "1001", - "Destination": "2002"}, Units: 7, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account":"1002","Subject":"1001","Destination":"1002"} var allowed bool if err := rlsV1Rpc.Call(utils.ResourceSv1AllowUsage, argsRU, &allowed); err != nil { t.Error(err) @@ -397,12 +361,9 @@ func testV1RsReleaseResource(t *testing.T) { t.Error("resource should be allowed") } var rs *engine.Resources - args := &utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", - Event: map[string]interface{}{ - "Account": "1002", - "Subject": "1001", - "Destination": "1002"}} + args := &utils.ArgRSv1ResourceUsage{} + args.CGREvent.Tenant="cgrates.org" + args.CGREvent.Event=map[string]interface{}{"Account":"1002","Subject":"1001","Destination":"1002"} if err := rlsV1Rpc.Call(utils.ResourceSv1GetResourcesForEvent, args, &rs); err != nil { t.Error(err) } else if len(*rs) != 2 { @@ -419,14 +380,11 @@ func testV1RsReleaseResource(t *testing.T) { func testV1RsDBStore(t *testing.T) { argsRU := utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", UsageID: "651a8db2-4f67-4cf8-b622-169e8a482e71", - Event: map[string]interface{}{ - "Account": "1002", - "Subject": "1001", - "Destination": "1002"}, Units: 1, } + argsRU.CGREvent.Tenant="cgrates.org" + argsRU.CGREvent.Event=map[string]interface{}{"Account":"1002","Subject":"1001","Destination":"1002"} var reply string eAllocationMsg := "ResGroup1" if err := rlsV1Rpc.Call(utils.ResourceSv1AllocateResource, argsRU, &reply); err != nil { @@ -435,12 +393,9 @@ func testV1RsDBStore(t *testing.T) { t.Errorf("Expecting: %+v, received: %+v", eAllocationMsg, reply) } var rs *engine.Resources - args := &utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", - Event: map[string]interface{}{ - "Account": "1002", - "Subject": "1001", - "Destination": "1002"}} + args := &utils.ArgRSv1ResourceUsage{} + args.CGREvent.Tenant="cgrates.org" + args.CGREvent.Event=map[string]interface{}{"Account":"1002","Subject":"1001","Destination":"1002"} if err := rlsV1Rpc.Call(utils.ResourceSv1GetResourcesForEvent, args, &rs); err != nil { t.Error(err) } else if len(*rs) != 2 { @@ -468,12 +423,10 @@ func testV1RsDBStore(t *testing.T) { t.Fatal("Could not connect to rater: ", err.Error()) } rs = new(engine.Resources) - args = &utils.ArgRSv1ResourceUsage{ - Tenant: "cgrates.org", - Event: map[string]interface{}{ - "Account": "1002", - "Subject": "1001", - "Destination": "1002"}} + args = &utils.ArgRSv1ResourceUsage{} + args.CGREvent.Tenant="cgrates.org" + args.CGREvent.Event=map[string]interface{}{"Account":"1002","Subject":"1001","Destination":"1002"} + if err := rlsV1Rpc.Call(utils.ResourceSv1GetResourcesForEvent, args, &rs); err != nil { t.Error(err) } else if len(*rs) != 2 { diff --git a/console/threshold_process_event.go b/console/threshold_process_event.go index d7b775651..3a7949ff9 100644 --- a/console/threshold_process_event.go +++ b/console/threshold_process_event.go @@ -63,8 +63,8 @@ func (self *CmdThresholdProcessEvent) PostprocessRpcParams() error { //utils.CGR ID: utils.UUIDSha1Prefix(), Event: *param, } - if (*param)[utils.TENANT] != nil && (*param)[utils.TENANT].(string) != "" { - cgrev.Tenant = (*param)[utils.TENANT].(string) + if (*param)[utils.Tenant] != nil && (*param)[utils.Tenant].(string) != "" { + cgrev.Tenant = (*param)[utils.Tenant].(string) } self.rpcParams = cgrev return nil diff --git a/engine/libstats.go b/engine/libstats.go index a365bb684..3758be945 100755 --- a/engine/libstats.go +++ b/engine/libstats.go @@ -45,7 +45,6 @@ func (sqp *StatQueueProfile) TenantID() string { return utils.ConcatenatedKey(sqp.Tenant, sqp.ID) } - // NewStoredStatQueue initiates a StoredStatQueue out of StatQueue func NewStoredStatQueue(sq *StatQueue, ms Marshaler) (sSQ *StoredStatQueue, err error) { sSQ = &StoredStatQueue{ diff --git a/engine/resources.go b/engine/resources.go index d2f8e14d0..a7cc1196a 100755 --- a/engine/resources.go +++ b/engine/resources.go @@ -510,18 +510,20 @@ func (rS *ResourceService) processThresholds(r *Resource) (err error) { // V1ResourcesForEvent returns active resource configs matching the event func (rS *ResourceService) V1ResourcesForEvent(args utils.ArgRSv1ResourceUsage, reply *Resources) (err error) { - if args.Tenant == "" { + if args.CGREvent.Tenant == "" { return utils.NewErrMandatoryIeMissing("Tenant") + } else if args.UsageID == "" { + return utils.NewErrMandatoryIeMissing("UsageID") } var mtcRLs Resources if args.UsageID != "" { // only cached if UsageID is present - mtcRLs = rS.cachedResourcesForEvent(args.TenantID()) + mtcRLs = rS.cachedResourcesForEvent(args.CGREvent.TenantID()) } if mtcRLs == nil { - if mtcRLs, err = rS.matchingResourcesForEvent(args.Tenant, args.Event); err != nil { + if mtcRLs, err = rS.matchingResourcesForEvent(args.CGREvent.Tenant, args.CGREvent.Event); err != nil { return err } - cache.Set(utils.EventResourcesPrefix+args.TenantID(), mtcRLs.tenantIDs(), true, "") + cache.Set(utils.EventResourcesPrefix+args.CGREvent.TenantID(), mtcRLs.tenantIDs(), true, "") } if len(mtcRLs) == 0 { return utils.ErrNotFound @@ -530,10 +532,14 @@ func (rS *ResourceService) V1ResourcesForEvent(args utils.ArgRSv1ResourceUsage, return } +//cgrevent tenant +id si la args USAGEID + // V1AllowUsage queries service to find if an Usage is allowed func (rS *ResourceService) V1AllowUsage(args utils.ArgRSv1ResourceUsage, allow *bool) (err error) { - if missing := utils.MissingStructFields(&args, []string{"Tenant", "UsageID"}); len(missing) != 0 { //Params missing - return utils.NewErrMandatoryIeMissing(missing...) + if args.CGREvent.Tenant == "" { + return utils.NewErrMandatoryIeMissing("Tenant") + } else if args.UsageID == "" { + return utils.NewErrMandatoryIeMissing("UsageID") } mtcRLs := rS.cachedResourcesForEvent(args.TenantID()) if mtcRLs == nil { @@ -560,8 +566,10 @@ func (rS *ResourceService) V1AllowUsage(args utils.ArgRSv1ResourceUsage, allow * // V1AllocateResource is called when a resource requires allocation func (rS *ResourceService) V1AllocateResource(args utils.ArgRSv1ResourceUsage, reply *string) (err error) { - if missing := utils.MissingStructFields(&args, []string{"Tenant", "UsageID"}); len(missing) != 0 { //Params missing - return utils.NewErrMandatoryIeMissing(missing...) + if args.CGREvent.Tenant == "" { + return utils.NewErrMandatoryIeMissing("Tenant") + } else if args.UsageID == "" { + return utils.NewErrMandatoryIeMissing("UsageID") } var wasCached bool mtcRLs := rS.cachedResourcesForEvent(args.UsageID) @@ -613,8 +621,10 @@ func (rS *ResourceService) V1AllocateResource(args utils.ArgRSv1ResourceUsage, r // V1ReleaseResource is called when we need to clear an allocation func (rS *ResourceService) V1ReleaseResource(args utils.ArgRSv1ResourceUsage, reply *string) (err error) { - if missing := utils.MissingStructFields(&args, []string{"Tenant", "UsageID"}); len(missing) != 0 { //Params missing - return utils.NewErrMandatoryIeMissing(missing...) + if args.CGREvent.Tenant == "" { + return utils.NewErrMandatoryIeMissing("Tenant") + } else if args.UsageID == "" { + return utils.NewErrMandatoryIeMissing("UsageID") } mtcRLs := rS.cachedResourcesForEvent(args.UsageID) if mtcRLs == nil { diff --git a/sessionmanager/fssessionmanager.go b/sessionmanager/fssessionmanager.go index 70802e18e..7cc339eec 100644 --- a/sessionmanager/fssessionmanager.go +++ b/sessionmanager/fssessionmanager.go @@ -214,11 +214,12 @@ func (sm *FSSessionManager) onChannelPark(ev engine.Event, connId string) { if sm.rls != nil { var reply string attrRU := utils.ArgRSv1ResourceUsage{ - Tenant: ev.(FSEvent).GetTenant(utils.META_DEFAULT), UsageID: ev.GetUUID(), - Event: ev.(FSEvent).AsMapStringInterface(sm.timezone), Units: 1, } + attrRU.CGREvent.ID=utils.UUIDSha1Prefix() + attrRU.CGREvent.Tenant=ev.(FSEvent).GetTenant(utils.META_DEFAULT) + attrRU.CGREvent.Event=ev.(FSEvent).AsMapStringInterface(sm.timezone) if err := sm.rls.Call(utils.ResourceSv1AllocateResource, attrRU, &reply); err != nil { if err.Error() == utils.ErrResourceUnavailable.Error() { sm.unparkCall(ev.GetUUID(), connId, ev.GetCallDestNr(utils.META_DEFAULT), "-"+utils.ErrResourceUnavailable.Error()) @@ -280,11 +281,12 @@ func (sm *FSSessionManager) onChannelHangupComplete(ev engine.Event) { } var reply string attrRU := utils.ArgRSv1ResourceUsage{ - Tenant: ev.(FSEvent).GetTenant(utils.META_DEFAULT), - UsageID: ev.GetUUID(), - Event: ev.(FSEvent).AsMapStringInterface(sm.timezone), - Units: 1, - } + UsageID: ev.GetUUID(), + Units: 1, + } + attrRU.CGREvent.ID=utils.UUIDSha1Prefix() + attrRU.CGREvent.Tenant=ev.(FSEvent).GetTenant(utils.META_DEFAULT) + attrRU.CGREvent.Event=ev.(FSEvent).AsMapStringInterface(sm.timezone) if sm.rls != nil { if err := sm.rls.Call(utils.ResourceSv1ReleaseResource, attrRU, &reply); err != nil { utils.Logger.Err(fmt.Sprintf(" RLs API error: %s", err.Error())) diff --git a/sessionmanager/kamailiosm.go b/sessionmanager/kamailiosm.go index 5b2197838..a5fb40237 100644 --- a/sessionmanager/kamailiosm.go +++ b/sessionmanager/kamailiosm.go @@ -81,11 +81,12 @@ func (self *KamailioSessionManager) allocateResources(kev KamEvent) (err error) return } attrRU := utils.ArgRSv1ResourceUsage{ - Tenant: kev.GetTenant(utils.META_DEFAULT), - UsageID: kev.GetUUID(), - Event: ev, - Units: 1, // One channel reserved - } + UsageID: kev.GetUUID(), + Units: 1, + } + attrRU.CGREvent.ID=utils.UUIDSha1Prefix() + attrRU.CGREvent.Tenant=kev.GetTenant(utils.META_DEFAULT) + attrRU.CGREvent.Event=ev var reply string return self.rlS.Call(utils.ResourceSv1AllocateResource, attrRU, &reply) } @@ -214,12 +215,13 @@ func (self *KamailioSessionManager) onCallEnd(evData []byte, connId string) { return } var reply string - attrRU := utils.ArgRSv1ResourceUsage{ - Tenant: kev.GetTenant(utils.META_DEFAULT), - UsageID: kev.GetUUID(), - Event: ev, - Units: 1, - } + attrRU := utils.ArgRSv1ResourceUsage{ + UsageID: kev.GetUUID(), + Units: 1, + } + attrRU.CGREvent.ID=utils.UUIDSha1Prefix() + attrRU.CGREvent.Tenant=kev.GetTenant(utils.META_DEFAULT) + attrRU.CGREvent.Event=ev if err := self.rlS.Call(utils.ResourceSv1ReleaseResource, attrRU, &reply); err != nil { utils.Logger.Err(fmt.Sprintf(" RLs API error: %s", err.Error())) } diff --git a/utils/apitpdata.go b/utils/apitpdata.go index 2592140b5..b059b4c41 100755 --- a/utils/apitpdata.go +++ b/utils/apitpdata.go @@ -23,6 +23,7 @@ import ( "sort" "strings" "time" + ) // Used to extract ids from stordb @@ -1276,10 +1277,9 @@ type AttrRLsCache struct { } type ArgRSv1ResourceUsage struct { - Tenant string + CGREvent UsageID string // ResourceUsage Identifier Units float64 - Event map[string]interface{} } func (args *ArgRSv1ResourceUsage) TenantID() string {