cli/azd/internal/scaffold/funcs.go (218 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package scaffold import ( "encoding/json" "fmt" "net/url" "regexp" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" ) // BicepName returns a name suitable for use as a bicep variable name. // // The name is converted to camel case, with treatment for underscore or dash separators, // and all non-alphanumeric characters are removed. func BicepName(name string) string { sb := strings.Builder{} separatorStart := -1 allUpper := isAllUpperCase(name) for i := range name { switch name[i] { case '-', '_': if separatorStart == -1 { // track first occurrence of consecutive separators separatorStart = i } default: if !isAsciiAlphaNumeric(name[i]) { continue } var char byte if separatorStart == 0 || i == 0 { // we are at the start char = lowerCase(name[i]) separatorStart = -1 } else if separatorStart > 0 { // end of separator, and it's not the first one char = upperCase(name[i]) separatorStart = -1 } else if allUpper { // when the input is all uppercase, convert to lowercase char = lowerCase(name[i]) } else { char = name[i] } sb.WriteByte(char) } } return sb.String() } // BicepNameInfix is like BicepName, except that the first character is upper-cased for infix use. func BicepNameInfix(name string) string { bicepName := BicepName(name) return capitalizeFirst(bicepName) } func capitalizeFirst(s string) string { if s == "" { return "" } return strings.ToUpper(s[:1]) + s[1:] } func RemoveDotAndDash(name string) string { return strings.ReplaceAll(strings.ReplaceAll(name, ".", ""), "-", "") } // AlphaSnakeUpper returns a name in upper-snake case alphanumeric name separated only by underscores. // // Non-alphanumeric characters are discarded, while consecutive separators ('-', '_', and '.') are treated // as a single underscore separator. func AlphaSnakeUpper(name string) string { sb := strings.Builder{} separatorStart := -1 for i := range name { switch name[i] { case '-', '_', '.': if separatorStart == -1 { // track first occurrence of consecutive separators separatorStart = i } default: if !isAsciiAlphaNumeric(name[i]) { continue } if separatorStart != -1 { if separatorStart != 0 { // don't write prefix separator sb.WriteByte('_') } separatorStart = -1 } sb.WriteByte(upperCase(name[i])) } } return sb.String() } // AlphaSnakeUpperFromCasing transforms a camel case or pascal case name into an upper-snake case name. func AlphaSnakeUpperFromCasing(name string) string { result := strings.Builder{} for i := range name { c := name[i] if 'A' <= c && c <= 'Z' { if i > 0 && i != len(name)-1 { result.WriteRune('_') } } result.WriteByte(upperCase(c)) } return result.String() } func isAllUpperCase(c string) bool { for i := range c { if 'a' <= c[i] && c[i] <= 'z' { return false } } return true } func isAsciiAlphaNumeric(c byte) bool { return ('0' <= c && c <= '9') || ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') } func upperCase(r byte) byte { if 'a' <= r && r <= 'z' { r -= 'a' - 'A' } return r } func lowerCase(r byte) byte { if 'A' <= r && r <= 'Z' { r += 'a' - 'A' } return r } // 32 characters are allowed for the Container App name. See // https://learn.microsoft.com/azure/azure-resource-manager/management/resource-name-rules#microsoftapp // // We allow 2 additional characters for wiggle-room. We've seen failures when container app name is exactly at 32. const containerAppNameMaxLen = 30 func containerAppName(name string, maxLen int) string { if len(name) > maxLen { name = name[:maxLen] } // trim to allowed characters: // - only alphanumeric and '-' // - no repeated '-' // - no '-' as the first or last character sb := strings.Builder{} i := 0 for i < len(name) { if isAsciiAlphaNumeric(name[i]) { sb.WriteByte(lowerCase(name[i])) } else if name[i] == '-' || name[i] == '_' { j := i + 1 for j < len(name) && (name[j] == '-' || name[i] == '_') { // find consecutive matches j++ } if i != 0 && j != len(name) { // only write '-' if not first or last character sb.WriteByte('-') } i = j continue } i++ } return sb.String() } // ContainerAppName returns a suitable name a container app resource. // // The name is treated to only contain alphanumeric and dash characters, with no repeated dashes, and no dashes // as the first or last character. func ContainerAppName(name string) string { return containerAppName(name, containerAppNameMaxLen) } // ContainerAppSecretName returns a suitable name a container app secret name. // // The name is treated to only contain lowercase alphanumeric and dash characters, and must start and end with an // alphanumeric character func ContainerAppSecretName(name string) string { return strings.ReplaceAll(strings.ToLower(name), "_", "-") } // camelCaseRegex is a regular expression used to match camel case patterns. // It matches a lowercase letter or digit followed by an uppercase letter. var camelCaseRegex = regexp.MustCompile(`([a-z0-9])([A-Z])`) // EnvFormat takes an input parameter like `fooParam` which is expected to be in camel case and returns it in // upper snake case with env var template, like `${AZURE_FOO_PARAM}`. func EnvFormat(src string) string { snake := strings.ReplaceAll(strings.ToUpper(camelCaseRegex.ReplaceAllString(src, "${1}_${2}")), "-", "_") return fmt.Sprintf("${AZURE_%s}", snake) } func HasACA(services []ServiceSpec) bool { return hasHostType(services, ContainerAppKind) } func HasAppService(services []ServiceSpec) bool { return hasHostType(services, AppServiceKind) } func IsACA(host HostKind) bool { return host == ContainerAppKind } func IsAppService(host HostKind) bool { return host == AppServiceKind } func hasHostType(services []ServiceSpec, host HostKind) bool { for _, service := range services { if service.Host == host { return true } } return false } // Formats a parameter value for use in a bicep file. // If the value is a string, it is quoted inline with no indentation. // Otherwise, the value is marshaled with indentation specified by prefix and indent. func FormatParameter(prefix string, indent string, value any) (string, error) { if valueStr, ok := value.(string); ok { return fmt.Sprintf("\"%s\"", valueStr), nil } val, err := json.MarshalIndent(value, prefix, indent) if err != nil { return "", err } return string(val), nil } func hostFromEndpoint(endpoint string) (string, error) { urlEndpoint, err := url.Parse(endpoint) if err != nil { return "", fmt.Errorf("invalid endpoint: %s", endpoint) } return urlEndpoint.Hostname(), nil } func aiProjectConnectionString(resourceId string, projectUrl string) (string, error) { hostName, err := hostFromEndpoint(projectUrl) if err != nil { return "", fmt.Errorf("failed to parse project URL: %w", err) } resId, err := arm.ParseResourceID(resourceId) if err != nil { return "", nil } return fmt.Sprintf("%s;%s;%s;%s", hostName, resId.SubscriptionID, resId.ResourceGroupName, resId.Name), nil } func emitAiProjectConnectionString(resourceIdVar string, projectUrlVar string) (string, error) { return fmt.Sprintf( "${split(%s, '/')[2]};${split(%s, '/')[2]};${split(%s, '/')[4]};${split(%s, '/')[8]}", projectUrlVar, resourceIdVar, resourceIdVar, resourceIdVar), nil } func emitHostFromEndpoint(endpointVar string) (string, error) { // example: https://{your-namespace}.servicebus.windows.net:443 return fmt.Sprintf("split(split(%s, '//')[1], ':')[0]", endpointVar), nil } func bicepFuncCall(funcName string) func(name string) string { // example: toLower(foo) return func(name string) string { return fmt.Sprintf("%s(%s)", funcName, name) } } func bicepFuncCallThree(funcName string) func(a string, b string, c string) string { // example: replace(foo, bar, baz) return func(a string, b string, c string) string { return fmt.Sprintf("%s(%s, %s, %s)", funcName, a, b, c) } }