resource-generator: SubParams nested fix (Соннет) — AlignParamTypes recursive, HasNestedParams/NeedsStrings skip array-map-fixed, bump 5.0.74

This commit is contained in:
“Naeel”
2026-07-16 15:43:01 +04:00
parent e097d0dae9
commit ae64ee5931
214 changed files with 89122 additions and 35 deletions
@@ -193,14 +193,15 @@ func PlanModifierExpr(p types.Param) string {
// ─── Nested (map-fixed) helpers ──────────────────────────────────────────────
// IsNested возвращает true для параметров с SubParams (map-fixed/array-map-fixed).
// IsNested возвращает true для параметров с SubParams (map-fixed).
// array-map-fixed исключается — требует отдельной обработки.
func IsNested(p types.Param) bool {
return p.IsNested
return p.HasSubParams && !IsNestedList(p)
}
// IsNestedList возвращает true для array-map-fixed (список объектов).
func IsNestedList(p types.Param) bool {
return p.IsNested && strings.Contains(strings.ToLower(p.Type), "array")
return p.HasSubParams && strings.Contains(strings.ToLower(p.Type), "array")
}
// NestedModelName возвращает имя вложенного Go-struct для ресурса и параметра.
@@ -245,12 +246,13 @@ func NestedSchemaBlock(p types.Param) string {
}
// NestedSchemaEnd возвращает закрывающую строку для nested-атрибута.
// Закрывает Attributes map и сам атрибут: `}},` для SingleNested, `}}},` для ListNested.
func NestedSchemaEnd(p types.Param) string {
t := NestedSchemaType(p)
if t == "ListNested" {
return "}},"
return "}}},"
}
return "},"
return "}},"
}
// SubSchemaType возвращает тип схемы для подпараметра.
@@ -295,23 +297,20 @@ func SubParamFormat(p types.Param, varPrefix string) string {
}
// NestedJSONExpr возвращает выражение для сборки JSON из nested struct через resources_core.BuildJSON.
// BuildJSON принимает map[string]string где ключи — имена полей,
// значения — уже отформатированные строки (числа без кавычек, строки с кавычками).
// Пример: resources_core.BuildJSON(map[string]string{"cpu": "500", "name": "\"hello\""}) → {"cpu":500,"name":"hello"}
func NestedJSONExpr(p types.Param, varName string) string {
// 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
switch strings.ToLower(sp.Type) {
case "bool":
// BuildJSON ожидает "true"/"false" (без кавычек)
valExpr = fmt.Sprintf(`fmt.Sprintf("%%v", %s.ValueBool())`, field)
case "int64", "int", "number":
// BuildJSON ожидает "500" (без кавычек)
valExpr = fmt.Sprintf(`fmt.Sprintf("%%d", %s.ValueInt64())`, field)
default:
// BuildJSON ожидает "\"hello\"" (с кавычками)
valExpr = fmt.Sprintf(`fmt.Sprintf("\"%%s\"", %s.ValueString())`, field)
}
pairs = append(pairs, fmt.Sprintf(`"%s": %s`, sp.Code, valExpr))
@@ -104,8 +104,13 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
HasDomainParam: hasDomainParam,
}
params.Analyze(&gr.UsesBool, &gr.UsesInt64, &gr.UsesString, &gr.HasDefaults, &gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams)
// Анализируем nested sub-params для default-импортов
if params.AnalyzeNestedDefaults(&gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams) {
gr.NeedsFmtImport = true
}
gr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(gr.SchemaParams)
gr.NeedsStringsImport = params.HasRestoreCasingParams(gr.SchemaParams) || params.AnalyzeNeedsStrings(gr.ModifyParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyRequiredParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyParams)
gr.NeedsFmtImport = params.HasNestedParams(gr.SchemaParams)
// --- Action processing (ДО append, чтобы HasRedeploy попал в слайс) ---
for _, op := range spec.Operations {
@@ -239,18 +244,18 @@ func ConvertParams(params []types.ParamSpec) []types.Param {
}
}
out = append(out, types.Param{
ID: p.ID,
Code: p.Code,
Type: typeName,
Required: p.Required,
Default: defVal,
RefSvcId: ref,
Descr: strings.TrimSpace(p.Descr),
Man: strings.TrimSpace(p.Man),
Sensitive: p.IsSensitive,
IsJson: strings.EqualFold(strings.TrimSpace(p.DataType), "json"),
IsNested: isNested,
SubParams: subParams,
ID: p.ID,
Code: p.Code,
Type: typeName,
Required: p.Required,
Default: defVal,
RefSvcId: ref,
Descr: strings.TrimSpace(p.Descr),
Man: strings.TrimSpace(p.Man),
Sensitive: p.IsSensitive,
IsJson: strings.EqualFold(strings.TrimSpace(p.DataType), "json"),
HasSubParams: isNested,
SubParams: subParams,
})
}
return out
@@ -66,6 +66,21 @@ func AlignParamTypes(params []types.Param, schema []types.Param) []types.Param {
}
}
// Рекурсивно выравниваем SubParams (например cpu может быть string в modify, но int64 в create).
schemaByCode := make(map[string]types.Param, len(schema))
for _, p := range schema {
schemaByCode[strings.ToLower(strings.TrimSpace(p.Code))] = p
}
for i, p := range params {
if !p.HasSubParams {
continue
}
key := strings.ToLower(strings.TrimSpace(p.Code))
if sp, ok := schemaByCode[key]; ok && len(sp.SubParams) > 0 {
params[i].SubParams = AlignParamTypes(p.SubParams, sp.SubParams)
}
}
return params
}
@@ -231,6 +246,42 @@ func AnalyzeJsonPlanMod(params []types.Param) bool {
return false
}
// HasNestedParams возвращает true, если есть nested (только map-fixed, не array-map-fixed) параметры.
func HasNestedParams(params []types.Param) bool {
for _, p := range params {
if p.HasSubParams && !strings.Contains(strings.ToLower(p.Type), "array") {
return true
}
}
return false
}
// AnalyzeNestedDefaults проверяет sub-params nested-параметров на наличие default'ов.
// Возвращает true если найдены nested параметры.
func AnalyzeNestedDefaults(needsBoolDefault *bool, needsInt64Default *bool, needsStringDefault *bool, params []types.Param) bool {
found := false
for _, p := range params {
if len(p.SubParams) == 0 {
continue
}
found = true
for _, sp := range p.SubParams {
if sp.Default == "" {
continue
}
switch strings.ToLower(sp.Type) {
case "bool":
*needsBoolDefault = true
case "int64", "int", "number":
*needsInt64Default = true
default:
*needsStringDefault = true
}
}
}
return found
}
// HasRestoreCasingParams возвращает true, если есть ref_svc-параметры (кроме S3).
func HasRestoreCasingParams(params []types.Param) bool {
for _, p := range params {
@@ -244,6 +295,10 @@ func HasRestoreCasingParams(params []types.Param) bool {
// AnalyzeNeedsStrings возвращает true, если есть строковые параметры.
func AnalyzeNeedsStrings(params []types.Param) bool {
for _, p := range params {
// Пропускаем IsNested (map-fixed без array): они не участвуют в strings.EqualFold
if p.HasSubParams && !strings.Contains(strings.ToLower(p.Type), "array") {
continue
}
t := strings.ToLower(strings.TrimSpace(p.Type))
if t != "bool" && t != "int" && t != "int64" && t != "number" {
return true
@@ -7,6 +7,9 @@ import (
{{- if .NeedsStringsImport }}
"strings"
{{- end }}
{{- if .NeedsFmtImport }}
"fmt"
{{- end }}
"terraform-provider-nubes/internal/core"
"terraform-provider-nubes/internal/resources_core"
@@ -39,7 +42,7 @@ type {{ToCamel .Name}}Resource struct {
}
{{- range .SchemaParams }}
{{- if .IsNested }}
{{- if (IsNested .) }}
// {{NestedModelName $.Name .Code}} — вложенная модель для map-fixed параметра {{.Code}}.
type {{NestedModelName $.Name .Code}} struct {
{{- range .SubParams }}
@@ -61,7 +64,7 @@ type {{ToCamel .Name}}Model struct {
GitRevision types.String ` + "`" + `tfsdk:"git_revision"` + "`" + `
{{- end }}
{{- range .SchemaParams }}
{{- if .IsNested }}
{{- if (IsNested .) }}
{{ToCamel .Code}} {{NestedTfType . $.Name}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
{{- else }}
{{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
@@ -89,7 +92,7 @@ func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.Sch
"operation_timeout": schema.StringAttribute{Optional: true},
"log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."},
{{- range .SchemaParams }}
{{- if .IsNested }}
{{- if (IsNested .) }}
"{{ToSnake .Code}}": {{NestedSchemaBlock .}}
{{- range .SubParams }}
"{{ToSnake .Code}}": schema.{{SubSchemaType .}}Attribute{
@@ -171,6 +174,7 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource
}
}
{{- range .CreateOnlyParams }}
{{- if not (IsNested .) }}
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
{{- if eq (ParamType .) "types.Bool" }}
if plan.{{ToCamel .Code}}.ValueBool() != state.{{ToCamel .Code}}.ValueBool() {
@@ -193,10 +197,11 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource
{{- end }}
}
{{- end }}
{{- end }}
return
}
{{- range .SchemaParams }}
{{- if and .Required (eq (ParamDefaultExpr .) "") }}
{{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }}
if config.{{ToCamel .Code}}.IsNull() {
resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
return
@@ -230,7 +235,7 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource
{{- end }}
params := map[int]string{
{{- range .CreateParams }}
{{- if .IsNested }}
{{- if (IsNested .) }}
{{.ID}}: {{NestedJSONExpr . "config"}},
{{- else }}
{{.ID}}: {{ParamFormat . (printf "config.%s" (ToCamel .Code))}},
@@ -255,7 +260,7 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
return
}
{{- range .SchemaParams }}
{{- if and .Required (eq (ParamDefaultExpr .) "") }}
{{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }}
if data.{{ToCamel .Code}}.IsNull() || data.{{ToCamel .Code}}.IsUnknown() {
resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
return
@@ -320,7 +325,7 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
params := map[int]string{
{{- range .CreateParams }}
{{- if .IsNested }}
{{- if (IsNested .) }}
{{.ID}}: {{NestedJSONExpr . "data"}},
{{- else }}
{{.ID}}: {{ParamFormat . (printf "data.%s" (ToCamel .Code))}},
@@ -477,6 +482,7 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd
hasServiceParamChanges := false
{{- range .ModifyParams }}
{{- if not (IsNested .) }}
if !hasServiceParamChanges {
if plan.{{ToCamel .Code}}.IsNull() != state.{{ToCamel .Code}}.IsNull() || plan.{{ToCamel .Code}}.IsUnknown() != state.{{ToCamel .Code}}.IsUnknown() {
hasServiceParamChanges = true
@@ -497,6 +503,7 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd
}
}
{{- end }}
{{- end }}
{{- if .HasRedeploy }}
// --- Redeploy support (ARCHITECTURE.md) ---
@@ -540,7 +547,7 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd
{{- end }}
params := map[int]string{
{{- range .ModifyParams }}
{{- if .IsNested }}
{{- if (IsNested .) }}
{{.ID}}: {{NestedJSONExpr . "plan"}},
{{- else }}
{{.ID}}: {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
@@ -53,8 +53,8 @@ type Param struct {
// При IsJson=true в схему добавляется JsonNormalize план-модификатор,
// чтобы план и API-ответ (компактный JSON) всегда совпадали.
IsJson bool
// IsNested — параметр типа map-fixed/array-map-fixed с подполями.
IsNested bool
// HasSubParams — параметр имеет подполя (map-fixed или array-map-fixed).
HasSubParams bool
// SubParams — подполя для map-fixed/array-map-fixed параметров.
SubParams []Param
}
@@ -93,6 +93,8 @@ type GenResource struct {
NeedsJsonPlanMod bool
// NeedsStringsImport: есть строковые ModifyParams (нужен EqualFold в Update).
NeedsStringsImport bool
// NeedsFmtImport: есть nested (map-fixed) параметры, нужен fmt.Sprintf.
NeedsFmtImport bool
}
// GenSubresource — подресурс (nubes_{service}_{sub}).