Skip to content

Commit 5cdcd83

Browse files
committed
Improve TF schema attribute name generation
1 parent c4bb340 commit 5cdcd83

35 files changed

+235
-157
lines changed

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ require (
1919
github.com/hashicorp/terraform-plugin-mux v0.21.0
2020
github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1
2121
github.com/hashicorp/terraform-plugin-testing v1.13.3
22-
github.com/huandu/xstrings v1.5.0
2322
github.com/jarcoal/httpmock v1.4.1
2423
github.com/mongodb-forks/digest v1.1.0
2524
github.com/mongodb/atlas-sdk-go v1.0.1-0.20251103084024-4a19449c6541

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,6 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8
345345
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
346346
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
347347
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
348-
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
349-
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
350348
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
351349
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
352350
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=

internal/common/autogen/stringcase/string_case.go

Lines changed: 32 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,49 @@
11
package stringcase
22

33
import (
4-
"fmt"
54
"regexp"
65
"strings"
76
"unicode"
87
"unicode/utf8"
9-
10-
"github.com/huandu/xstrings"
11-
)
12-
13-
var (
14-
camelCase = regexp.MustCompile(`([a-z])[A-Z]`)
15-
unsupportedCharacters = regexp.MustCompile(`[^a-zA-Z0-9_]+`)
168
)
179

18-
type SnakeCaseString string
19-
20-
func (snake SnakeCaseString) SnakeCase() string {
21-
return string(snake)
22-
}
23-
24-
func (snake SnakeCaseString) PascalCase() string {
25-
return xstrings.ToPascalCase(string(snake))
26-
}
27-
28-
func (snake SnakeCaseString) CamelCase() string {
29-
return xstrings.ToCamelCase(string(snake))
30-
}
31-
32-
func (snake SnakeCaseString) LowerCaseNoUnderscore() string {
33-
return strings.ReplaceAll(string(snake), "_", "")
34-
}
10+
var unsupportedCharacters = regexp.MustCompile(`[^a-zA-Z0-9_]+`)
3511

36-
func FromCamelCase(input string) SnakeCaseString {
37-
if input == "" {
38-
return SnakeCaseString(input)
12+
// ToSnakeCase Multiple consecutive uppercase letters are treated as part of the same word except for the last one.
13+
// Example: "MongoDBMajorVersion" -> "mongo_db_major_version"
14+
func ToSnakeCase(str string) string {
15+
if str == "" {
16+
return str
3917
}
4018

41-
removedUnsupported := unsupportedCharacters.ReplaceAllString(input, "")
42-
43-
insertedUnderscores := camelCase.ReplaceAllStringFunc(removedUnsupported, func(s string) string {
44-
firstChar := s[0]
45-
restOfString := s[1:]
46-
return fmt.Sprintf("%c_%s", firstChar, strings.ToLower(restOfString))
47-
})
48-
49-
return SnakeCaseString(strings.ToLower(insertedUnderscores))
50-
}
19+
str = unsupportedCharacters.ReplaceAllString(str, "")
5120

52-
func ToCamelCase(str string) string {
53-
return xstrings.ToCamelCase(str)
54-
}
21+
builder := &strings.Builder{}
22+
runes := []rune(str)
23+
length := len(runes)
24+
25+
prevIsUpper := unicode.IsUpper(runes[0])
26+
builder.WriteRune(unicode.ToLower(runes[0]))
27+
28+
for i := 1; i < length; i++ {
29+
current := runes[i]
30+
currentIsUpper := unicode.IsUpper(runes[i])
31+
32+
// Write an underscore before uppercase letter if:
33+
// - Previous char was lowercase, so this is the first uppercase.
34+
// - Next char is lowercase, so this is the last uppercase in a sequence.
35+
if currentIsUpper {
36+
if !prevIsUpper || (i+1 != length && unicode.IsLower(runes[i+1])) {
37+
builder.WriteByte('_')
38+
}
39+
current = unicode.ToLower(current)
40+
}
41+
42+
builder.WriteRune(current)
43+
prevIsUpper = currentIsUpper
44+
}
5545

56-
func ToSnakeCase(str string) string {
57-
return xstrings.ToSnakeCase(str)
46+
return builder.String()
5847
}
5948

6049
func Capitalize(str string) string {

internal/common/autogen/stringcase/string_case_test.go

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestCapitalize(t *testing.T) {
4646
for _, tt := range tests {
4747
t.Run(tt.name, func(t *testing.T) {
4848
if actual := stringcase.Capitalize(tt.input); actual != tt.expected {
49-
t.Errorf("Capitalize() returned %v, expected %v", actual, tt.expected)
49+
t.Errorf("Capitalize(%q) returned %q, expected %q", tt.input, actual, tt.expected)
5050
}
5151
})
5252
}
@@ -92,7 +92,78 @@ func TestUncapitalize(t *testing.T) {
9292
for _, tt := range tests {
9393
t.Run(tt.name, func(t *testing.T) {
9494
if actual := stringcase.Uncapitalize(tt.input); actual != tt.expected {
95-
t.Errorf("Uncapitalize() returned %v, expected %v", actual, tt.expected)
95+
t.Errorf("Uncapitalize(%q) returned %q, expected %q", tt.input, actual, tt.expected)
96+
}
97+
})
98+
}
99+
}
100+
101+
func TestToSnakeCase(t *testing.T) {
102+
tests := []struct {
103+
name string
104+
input string
105+
expected string
106+
}{
107+
{
108+
name: "Empty string",
109+
input: "",
110+
expected: "",
111+
},
112+
{
113+
name: "Single letter",
114+
input: "a",
115+
expected: "a",
116+
},
117+
{
118+
name: "Single uppercase letter",
119+
input: "A",
120+
expected: "a",
121+
},
122+
{
123+
name: "Simple camelCase",
124+
input: "camelCase",
125+
expected: "camel_case",
126+
},
127+
{
128+
name: "Simple PascalCase",
129+
input: "PascalCase",
130+
expected: "pascal_case",
131+
},
132+
{
133+
name: "All lowercase",
134+
input: "word",
135+
expected: "word",
136+
},
137+
{
138+
name: "All uppercase",
139+
input: "WORD",
140+
expected: "word",
141+
},
142+
{
143+
name: "Consecutive uppercase at start, middle and end",
144+
input: "THISIsANExampleWORD",
145+
expected: "this_is_an_example_word",
146+
},
147+
{
148+
name: "Numbers do not split words",
149+
input: "Example123Word456WithNUMBERS789",
150+
expected: "example123_word456_with_numbers789",
151+
},
152+
{
153+
name: "Already snake_case",
154+
input: "already_snake_case",
155+
expected: "already_snake_case",
156+
},
157+
{
158+
name: "Unsupported characters are removed",
159+
input: "Example#!Unsup-.ported%&Chars",
160+
expected: "example_unsupported_chars",
161+
},
162+
}
163+
for _, tt := range tests {
164+
t.Run(tt.name, func(t *testing.T) {
165+
if actual := stringcase.ToSnakeCase(tt.input); actual != tt.expected {
166+
t.Errorf("ToSnakeCase(%q) returned %q, expected %q", tt.input, actual, tt.expected)
96167
}
97168
})
98169
}

internal/serviceapi/clusterapi/resource_schema.go

Lines changed: 6 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/serviceapi/clusterapi/resource_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestAccClusterAPI_basic(t *testing.T) {
4343
ImportStateVerifyIdentifierAttribute: "name",
4444
ImportStateVerifyIgnore: []string{
4545
"retain_backups_enabled", // This field is TF specific and not returned by Atlas, so Import can't fill it in.
46-
"mongo_dbmajor_version", // Risks plan change of 8 --> 8.0 (always normalized to `major.minor`)
46+
"mongo_db_major_version", // Risks plan change of 8 --> 8.0 (always normalized to `major.minor`)
4747
"state_name", // Cluster state can change from IDLE to UPDATING and risks making the test flaky
4848
"delete_on_create_timeout", // This field is TF specific and not returned by Atlas, so Import can't fill it in.
4949
},

internal/serviceapi/databaseuserapi/resource_schema.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/codegen/codespec/api_to_provider_spec_mapper.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"log"
77
"strings"
88

9-
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/autogen/stringcase"
109
high "github.com/pb33f/libopenapi/datamodel/high/v3"
1110
low "github.com/pb33f/libopenapi/datamodel/low/v3"
1211
"github.com/pb33f/libopenapi/orderedmap"
@@ -47,12 +46,12 @@ func ToCodeSpecModel(atlasAdminAPISpecFilePath, configPath string, resourceName
4746
for name, resourceConfig := range resourceConfigsToIterate {
4847
log.Printf("[INFO] Generating resource model: %s", name)
4948
// find resource operations, schemas, etc from OAS
50-
oasResource, err := getAPISpecResource(&apiSpec.Model, &resourceConfig, stringcase.SnakeCaseString(name))
49+
oasResource, err := getAPISpecResource(&apiSpec.Model, &resourceConfig, name)
5150
if err != nil {
5251
return nil, fmt.Errorf("unable to get APISpecResource schema: %v", err)
5352
}
5453
// map OAS resource model to CodeSpecModel
55-
resource, err := apiSpecResourceToCodeSpecModel(oasResource, &resourceConfig, stringcase.SnakeCaseString(name))
54+
resource, err := apiSpecResourceToCodeSpecModel(oasResource, &resourceConfig, name)
5655
if err != nil {
5756
return nil, fmt.Errorf("unable to map to code spec model for %s: %w", name, err)
5857
}
@@ -81,7 +80,7 @@ func validateRequiredOperations(resourceConfigs map[string]config.Resource) erro
8180
return nil
8281
}
8382

84-
func apiSpecResourceToCodeSpecModel(oasResource APISpecResource, resourceConfig *config.Resource, name stringcase.SnakeCaseString) (*Resource, error) {
83+
func apiSpecResourceToCodeSpecModel(oasResource APISpecResource, resourceConfig *config.Resource, name string) (*Resource, error) {
8584
createOp := oasResource.CreateOp
8685
updateOp := oasResource.UpdateOp
8786
readOp := oasResource.ReadOp
@@ -117,9 +116,10 @@ func apiSpecResourceToCodeSpecModel(oasResource APISpecResource, resourceConfig
117116
operations.VersionHeader = getLatestVersionFromAPISpec(readOp)
118117
}
119118
resource := &Resource{
120-
Name: name,
121-
Schema: schema,
122-
Operations: operations,
119+
Name: name,
120+
PackageName: strings.ReplaceAll(name, "_", ""),
121+
Schema: schema,
122+
Operations: operations,
123123
}
124124

125125
applyTransformationsWithConfigOpts(resourceConfig, resource)
@@ -234,7 +234,7 @@ func opResponseToAttributes(op *high.Operation) Attributes {
234234
return responseAttributes
235235
}
236236

237-
func getAPISpecResource(spec *high.Document, resourceConfig *config.Resource, name stringcase.SnakeCaseString) (APISpecResource, error) {
237+
func getAPISpecResource(spec *high.Document, resourceConfig *config.Resource, name string) (APISpecResource, error) {
238238
var errResult error
239239
var resourceDeprecationMsg *string
240240

0 commit comments

Comments
 (0)