Skip to content

Commit ce7d6f6

Browse files
authored
feat(util): implement type name conversion utility (#85)
* feat(util): implement type name conversion utility • Adds ConvertToTypeName and capGroupSingularLength functions • Supports formatting group and singular names for consistent type naming Signed-off-by: Bastian Echterhölter <[email protected]> On-behalf-of: @SAP <[email protected]> * refactor(util): improve variable naming for clarity • Renames variables for better readability and understanding Signed-off-by: Bastian Echterhölter <[email protected]> On-behalf-of: @SAP <[email protected]> * feat(util): enhance type name conversion utility • Add detailed documentation for ConvertToTypeName and capGroupSingularLength functions • Ensure consistent naming conventions for authorization systems Signed-off-by: Bastian Echterhölter <[email protected]> On-behalf-of: @SAP <[email protected]> --------- Signed-off-by: Bastian Echterhölter <[email protected]>
1 parent 1dd9246 commit ce7d6f6

File tree

3 files changed

+247
-0
lines changed

3 files changed

+247
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
*.out
44
.DS_Store
55
.idea
6+
.claude
67
/bin

fga/util/util.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Package util provides utility functions for converting API group and resource names
2+
// into normalized type names suitable for use in authorization systems.
3+
//
4+
// This package is primarily designed for use with Fine-Grained Authorization (FGA)
5+
// systems where consistent naming conventions are required for type definitions.
6+
package util
7+
8+
import (
9+
"fmt"
10+
"strings"
11+
)
12+
13+
// maxRelationLength defines the maximum allowed length for relation names in FGA systems.
14+
// This limit ensures compatibility with authorization backends that have string length
15+
// constraints on relation and type names.
16+
//
17+
// The value of 50 is chosen to accommodate the longest possible relation format:
18+
// "create_<group>_<singular>s" while leaving room for reasonable group and resource names.
19+
const maxRelationLength = 50
20+
21+
// ConvertToTypeName converts an API group and singular resource name into a normalized
22+
// type name suitable for use in authorization systems.
23+
//
24+
// Parameters:
25+
// - group: The API group name (e.g., "apps", "networking.k8s.io", "")
26+
// - singular: The singular form of the resource name (e.g., "deployment", "pod", "Service")
27+
//
28+
// Returns:
29+
//
30+
// A normalized type name string suitable for authorization system usage.
31+
//
32+
// Examples:
33+
//
34+
// ConvertToTypeName("apps", "deployment") → "apps_deployment"
35+
// ConvertToTypeName("", "pod") → "core_pod"
36+
// ConvertToTypeName("networking.k8s.io", "ingress") → "networking_k8s_io_ingress"
37+
// ConvertToTypeName("Apps", "Deployment") → "apps_deployment"
38+
//
39+
// The function handles edge cases gracefully:
40+
// - Empty group names default to "core"
41+
// - Very long names are truncated to respect maxRelationLength
42+
// - Special characters like dots are normalized to underscores
43+
// - Mixed case is normalized to lowercase
44+
func ConvertToTypeName(group, singular string) string {
45+
if group == "" {
46+
group = "core"
47+
}
48+
49+
// Cap the length of the group_singular string to respect relation length limits
50+
objectType := capGroupSingularLength(group, singular, maxRelationLength)
51+
52+
// Make sure the result does not start with an underscore (can happen with empty groups)
53+
objectType = strings.TrimPrefix(objectType, "_")
54+
55+
// Replace dots with underscores in the final objectType for system compatibility
56+
objectType = strings.ReplaceAll(objectType, ".", "_")
57+
58+
// Convert to lowercase for consistent naming conventions
59+
objectType = strings.ToLower(objectType)
60+
61+
return objectType
62+
}
63+
64+
// capGroupSingularLength creates a group_singular string and truncates it if necessary
65+
// to ensure the resulting relation names don't exceed the specified maximum length.
66+
//
67+
// This function is used internally by ConvertToTypeName to handle length constraints
68+
// imposed by authorization systems. It calculates the potential length of the longest
69+
// relation that would be created ("create_<group>_<singular>") and truncates the
70+
// group_singular combination if needed.
71+
func capGroupSingularLength(group, singular string, maxLength int) string {
72+
groupSingular := fmt.Sprintf("%s_%s", group, singular)
73+
maxRelation := fmt.Sprintf("create_%ss", groupSingular)
74+
75+
if len(maxRelation) > maxLength && maxLength > 0 {
76+
truncateLen := len(maxRelation) - maxLength
77+
return groupSingular[truncateLen:]
78+
}
79+
return groupSingular
80+
}

fga/util/util_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package util
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestConvertToTypeName(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
group string
11+
singular string
12+
expected string
13+
}{
14+
{
15+
name: "basic conversion",
16+
group: "apps",
17+
singular: "deployment",
18+
expected: "apps_deployment",
19+
},
20+
{
21+
name: "empty group defaults to core",
22+
group: "",
23+
singular: "pod",
24+
expected: "core_pod",
25+
},
26+
{
27+
name: "group with dots gets replaced with underscores",
28+
group: "networking.k8s.io",
29+
singular: "ingress",
30+
expected: "networking_k8s_io_ingress",
31+
},
32+
{
33+
name: "single character group and singular",
34+
group: "a",
35+
singular: "b",
36+
expected: "a_b",
37+
},
38+
{
39+
name: "numeric group and singular",
40+
group: "v1",
41+
singular: "service",
42+
expected: "v1_service",
43+
},
44+
{
45+
name: "mixed case should be lowercased",
46+
group: "Apps",
47+
singular: "Deployment",
48+
expected: "apps_deployment",
49+
},
50+
{
51+
name: "short group and singular within limits",
52+
group: "batch",
53+
singular: "job",
54+
expected: "batch_job",
55+
},
56+
{
57+
name: "handles special characters in group",
58+
group: "group-with-dashes",
59+
singular: "kind",
60+
expected: "group-with-dashes_kind",
61+
},
62+
{
63+
name: "both group and singular empty",
64+
group: "",
65+
singular: "",
66+
expected: "core_",
67+
},
68+
{
69+
name: "dots in group get replaced properly",
70+
group: "apps.v1",
71+
singular: "deployment",
72+
expected: "apps_v1_deployment",
73+
},
74+
{
75+
name: "multiple dots get replaced",
76+
group: "foo.bar.baz",
77+
singular: "resource",
78+
expected: "foo_bar_baz_resource",
79+
},
80+
}
81+
82+
for _, tt := range tests {
83+
t.Run(tt.name, func(t *testing.T) {
84+
result := ConvertToTypeName(tt.group, tt.singular)
85+
if result != tt.expected {
86+
t.Errorf("ConvertToTypeName(%q, %q) = %q, want %q", tt.group, tt.singular, result, tt.expected)
87+
}
88+
})
89+
}
90+
}
91+
92+
func TestCapGroupSingularLength(t *testing.T) {
93+
tests := []struct {
94+
name string
95+
group string
96+
singular string
97+
maxLength int
98+
expected string
99+
}{
100+
{
101+
name: "group and singular within max length",
102+
group: "apps",
103+
singular: "deployment",
104+
maxLength: 50,
105+
expected: "apps_deployment",
106+
},
107+
{
108+
name: "empty group with capGroupSingularLength",
109+
group: "",
110+
singular: "pod",
111+
maxLength: 50,
112+
expected: "_pod",
113+
},
114+
{
115+
name: "short group and singular that stays within limits",
116+
group: "batch",
117+
singular: "job",
118+
maxLength: 30,
119+
expected: "batch_job",
120+
},
121+
{
122+
name: "exact max length boundary",
123+
group: "test",
124+
singular: "resource",
125+
maxLength: 20,
126+
expected: "est_resource", // "create_test_resources" = 21 chars, truncate 1: "est_resource"
127+
},
128+
{
129+
name: "successful truncation case",
130+
group: "verylonggroup",
131+
singular: "job",
132+
maxLength: 20,
133+
expected: "onggroup_job", // "create_verylonggroup_jobs" = 25 chars, truncate 5: "onggroup_job"
134+
},
135+
{
136+
name: "truncation case with very long singular",
137+
group: "short",
138+
singular: "verylongkindnamethatexceedseverything",
139+
maxLength: 10,
140+
expected: "ng", // Additional "s" in relation makes it one char shorter
141+
},
142+
{
143+
name: "maxLength zero returns original groupKind",
144+
group: "apps",
145+
singular: "deployment",
146+
maxLength: 0,
147+
expected: "apps_deployment",
148+
},
149+
{
150+
name: "truncation within group length",
151+
group: "exactlength",
152+
singular: "test",
153+
maxLength: 15,
154+
expected: "th_test", // "create_exactlength_tests" = 24 chars, truncate 9: "th_test"
155+
},
156+
}
157+
158+
for _, tt := range tests {
159+
t.Run(tt.name, func(t *testing.T) {
160+
result := capGroupSingularLength(tt.group, tt.singular, tt.maxLength)
161+
if result != tt.expected {
162+
t.Errorf("capGroupSingularLength(%q, %q, %d) = %q, want %q", tt.group, tt.singular, tt.maxLength, result, tt.expected)
163+
}
164+
})
165+
}
166+
}

0 commit comments

Comments
 (0)