refactor: deduplicate utility helpers across the codebase (#23338)

Audited exported helpers in `coderd/util/*`, `testutil`, `cryptorand`,
and friends, then replaced duplicated implementations with canonical
versions.

- **fix: `maps.SortedKeys` generic signature** — value type was
hardcoded to `any`, making it impossible to actually call. Added second
type parameter `V any`. Added table-driven tests with `cmp.Diff`.
- **refactor: replace ad-hoc ptr helpers with `ptr.Ref`** — removed
`int64Ptr`, `stringPtr`, `boolPtr`, `i64ptr`, `strPtr`, `PtrInt32`
across 6 files.
- **refactor: replace local `sortedKeys`/`sortKeys` with
`maps.SortedKeys`** — now that the signature is fixed, scripts can use
it.
- **refactor: replace hand-rolled `capitalize` with
`strings.Capitalize`** — the typegen version was also not UTF-8 safe.

> 🤖 This PR was created with the help of Coder Agents, and was reviewed
by my human. 🧑‍💻
This commit is contained in:
Cian Johnston
2026-03-20 15:12:41 +00:00
committed by GitHub
parent 23542cb6af
commit f1d333f0e6
11 changed files with 116 additions and 114 deletions
+6 -9
View File
@@ -30,6 +30,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/pubsub"
coderdpubsub "github.com/coder/coder/v2/coderd/pubsub"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/webpush"
"github.com/coder/coder/v2/coderd/workspacestats"
"github.com/coder/coder/v2/codersdk"
@@ -2916,19 +2917,19 @@ func (p *Server) runChat(
var usageForCost codersdk.ChatMessageUsage
if hasUsage {
if step.Usage.InputTokens != 0 {
usageForCost.InputTokens = int64Ptr(step.Usage.InputTokens)
usageForCost.InputTokens = ptr.Ref(step.Usage.InputTokens)
}
if step.Usage.OutputTokens != 0 {
usageForCost.OutputTokens = int64Ptr(step.Usage.OutputTokens)
usageForCost.OutputTokens = ptr.Ref(step.Usage.OutputTokens)
}
if step.Usage.ReasoningTokens != 0 {
usageForCost.ReasoningTokens = int64Ptr(step.Usage.ReasoningTokens)
usageForCost.ReasoningTokens = ptr.Ref(step.Usage.ReasoningTokens)
}
if step.Usage.CacheCreationTokens != 0 {
usageForCost.CacheCreationTokens = int64Ptr(step.Usage.CacheCreationTokens)
usageForCost.CacheCreationTokens = ptr.Ref(step.Usage.CacheCreationTokens)
}
if step.Usage.CacheReadTokens != 0 {
usageForCost.CacheReadTokens = int64Ptr(step.Usage.CacheReadTokens)
usageForCost.CacheReadTokens = ptr.Ref(step.Usage.CacheReadTokens)
}
}
totalCostMicros := chatcost.CalculateTotalCostMicros(usageForCost, callConfig.Cost)
@@ -3532,10 +3533,6 @@ func (p *Server) resolveModelConfig(
return defaultConfig, nil
}
func int64Ptr(value int64) *int64 {
return &value
}
func refreshChatWorkspaceSnapshot(
ctx context.Context,
chat database.Chat,
+21 -32
View File
@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/chatd/chatprovider"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
)
@@ -25,37 +26,37 @@ func TestReasoningEffortFromChat(t *testing.T) {
{
name: "OpenAICaseInsensitive",
provider: "openai",
input: stringPtr(" HIGH "),
want: stringPtr(string(fantasyopenai.ReasoningEffortHigh)),
input: ptr.Ref(" HIGH "),
want: ptr.Ref(string(fantasyopenai.ReasoningEffortHigh)),
},
{
name: "AnthropicEffort",
provider: "anthropic",
input: stringPtr("max"),
want: stringPtr(string(fantasyanthropic.EffortMax)),
input: ptr.Ref("max"),
want: ptr.Ref(string(fantasyanthropic.EffortMax)),
},
{
name: "OpenRouterEffort",
provider: "openrouter",
input: stringPtr("medium"),
want: stringPtr(string(fantasyopenrouter.ReasoningEffortMedium)),
input: ptr.Ref("medium"),
want: ptr.Ref(string(fantasyopenrouter.ReasoningEffortMedium)),
},
{
name: "VercelEffort",
provider: "vercel",
input: stringPtr("xhigh"),
want: stringPtr(string(fantasyvercel.ReasoningEffortXHigh)),
input: ptr.Ref("xhigh"),
want: ptr.Ref(string(fantasyvercel.ReasoningEffortXHigh)),
},
{
name: "InvalidEffortReturnsNil",
provider: "openai",
input: stringPtr("unknown"),
input: ptr.Ref("unknown"),
want: nil,
},
{
name: "UnsupportedProviderReturnsNil",
provider: "bedrock",
input: stringPtr("high"),
input: ptr.Ref("high"),
want: nil,
},
{
@@ -83,7 +84,7 @@ func TestMergeMissingProviderOptions_OpenRouterNested(t *testing.T) {
options := &codersdk.ChatModelProviderOptions{
OpenRouter: &codersdk.ChatModelOpenRouterProviderOptions{
Reasoning: &codersdk.ChatModelReasoningOptions{
Enabled: boolPtr(true),
Enabled: ptr.Ref(true),
},
Provider: &codersdk.ChatModelOpenRouterProvider{
Order: []string{"openai"},
@@ -93,21 +94,21 @@ func TestMergeMissingProviderOptions_OpenRouterNested(t *testing.T) {
defaults := &codersdk.ChatModelProviderOptions{
OpenRouter: &codersdk.ChatModelOpenRouterProviderOptions{
Reasoning: &codersdk.ChatModelReasoningOptions{
Enabled: boolPtr(false),
Exclude: boolPtr(true),
MaxTokens: int64Ptr(123),
Effort: stringPtr("high"),
Enabled: ptr.Ref(false),
Exclude: ptr.Ref(true),
MaxTokens: ptr.Ref[int64](123),
Effort: ptr.Ref("high"),
},
IncludeUsage: boolPtr(true),
IncludeUsage: ptr.Ref(true),
Provider: &codersdk.ChatModelOpenRouterProvider{
Order: []string{"anthropic"},
AllowFallbacks: boolPtr(true),
RequireParameters: boolPtr(false),
DataCollection: stringPtr("allow"),
AllowFallbacks: ptr.Ref(true),
RequireParameters: ptr.Ref(false),
DataCollection: ptr.Ref("allow"),
Only: []string{"openai"},
Ignore: []string{"foo"},
Quantizations: []string{"int8"},
Sort: stringPtr("latency"),
Sort: ptr.Ref("latency"),
},
},
}
@@ -136,15 +137,3 @@ func TestMergeMissingProviderOptions_OpenRouterNested(t *testing.T) {
require.Equal(t, []string{"int8"}, options.OpenRouter.Provider.Quantizations)
require.Equal(t, "latency", *options.OpenRouter.Provider.Sort)
}
func stringPtr(value string) *string {
return &value
}
func boolPtr(value bool) *bool {
return &value
}
func int64Ptr(value int64) *int64 {
return &value
}
+3 -6
View File
@@ -43,6 +43,7 @@ import (
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
"github.com/coder/coder/v2/coderd/promoauth"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/testutil"
@@ -405,7 +406,7 @@ func TestUserOAuth2Github(t *testing.T) {
AuthenticatedUser: func(ctx context.Context, _ *http.Client) (*github.User, error) {
return &github.User{
AvatarURL: github.String("/hello-world"),
ID: i64ptr(1234),
ID: ptr.Ref[int64](1234),
Login: github.String("kyle"),
Name: github.String("Kylium Carbonate"),
}, nil
@@ -473,7 +474,7 @@ func TestUserOAuth2Github(t *testing.T) {
AuthenticatedUser: func(_ context.Context, _ *http.Client) (*github.User, error) {
return &github.User{
AvatarURL: github.String("/hello-world"),
ID: i64ptr(1234),
ID: ptr.Ref[int64](1234),
Login: github.String("kyle"),
Name: github.String(" " + strings.Repeat("a", 129) + " "),
}, nil
@@ -2525,10 +2526,6 @@ func oauth2Callback(t *testing.T, client *codersdk.Client, opts ...func(*http.Re
return res
}
func i64ptr(i int64) *int64 {
return &i
}
func authCookieValue(cookies []*http.Cookie) string {
for _, cookie := range cookies {
if cookie.Name == codersdk.SessionTokenCookie {
+1 -1
View File
@@ -31,7 +31,7 @@ func Subset[T, U comparable](a, b map[T]U) bool {
}
// SortedKeys returns the keys of m in sorted order.
func SortedKeys[T constraints.Ordered](m map[T]any) (keys []T) {
func SortedKeys[K constraints.Ordered, V any](m map[K]V) (keys []K) {
for k := range m {
keys = append(keys, k)
}
+44
View File
@@ -4,9 +4,53 @@ import (
"strconv"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/coder/coder/v2/coderd/util/maps"
)
func TestSortedKeys(t *testing.T) {
t.Parallel()
for idx, tc := range []struct {
name string
input map[string]int
expected []string
}{
{
name: "SortsAlphabetically",
input: map[string]int{
"banana": 1,
"apple": 2,
"cherry": 3,
},
expected: []string{"apple", "banana", "cherry"},
},
{
name: "AlreadySorted",
input: map[string]int{
"alpha": 1,
"mango": 2,
"zebra": 3,
},
expected: []string{"alpha", "mango", "zebra"},
},
{
name: "EmptyMap",
input: map[string]int{},
expected: nil,
},
} {
t.Run("#"+strconv.Itoa(idx)+"_"+tc.name, func(t *testing.T) {
t.Parallel()
got := maps.SortedKeys(tc.input)
if diff := cmp.Diff(tc.expected, got); diff != "" {
t.Fatalf("unexpected result (-want +got):\n%s", diff)
}
})
}
}
func TestSubset(t *testing.T) {
t.Parallel()
@@ -33,6 +33,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/externalauth"
codermcp "github.com/coder/coder/v2/coderd/mcp"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/enterprise/aibridged"
@@ -421,7 +422,7 @@ func TestRecordInterception(t *testing.T) {
Model: "claude-4-opus",
Metadata: metadataProto,
StartedAt: timestamppb.Now(),
ClientSessionId: strPtr("session-abc-123"),
ClientSessionId: ptr.Ref("session-abc-123"),
},
setupMocks: func(t *testing.T, db *dbmock.MockStore, req *proto.RecordInterceptionRequest) {
interceptionID, err := uuid.Parse(req.GetId())
@@ -459,7 +460,7 @@ func TestRecordInterception(t *testing.T) {
Model: "claude-4-opus",
Metadata: metadataProto,
StartedAt: timestamppb.Now(),
ClientSessionId: strPtr(""),
ClientSessionId: ptr.Ref(""),
},
setupMocks: func(t *testing.T, db *dbmock.MockStore, req *proto.RecordInterceptionRequest) {
interceptionID, err := uuid.Parse(req.GetId())
@@ -546,7 +547,7 @@ func TestRecordInterception(t *testing.T) {
Provider: "anthropic",
Model: "claude-4-opus",
StartedAt: timestamppb.Now(),
CorrelatingToolCallId: strPtr("call_abc"),
CorrelatingToolCallId: ptr.Ref("call_abc"),
},
setupMocks: func(t *testing.T, db *dbmock.MockStore, req *proto.RecordInterceptionRequest) {
selfID, err := uuid.Parse(req.GetId())
@@ -580,7 +581,7 @@ func TestRecordInterception(t *testing.T) {
Provider: "anthropic",
Model: "claude-4-opus",
StartedAt: timestamppb.Now(),
CorrelatingToolCallId: strPtr("call_abc"),
CorrelatingToolCallId: ptr.Ref("call_abc"),
},
setupMocks: func(t *testing.T, db *dbmock.MockStore, req *proto.RecordInterceptionRequest) {
selfID, err := uuid.Parse(req.GetId())
@@ -609,7 +610,7 @@ func TestRecordInterception(t *testing.T) {
Provider: "anthropic",
Model: "claude-4-opus",
StartedAt: timestamppb.Now(),
CorrelatingToolCallId: strPtr("call_abc"),
CorrelatingToolCallId: ptr.Ref("call_abc"),
},
setupMocks: func(t *testing.T, db *dbmock.MockStore, req *proto.RecordInterceptionRequest) {
selfID, err := uuid.Parse(req.GetId())
@@ -641,7 +642,7 @@ func TestRecordInterception(t *testing.T) {
Provider: "anthropic",
Model: "claude-4-opus",
StartedAt: timestamppb.Now(),
CorrelatingToolCallId: strPtr("call_orphan"),
CorrelatingToolCallId: ptr.Ref("call_orphan"),
},
setupMocks: func(t *testing.T, db *dbmock.MockStore, req *proto.RecordInterceptionRequest) {
selfID, err := uuid.Parse(req.GetId())
@@ -901,11 +902,11 @@ func TestRecordToolUsage(t *testing.T) {
InterceptionId: uuid.NewString(),
MsgId: "msg_123",
ToolCallId: "call_xyz",
ServerUrl: strPtr("https://api.example.com"),
ServerUrl: ptr.Ref("https://api.example.com"),
Tool: "read_file",
Input: `{"path": "/etc/hosts"}`,
Injected: false,
InvocationError: strPtr("permission denied"),
InvocationError: ptr.Ref("permission denied"),
Metadata: metadataProto,
CreatedAt: timestamppb.Now(),
},
@@ -1107,10 +1108,6 @@ func mustMarshalAny(t *testing.T, msg protobufproto.Message) *anypb.Any {
return v
}
func strPtr(s string) *string {
return &s
}
// logLine represents a parsed JSON log entry.
type logLine struct {
Msg string `json:"msg"`
@@ -1192,8 +1189,8 @@ func TestStructuredLogging(t *testing.T) {
Model: "claude-4-opus",
Metadata: metadataProto,
StartedAt: timestamppb.Now(),
CorrelatingToolCallId: strPtr(toolCallID),
ClientSessionId: strPtr(sessionID),
CorrelatingToolCallId: ptr.Ref(toolCallID),
ClientSessionId: ptr.Ref(sessionID),
})
return err
@@ -1344,11 +1341,11 @@ func TestStructuredLogging(t *testing.T) {
_, err := srv.RecordToolUsage(ctx, &proto.RecordToolUsageRequest{
InterceptionId: intcID.String(),
MsgId: "msg_123",
ServerUrl: strPtr("https://api.example.com"),
ServerUrl: ptr.Ref("https://api.example.com"),
Tool: "read_file",
Input: `{"path": "/etc/hosts"}`,
Injected: true,
InvocationError: strPtr("permission denied"),
InvocationError: ptr.Ref("permission denied"),
Metadata: metadataProto,
CreatedAt: timestamppb.Now(),
})
@@ -1487,7 +1484,7 @@ func TestInferredThreadsByToolCalls(t *testing.T) {
Provider: "anthropic",
Model: "claude-4-opus",
StartedAt: timestamppb.Now(),
CorrelatingToolCallId: strPtr("call_a"),
CorrelatingToolCallId: ptr.Ref("call_a"),
})
require.NoError(t, err)
@@ -1515,7 +1512,7 @@ func TestInferredThreadsByToolCalls(t *testing.T) {
Provider: "anthropic",
Model: "claude-4-opus",
StartedAt: timestamppb.Now(),
CorrelatingToolCallId: strPtr("call_b"),
CorrelatingToolCallId: ptr.Ref("call_b"),
})
require.NoError(t, err)
+5 -8
View File
@@ -16,6 +16,7 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/util/slice"
stringutil "github.com/coder/coder/v2/coderd/util/strings"
"github.com/coder/coder/v2/codersdk"
@@ -889,10 +890,12 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
}
if !param.Validation[0].MaxDisabled {
protoParam.ValidationMax = PtrInt32(param.Validation[0].Max)
// #nosec G115 - Safe conversion as the number is expected to be within int32 range
protoParam.ValidationMax = ptr.Ref(int32(param.Validation[0].Max))
}
if !param.Validation[0].MinDisabled {
protoParam.ValidationMin = PtrInt32(param.Validation[0].Min)
// #nosec G115 - Safe conversion as the number is expected to be within int32 range
protoParam.ValidationMin = ptr.Ref(int32(param.Validation[0].Min))
}
protoParam.ValidationMonotonic = param.Validation[0].Monotonic
}
@@ -1141,12 +1144,6 @@ func safeInt32Conversion(n int) int32 {
return int32(n)
}
func PtrInt32(number int) *int32 {
// #nosec G115 - Safe conversion as the number is expected to be within int32 range
n := int32(number)
return &n
}
// sortedResourcesByType collects all resources of the given type from the
// label map and returns them sorted by address. This ensures deterministic
// iteration order when processing resources that are stored in Go maps.
+13 -12
View File
@@ -20,6 +20,7 @@ import (
"cdr.dev/slog/v3"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/provisioner/terraform"
"github.com/coder/coder/v2/provisionersdk/proto"
@@ -699,22 +700,22 @@ func TestConvertResources(t *testing.T) {
Name: "number_example_max_zero",
Type: "number",
DefaultValue: "-2",
ValidationMin: terraform.PtrInt32(-3),
ValidationMax: terraform.PtrInt32(0),
ValidationMin: ptr.Ref(int32(-3)),
ValidationMax: ptr.Ref(int32(0)),
FormType: proto.ParameterFormType_INPUT,
}, {
Name: "number_example_min_max",
Type: "number",
DefaultValue: "4",
ValidationMin: terraform.PtrInt32(3),
ValidationMax: terraform.PtrInt32(6),
ValidationMin: ptr.Ref(int32(3)),
ValidationMax: ptr.Ref(int32(6)),
FormType: proto.ParameterFormType_INPUT,
}, {
Name: "number_example_min_zero",
Type: "number",
DefaultValue: "4",
ValidationMin: terraform.PtrInt32(0),
ValidationMax: terraform.PtrInt32(6),
ValidationMin: ptr.Ref(int32(0)),
ValidationMax: ptr.Ref(int32(6)),
FormType: proto.ParameterFormType_INPUT,
}, {
Name: "Sample",
@@ -783,34 +784,34 @@ func TestConvertResources(t *testing.T) {
Type: "number",
DefaultValue: "4",
ValidationMin: nil,
ValidationMax: terraform.PtrInt32(6),
ValidationMax: ptr.Ref(int32(6)),
FormType: proto.ParameterFormType_INPUT,
}, {
Name: "number_example_max_zero",
Type: "number",
DefaultValue: "-3",
ValidationMin: nil,
ValidationMax: terraform.PtrInt32(0),
ValidationMax: ptr.Ref(int32(0)),
FormType: proto.ParameterFormType_INPUT,
}, {
Name: "number_example_min",
Type: "number",
DefaultValue: "4",
ValidationMin: terraform.PtrInt32(3),
ValidationMin: ptr.Ref(int32(3)),
ValidationMax: nil,
FormType: proto.ParameterFormType_INPUT,
}, {
Name: "number_example_min_max",
Type: "number",
DefaultValue: "4",
ValidationMin: terraform.PtrInt32(3),
ValidationMax: terraform.PtrInt32(6),
ValidationMin: ptr.Ref(int32(3)),
ValidationMax: ptr.Ref(int32(6)),
FormType: proto.ParameterFormType_INPUT,
}, {
Name: "number_example_min_zero",
Type: "number",
DefaultValue: "4",
ValidationMin: terraform.PtrInt32(0),
ValidationMin: ptr.Ref(int32(0)),
ValidationMax: nil,
FormType: proto.ParameterFormType_INPUT,
}},
+3 -12
View File
@@ -5,12 +5,12 @@ import (
"flag"
"log"
"os"
"sort"
"strconv"
"strings"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/util/maps"
"github.com/coder/coder/v2/enterprise/audit"
"github.com/coder/coder/v2/scripts/atomicwrite"
)
@@ -96,7 +96,7 @@ func readAuditDoc() ([]byte, error) {
// Writes a markdown table of audit log resources to a buffer
func updateAuditDoc(doc []byte, auditableResourcesMap AuditableResourcesMap) ([]byte, error) {
// We must sort the resources to ensure table ordering
sortedResourceNames := sortKeys(auditableResourcesMap)
sortedResourceNames := maps.SortedKeys(auditableResourcesMap)
i := bytes.Index(doc, generatorPrefix)
if i < 0 {
@@ -135,7 +135,7 @@ func updateAuditDoc(doc []byte, auditableResourcesMap AuditableResourcesMap) ([]
_, _ = buffer.WriteString("|" + readableResourceName + "<br><i>" + auditActionsString + "</i>|<table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody>" + "|")
// We must sort the field names to ensure sub-table ordering
sortedFieldNames := sortKeys(auditableResourcesMap[resourceName])
sortedFieldNames := maps.SortedKeys(auditableResourcesMap[resourceName])
for _, fieldName := range sortedFieldNames {
isTracked := auditableResourcesMap[resourceName][fieldName]
@@ -153,12 +153,3 @@ func updateAuditDoc(doc []byte, auditableResourcesMap AuditableResourcesMap) ([]
func writeAuditDoc(doc []byte) error {
return atomicwrite.File(auditDocFile, doc)
}
func sortKeys[T any](stringMap map[string]T) []string {
var keyNames []string
for key := range stringMap {
keyNames = append(keyNames, key)
}
sort.Strings(keyNames)
return keyNames
}
+2 -10
View File
@@ -14,6 +14,7 @@ import (
"github.com/prometheus/common/expfmt"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/util/maps"
"github.com/coder/coder/v2/scripts/atomicwrite"
)
@@ -176,7 +177,7 @@ func updatePrometheusDoc(doc []byte, metricFamilies []*dto.MetricFamily) ([]byte
}
if len(labels) > 0 {
_, _ = buffer.WriteString(strings.Join(sortedKeys(labels), " "))
_, _ = buffer.WriteString(strings.Join(maps.SortedKeys(labels), " "))
}
_, _ = buffer.WriteString(" |\n")
@@ -190,12 +191,3 @@ func updatePrometheusDoc(doc []byte, metricFamilies []*dto.MetricFamily) ([]byte
func writePrometheusDoc(doc []byte) error {
return atomicwrite.File(prometheusDocFile, doc)
}
func sortedKeys(m map[string]struct{}) []string {
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
+3 -6
View File
@@ -20,6 +20,7 @@ import (
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
utilstrings "github.com/coder/coder/v2/coderd/util/strings"
"github.com/coder/coder/v2/codersdk"
)
@@ -131,15 +132,11 @@ func generateCountries() ([]byte, error) {
func pascalCaseName[T ~string](name T) string {
names := strings.Split(string(name), "_")
for i := range names {
names[i] = capitalize(names[i])
names[i] = utilstrings.Capitalize(names[i])
}
return strings.Join(names, "")
}
func capitalize(name string) string {
return strings.ToUpper(string(name[0])) + name[1:]
}
type Definition struct {
policy.PermissionDefinition
Type string
@@ -226,7 +223,7 @@ func generateRbacObjects(templateSource string) ([]byte, error) {
var errorList []error
var x int
tpl, err := template.New("object.gotmpl").Funcs(template.FuncMap{
"capitalize": capitalize,
"capitalize": utilstrings.Capitalize,
"pascalCaseName": pascalCaseName[string],
"actionsList": func() []ActionDetails {
return actionList