334 lines
11 KiB
Go
334 lines
11 KiB
Go
// Package helpers — конвертация имён, форматирование, plan-модификаторы.
|
||
package helpers
|
||
|
||
import (
|
||
"fmt"
|
||
"go/format"
|
||
"strings"
|
||
"unicode"
|
||
|
||
"resource-generator/internal/types"
|
||
)
|
||
|
||
// ToCamel конвертирует snake_case → CamelCase.
|
||
func ToCamel(s string) string {
|
||
parts := strings.FieldsFunc(s, func(r rune) bool { return r == '_' || r == '-' })
|
||
for i, p := range parts {
|
||
if len(p) == 0 {
|
||
continue
|
||
}
|
||
parts[i] = strings.ToUpper(p[:1]) + p[1:]
|
||
}
|
||
out := strings.Join(parts, "")
|
||
return EnsureGoIdent(out)
|
||
}
|
||
|
||
// ToSnake конвертирует CamelCase → snake_case.
|
||
// Аббревиатуры (CPU, TTL, CA, API, VIP) сохраняются слитно: resourceCPU → resource_cpu.
|
||
func ToSnake(s string) string {
|
||
var out []rune
|
||
runes := []rune(s)
|
||
for i, r := range runes {
|
||
if i > 0 && r >= 'A' && r <= 'Z' {
|
||
// Вставляем _ только если предыдущий символ — строчная буква
|
||
// (переход из слова в аббревиатуру: resourceCPU → resource_cpu)
|
||
// ИЛИ если следующий символ — строчная буква
|
||
// (переход из аббревиатуры в слово: HTTPSProxy → https_proxy)
|
||
prev := runes[i-1]
|
||
if prev >= 'a' && prev <= 'z' {
|
||
out = append(out, '_')
|
||
} else if i+1 < len(runes) && runes[i+1] >= 'a' && runes[i+1] <= 'z' {
|
||
out = append(out, '_')
|
||
}
|
||
}
|
||
out = append(out, rune(strings.ToLower(string(r))[0]))
|
||
}
|
||
return string(out)
|
||
}
|
||
|
||
// ParamType возвращает Go-тип для параметра (types.Bool/Int64/String).
|
||
func ParamType(p types.Param) string {
|
||
switch strings.ToLower(p.Type) {
|
||
case "bool":
|
||
return "types.Bool"
|
||
case "int", "int64", "number":
|
||
return "types.Int64"
|
||
default:
|
||
return "types.String"
|
||
}
|
||
}
|
||
|
||
// ParamDefaultExpr возвращает выражение значения по умолчанию.
|
||
func ParamDefaultExpr(p types.Param) string {
|
||
if p.Default == "" {
|
||
return ""
|
||
}
|
||
switch strings.ToLower(p.Type) {
|
||
case "bool":
|
||
val := strings.ToLower(p.Default)
|
||
return fmt.Sprintf("booldefault.StaticBool(%s)", val)
|
||
case "int", "int64", "number":
|
||
return fmt.Sprintf("int64default.StaticInt64(%s)", p.Default)
|
||
default:
|
||
return fmt.Sprintf("stringdefault.StaticString(%q)", p.Default)
|
||
}
|
||
}
|
||
|
||
// ParamFormat возвращает вызов Format* для форматирования значения параметра.
|
||
func ParamFormat(p types.Param, varName string) string {
|
||
switch strings.ToLower(p.Type) {
|
||
case "bool":
|
||
return fmt.Sprintf("resources_core.FormatBool(%s)", varName)
|
||
case "int", "int64", "number":
|
||
return fmt.Sprintf("resources_core.FormatInt64(%s)", varName)
|
||
default:
|
||
return fmt.Sprintf("resources_core.FormatString(%s)", varName)
|
||
}
|
||
}
|
||
|
||
// ParamParse возвращает имя Parse*-функции для параметра.
|
||
func ParamParse(p types.Param) string {
|
||
switch strings.ToLower(p.Type) {
|
||
case "bool":
|
||
return "resources_core.ParseBool"
|
||
case "int", "int64", "number":
|
||
return "resources_core.ParseInt64"
|
||
default:
|
||
return "resources_core.ParseString"
|
||
}
|
||
}
|
||
|
||
// ParamDescription формирует MarkdownDescription для параметра.
|
||
func ParamDescription(p types.Param) string {
|
||
var desc string
|
||
if strings.TrimSpace(p.Descr) != "" {
|
||
desc = strings.TrimSpace(p.Descr)
|
||
} else if strings.TrimSpace(p.Man) != "" {
|
||
desc = strings.TrimSpace(p.Man)
|
||
} else if p.RefSvcId > 0 {
|
||
desc = fmt.Sprintf("UI display_name for service_id=%d (mapped to UUID in core)", p.RefSvcId)
|
||
}
|
||
if p.CreateOnly {
|
||
if desc == "" {
|
||
desc = "Не изменяется после создания."
|
||
} else {
|
||
desc = desc + " Не изменяется после создания."
|
||
}
|
||
}
|
||
if p.ForceNew {
|
||
if desc == "" {
|
||
desc = "Изменение требует пересоздания."
|
||
} else {
|
||
desc = desc + " Изменение требует пересоздания."
|
||
}
|
||
}
|
||
if desc == "" {
|
||
return ""
|
||
}
|
||
return fmt.Sprintf("%q", desc)
|
||
}
|
||
|
||
// OutputParamType возвращает Go-тип для выходного параметра (types.Map/List/String).
|
||
func OutputParamType(p types.OutputParam) string {
|
||
switch strings.ToLower(p.Type) {
|
||
case "map":
|
||
return "types.Map"
|
||
case "list":
|
||
return "types.List"
|
||
default:
|
||
return "types.String"
|
||
}
|
||
}
|
||
|
||
// OutputParamIsMap возвращает true, если выходной параметр — map.
|
||
func OutputParamIsMap(p types.OutputParam) bool {
|
||
return strings.EqualFold(strings.TrimSpace(p.Type), "map")
|
||
}
|
||
|
||
// OutputParamIsList возвращает true, если выходной параметр — list.
|
||
func OutputParamIsList(p types.OutputParam) bool {
|
||
return strings.EqualFold(strings.TrimSpace(p.Type), "list")
|
||
}
|
||
|
||
// OutputParamSensitive возвращает true, если параметр секретный.
|
||
func OutputParamSensitive(p types.OutputParam) bool {
|
||
return p.Sensitive
|
||
}
|
||
|
||
// EnsureGoIdent проверяет, что строка начинается с буквы или _.
|
||
func EnsureGoIdent(s string) string {
|
||
if s == "" {
|
||
return "R"
|
||
}
|
||
first := rune(s[0])
|
||
if unicode.IsLetter(first) || first == '_' {
|
||
return s
|
||
}
|
||
return "R" + s
|
||
}
|
||
|
||
// FormatSourceOrWarn форматирует Go-код и возвращает ошибку при сбое gofmt.
|
||
func FormatSourceOrWarn(filePath string, src []byte) ([]byte, error) {
|
||
formatted, err := format.Source(src)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("gofmt failed for %s: %w", filePath, err)
|
||
}
|
||
return formatted, nil
|
||
}
|
||
|
||
// PlanModifierType возвращает суффикс типа план-модификатора (Bool/Int64/String).
|
||
func PlanModifierType(p types.Param) string {
|
||
switch strings.ToLower(strings.TrimSpace(p.Type)) {
|
||
case "bool":
|
||
return "Bool"
|
||
case "int", "int64", "number":
|
||
return "Int64"
|
||
default:
|
||
return "String"
|
||
}
|
||
}
|
||
|
||
// PlanModifierExpr возвращает выражение план-модификатора RequiresReplace.
|
||
func PlanModifierExpr(p types.Param) string {
|
||
switch strings.ToLower(strings.TrimSpace(p.Type)) {
|
||
case "bool":
|
||
return "boolplanmodifier.RequiresReplace()"
|
||
case "int", "int64", "number":
|
||
return "int64planmodifier.RequiresReplace()"
|
||
default:
|
||
return "stringplanmodifier.RequiresReplace()"
|
||
}
|
||
}
|
||
|
||
// ─── Nested (map-fixed) helpers ──────────────────────────────────────────────
|
||
|
||
// IsNested возвращает true для параметров с SubParams (map-fixed).
|
||
// array-map-fixed исключается — требует отдельной обработки.
|
||
func IsNested(p types.Param) bool {
|
||
return p.HasSubParams && !IsNestedList(p)
|
||
}
|
||
|
||
// IsNestedList возвращает true для array-map-fixed (список объектов).
|
||
func IsNestedList(p types.Param) bool {
|
||
return p.HasSubParams && strings.Contains(strings.ToLower(p.Type), "array")
|
||
}
|
||
|
||
// NestedModelName возвращает имя вложенного Go-struct для ресурса и параметра.
|
||
// Например: NestedModelName("postgres", "cluster_configuration") → "PostgresClusterConfigurationModel"
|
||
func NestedModelName(resourceName, paramCode string) string {
|
||
return ToCamel(resourceName) + ToCamel(paramCode) + "Model"
|
||
}
|
||
|
||
// NestedTfType возвращает Go-тип для вложенного поля в Model:
|
||
// *PostgresClusterConfigurationModel (SingleNestedAttribute) или
|
||
// []PostgresClusterConfigurationModel (ListNestedAttribute — array-map-fixed).
|
||
func NestedTfType(p types.Param, resourceName string) string {
|
||
name := NestedModelName(resourceName, p.Code)
|
||
if IsNestedList(p) {
|
||
return "[]" + name
|
||
}
|
||
return "*" + name
|
||
}
|
||
|
||
// NestedSchemaType возвращает тип схемы для nested параметра:
|
||
// "SingleNested" (map-fixed) или "ListNested" (array-map-fixed).
|
||
func NestedSchemaType(p types.Param) string {
|
||
if IsNestedList(p) {
|
||
return "ListNested"
|
||
}
|
||
return "SingleNested"
|
||
}
|
||
|
||
// NestedSchemaBlock генерирует открывающую строку для nested-атрибута в Schema.
|
||
func NestedSchemaBlock(p types.Param) string {
|
||
t := NestedSchemaType(p)
|
||
switch t {
|
||
case "ListNested":
|
||
return fmt.Sprintf("schema.%sAttribute{Optional: true,\n\t\t\tNestedObject: schema.NestedAttributeObject{Attributes: map[string]schema.Attribute{", t)
|
||
default:
|
||
req := "Required: true"
|
||
if p.Default != "" {
|
||
req = "Optional: true"
|
||
}
|
||
return fmt.Sprintf("schema.%sAttribute{%s,\n\t\t\tAttributes: map[string]schema.Attribute{", t, req)
|
||
}
|
||
}
|
||
|
||
// NestedSchemaEnd возвращает закрывающую строку для nested-атрибута.
|
||
// Закрывает Attributes map и сам атрибут: `}},` для SingleNested, `}}},` для ListNested.
|
||
func NestedSchemaEnd(p types.Param) string {
|
||
t := NestedSchemaType(p)
|
||
if t == "ListNested" {
|
||
return "}}},"
|
||
}
|
||
return "}},"
|
||
}
|
||
|
||
// SubSchemaType возвращает тип схемы для подпараметра.
|
||
func SubSchemaType(p types.Param) string {
|
||
switch strings.ToLower(p.Type) {
|
||
case "bool":
|
||
return "Bool"
|
||
case "int64", "int", "number":
|
||
return "Int64"
|
||
default:
|
||
return "String"
|
||
}
|
||
}
|
||
|
||
// SubDefaultExpr возвращает выражение default для подпараметра.
|
||
// ⚠️ Только Optional+Computed (Required несовместим с Default во фреймворке).
|
||
func SubDefaultExpr(p types.Param) string {
|
||
if p.Default == "" {
|
||
return ""
|
||
}
|
||
switch strings.ToLower(p.Type) {
|
||
case "bool":
|
||
return fmt.Sprintf("booldefault.StaticBool(%s)", strings.ToLower(p.Default))
|
||
case "int64", "int", "number":
|
||
return fmt.Sprintf("int64default.StaticInt64(%s)", p.Default)
|
||
default:
|
||
return fmt.Sprintf("stringdefault.StaticString(%q)", p.Default)
|
||
}
|
||
}
|
||
|
||
// SubParamFormat возвращает JSON-ключ:значение для форматирования подпараметра.
|
||
func SubParamFormat(p types.Param, varPrefix string) string {
|
||
varName := varPrefix + "." + ToCamel(p.Code)
|
||
switch strings.ToLower(p.Type) {
|
||
case "bool":
|
||
return fmt.Sprintf(`"%s": `+varName+`.ValueBool()`, p.Code)
|
||
case "int64", "int", "number":
|
||
return fmt.Sprintf(`"%s": %s `+varName+`.ValueInt64()`, p.Code, "%s")
|
||
default:
|
||
return fmt.Sprintf(`"%s": `+varName+`.ValueString()`, p.Code)
|
||
}
|
||
}
|
||
|
||
// NestedJSONExpr возвращает выражение для сборки JSON из nested struct через resources_core.BuildJSON.
|
||
// varPrefix — префикс переменной (например "data" или "plan").
|
||
// Генерирует доступ: data.ClusterConfiguration.Cpu, data.ClusterConfiguration.Memory, ...
|
||
func NestedJSONExpr(p types.Param, varPrefix string) string {
|
||
varName := varPrefix + "." + ToCamel(p.Code)
|
||
pairs := make([]string, 0, len(p.SubParams))
|
||
for _, sp := range p.SubParams {
|
||
field := varName + "." + ToCamel(sp.Code)
|
||
var valExpr string
|
||
// IsJson проверяем до Type — для json полей Type="string", но сериализация другая.
|
||
if sp.IsJson {
|
||
valExpr = fmt.Sprintf(`%s.ValueString()`, field)
|
||
} else {
|
||
switch strings.ToLower(sp.Type) {
|
||
case "bool":
|
||
valExpr = fmt.Sprintf(`fmt.Sprintf("%%v", %s.ValueBool())`, field)
|
||
case "int64", "int", "number":
|
||
valExpr = fmt.Sprintf(`fmt.Sprintf("%%d", %s.ValueInt64())`, field)
|
||
default:
|
||
valExpr = fmt.Sprintf(`fmt.Sprintf("\"%%s\"", %s.ValueString())`, field)
|
||
}
|
||
}
|
||
pairs = append(pairs, fmt.Sprintf(`"%s": %s`, sp.Code, valExpr))
|
||
}
|
||
return fmt.Sprintf("resources_core.BuildJSON(map[string]string{%s})", strings.Join(pairs, ", "))
|
||
}
|