// Package helpers — конвертация имён, форматирование, plan-модификаторы. package helpers import ( "fmt" "go/format" "os" "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. func ToSnake(s string) string { var out []rune for i, r := range s { if i > 0 && r >= 'A' && r <= '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-код. При ошибке пишет warning в stderr. func FormatSourceOrWarn(filePath string, src []byte) []byte { formatted, err := format.Source(src) if err != nil { fmt.Fprintf(os.Stderr, "WARNING: gofmt failed for %s: %v — writing unformatted code\n", filePath, err) return src } return formatted } // 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, ", ")) }