resource-generator: SubParams nested fix (Соннет) — AlignParamTypes recursive, HasNestedParams/NeedsStrings skip array-map-fixed, bump 5.0.74
This commit is contained in:
@@ -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}).
|
||||
|
||||
Reference in New Issue
Block a user