Repository: casbin/casbin
Pull request #1050 (2022-07-11 10:15:26)

feat: add KeyGet3 for support parsing curly bracket
We have `keyMatch3` for `/alice_data/{resource}` now while lacking the corresponded `keyGet` for it. 
A user case: https://github.com/casbin/casbin/issues/1048#issuecomment-1179533275

The document is WIP.

casbin-bot:  @tangyang9464 @closetool @sagilio please review
JalinWang: Do we need to follow the codeclimate's suggestion ?
![image](https://user-images.githubusercontent.com/20621784/178242811-da1ad1fa-2a79-4421-8bb1-a9b2e718fd6b.png)

hsluoyz: @tangyang9464 
mnkg561: Thank you @JalinWang!! I'll wait until the new version is released then instead of writing my own custom function.
hsluoyz: @mnkg561 @tangyang9464 plz review

Patch:
diff --git a/model/function.go b/model/function.go
index 4a50415..0ae1246 100644
--- a/model/function.go
+++ b/model/function.go
@@ -43,6 +43,7 @@ func LoadFunctionMap() FunctionMap {
 	fm.AddFunction("keyMatch2", util.KeyMatch2Func)
 	fm.AddFunction("keyGet2", util.KeyGet2Func)
 	fm.AddFunction("keyMatch3", util.KeyMatch3Func)
+	fm.AddFunction("keyGet3", util.KeyGet3Func)
 	fm.AddFunction("keyMatch4", util.KeyMatch4Func)
 	fm.AddFunction("keyMatch5", util.KeyMatch5Func)
 	fm.AddFunction("regexMatch", util.RegexMatchFunc)
diff --git a/util/builtin_operators.go b/util/builtin_operators.go
index efd4c3c..fd24c64 100755
--- a/util/builtin_operators.go
+++ b/util/builtin_operators.go
@@ -182,6 +182,42 @@ func KeyMatch3Func(args ...interface{}) (interface{}, error) {
 	return bool(KeyMatch3(name1, name2)), nil
 }
 
+// KeyGet3 returns value matched pattern
+// For example, "project/proj_project1_admin/" matches "project/proj_{project}_admin/"
+// if the pathVar == "project", then "project1" will be returned
+func KeyGet3(key1, key2 string, pathVar string) string {
+	key2 = strings.Replace(key2, "/*", "/.*", -1)
+
+	re := regexp.MustCompile(`\{[^/]+?\}`) // non-greedy match of `{...}` to support multiple {} in `/.../`
+	keys := re.FindAllString(key2, -1)
+	key2 = re.ReplaceAllString(key2, "$1([^/]+?)$2")
+	key2 = "^" + key2 + "$"
+	re2 := regexp.MustCompile(key2)
+	values := re2.FindAllStringSubmatch(key1, -1)
+	if len(values) == 0 {
+		return ""
+	}
+	for i, key := range keys {
+		if pathVar == key[1:len(key)-1] {
+			return values[0][i+1]
+		}
+	}
+	return ""
+}
+
+// KeyGet3Func is the wrapper for KeyGet3
+func KeyGet3Func(args ...interface{}) (interface{}, error) {
+	if err := validateVariadicArgs(3, args...); err != nil {
+		return false, fmt.Errorf("%s: %s", "keyGet3", err)
+	}
+
+	name1 := args[0].(string)
+	name2 := args[1].(string)
+	key := args[2].(string)
+
+	return KeyGet3(name1, name2, key), nil
+}
+
 // KeyMatch4 determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *.
 // Besides what KeyMatch3 does, KeyMatch4 can also match repeated patterns:
 // "/parent/123/child/123" matches "/parent/{id}/child/{id}"


Test patch:
diff --git a/util/builtin_operators_test.go b/util/builtin_operators_test.go
index 1ac786b..44224bd 100644
--- a/util/builtin_operators_test.go
+++ b/util/builtin_operators_test.go
@@ -158,6 +158,7 @@ func TestKeyGet2(t *testing.T) {
 
 	testKeyGet2(t, "/alice/all", "/:/all", "", "")
 }
+
 func testKeyMatch3(t *testing.T, key1 string, key2 string, res bool) {
 	t.Helper()
 	myRes := KeyMatch3(key1, key2)
@@ -169,7 +170,7 @@ func testKeyMatch3(t *testing.T, key1 string, key2 string, res bool) {
 }
 
 func TestKeyMatch3(t *testing.T) {
-	// keyMatch3() is similar with KeyMatch2(), except using "/proxy/{id}" instead of "/proxy/:id".
+	// KeyGet3() is similar with KeyGet2(), except using "/proxy/{id}" instead of "/proxy/:id".
 	testKeyMatch3(t, "/foo", "/foo", true)
 	testKeyMatch3(t, "/foo", "/foo*", true)
 	testKeyMatch3(t, "/foo", "/foo/*", false)
@@ -195,6 +196,56 @@ func TestKeyMatch3(t *testing.T) {
 	testKeyMatch3(t, "/myid/using/myresid", "/{id/using/{resId}", false)
 }
 
+func testKeyGet3(t *testing.T, key1 string, key2 string, pathVar string, res string) {
+	t.Helper()
+	myRes := KeyGet3(key1, key2, pathVar)
+	t.Logf(`%s < %s: %s = "%s"`, key1, key2, pathVar, myRes)
+
+	if myRes != res {
+		t.Errorf(`%s < %s: %s = "%s" supposed to be "%s"`, key1, key2, pathVar, myRes, res)
+	}
+}
+
+func TestKeyGet3(t *testing.T) {
+	// keyMatch3() is similar with KeyMatch2(), except using "/proxy/{id}" instead of "/proxy/:id".
+	testKeyGet2(t, "/foo", "/foo", "id", "")
+	testKeyGet2(t, "/foo", "/foo*", "id", "")
+	testKeyGet2(t, "/foo", "/foo/*", "id", "")
+	testKeyGet2(t, "/foo/bar", "/foo", "id", "")
+	testKeyGet2(t, "/foo/bar", "/foo*", "id", "")
+	testKeyGet2(t, "/foo/bar", "/foo/*", "id", "")
+	testKeyGet2(t, "/foobar", "/foo", "id", "")
+	testKeyGet2(t, "/foobar", "/foo*", "id", "")
+	testKeyGet2(t, "/foobar", "/foo/*", "id", "")
+
+	testKeyGet3(t, "/", "/{resource}", "resource", "")
+	testKeyGet3(t, "/resource1", "/{resource}", "resource", "resource1")
+	testKeyGet3(t, "/myid", "/{id}/using/{resId}", "id", "")
+	testKeyGet3(t, "/myid/using/myresid", "/{id}/using/{resId}", "id", "myid")
+	testKeyGet3(t, "/myid/using/myresid", "/{id}/using/{resId}", "resId", "myresid")
+
+	testKeyGet3(t, "/proxy/myid", "/proxy/{id}/*", "id", "")
+	testKeyGet3(t, "/proxy/myid/", "/proxy/{id}/*", "id", "myid")
+	testKeyGet3(t, "/proxy/myid/res", "/proxy/{id}/*", "id", "myid")
+	testKeyGet3(t, "/proxy/myid/res/res2", "/proxy/{id}/*", "id", "myid")
+	testKeyGet3(t, "/proxy/myid/res/res2/res3", "/proxy/{id}/*", "id", "myid")
+	testKeyGet3(t, "/proxy/", "/proxy/{id}/*", "id", "")
+
+	testKeyGet3(t, "/api/group1_group_name/project1_admin/info", "/api/{proj}_admin/info",
+		"proj", "")
+	testKeyGet3(t, "/{id/using/myresid", "/{id/using/{resId}", "resId", "myresid")
+	testKeyGet3(t, "/{id/using/myresid/status}", "/{id/using/{resId}/status}", "resId", "myresid")
+
+	testKeyGet3(t, "/proxy/myid/res/res2/res3", "/proxy/{id}/*/{res}", "res", "res3")
+	testKeyGet3(t, "/api/project1_admin/info", "/api/{proj}_admin/info", "proj", "project1")
+	testKeyGet3(t, "/api/group1_group_name/project1_admin/info", "/api/{g}_{gn}/{proj}_admin/info",
+		"g", "group1")
+	testKeyGet3(t, "/api/group1_group_name/project1_admin/info", "/api/{g}_{gn}/{proj}_admin/info",
+		"gn", "group_name")
+	testKeyGet3(t, "/api/group1_group_name/project1_admin/info", "/api/{g}_{gn}/{proj}_admin/info",
+		"proj", "project1")
+}
+
 func testKeyMatch4(t *testing.T, key1 string, key2 string, res bool) {
 	t.Helper()
 	myRes := KeyMatch4(key1, key2)