Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions internal/pkg/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"

"github.com/go-git/go-git/v5"
Expand Down Expand Up @@ -166,15 +167,28 @@ func Create(ctx context.Context, clients *shared.ClientFactory, log *logger.Logg
return appDirPath, nil
}

// getAppDirName will validate and return the app's directory name
// multiDashRe matches consecutive dashes.
var multiDashRe = regexp.MustCompile(`-{2,}`)

// nonAlphanumericRe matches any character that is not a lowercase letter, digit, or dash.
var nonAlphanumericRe = regexp.MustCompile(`[^a-z0-9-]+`)

// getAppDirName will validate and return the app's directory name in kebab-case
func getAppDirName(appName string) (string, error) {
if len(appName) <= 0 {
return "", fmt.Errorf("app name is required")
}

// trim whitespace
// Normalize to kebab-case: lowercase, replace non-alphanumeric with dashes, collapse, and trim
appName = strings.TrimSpace(appName)
appName = strings.ReplaceAll(appName, " ", "-")
appName = strings.ToLower(appName)
appName = nonAlphanumericRe.ReplaceAllString(appName, "-")
appName = multiDashRe.ReplaceAllString(appName, "-")
appName = strings.Trim(appName, "-")

if len(appName) == 0 {
return "", fmt.Errorf("app name must contain at least one alphanumeric character")
}

// name cannot be a reserved word
if goutils.Contains(reserved, appName, false) {
Expand Down
96 changes: 69 additions & 27 deletions internal/pkg/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,33 +38,75 @@ func TestCreate(t *testing.T) {
}

func TestGetProjectDirectoryName(t *testing.T) {
var appName string
var err error

// Test with empty name returns an error
appName, err = getAppDirName("")
assert.Error(t, err, "should return an error for empty name")
assert.Equal(t, "", appName)

// Test with app name
appName, err = getAppDirName("my-app")
assert.NoError(t, err, "should not return an error")
assert.Equal(t, "my-app", appName, "should return 'my-app'")

// Test with a dot in the app name
appName, err = getAppDirName(".my-app")
assert.NoError(t, err, "should not return an error")
assert.Equal(t, ".my-app", appName, "should return '.my-app'")

// Spaces replaced with hyphens
appName, err = getAppDirName("my cool app")
assert.NoError(t, err)
assert.Equal(t, "my-cool-app", appName)

// Leading/trailing spaces trimmed
appName, err = getAppDirName(" my-app ")
assert.NoError(t, err)
assert.Equal(t, "my-app", appName)
tests := map[string]struct {
input string
expected string
hasError bool
}{
"empty name returns error": {
input: "",
hasError: true,
},
"simple kebab-case name": {
input: "my-app",
expected: "my-app",
},
"spaces replaced with hyphens": {
input: "my cool app",
expected: "my-cool-app",
},
"leading and trailing spaces trimmed": {
input: " my-app ",
expected: "my-app",
},
"uppercase converted to lowercase": {
input: "My Slack App",
expected: "my-slack-app",
},
"mixed case normalized": {
input: "My-Slack-App",
expected: "my-slack-app",
},
"special characters replaced with dashes": {
input: "my_app!@#test",
expected: "my-app-test",
},
"consecutive special characters collapsed to single dash": {
input: "my---app",
expected: "my-app",
},
"leading and trailing special characters trimmed": {
input: "---my-app---",
expected: "my-app",
},
"dots converted to dashes": {
input: ".my-app",
expected: "my-app",
},
"only special characters returns error": {
input: "!!!",
hasError: true,
},
"numbers preserved": {
input: "app123",
expected: "app123",
},
"complex mixed input": {
input: " My Cool App! (v2) ",
expected: "my-cool-app-v2",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
result, err := getAppDirName(tc.input)
if tc.hasError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expected, result)
}
})
}
}

func TestGetAvailableDirectory(t *testing.T) {
Expand Down
Loading