From 3d227583851ae5084a598bd1ecc29caa0a60f259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 16 Jul 2026 15:06:35 +0400 Subject: [PATCH] =?UTF-8?q?resource-generator:=20SubParams=20=E2=86=92=20n?= =?UTF-8?q?ested=20SingleNestedAttribute/ListNestedAttribute=20+=20bump=20?= =?UTF-8?q?5.0.73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/helpers/helpers.go | 128 ++++++++++++++++++ .../internal/templates/instance.go | 38 ++++++ .../internal/writers/writers.go | 34 +++-- provider/internal/resources_core/helpers.go | 13 ++ provider/main.go | 2 +- 5 files changed, 202 insertions(+), 13 deletions(-) diff --git a/TOOLS/resource-generator/internal/helpers/helpers.go b/TOOLS/resource-generator/internal/helpers/helpers.go index a1dac43..4d131dc 100644 --- a/TOOLS/resource-generator/internal/helpers/helpers.go +++ b/TOOLS/resource-generator/internal/helpers/helpers.go @@ -190,3 +190,131 @@ func PlanModifierExpr(p types.Param) string { return "stringplanmodifier.RequiresReplace()" } } + +// ─── Nested (map-fixed) helpers ────────────────────────────────────────────── + +// IsNested возвращает true для параметров с SubParams (map-fixed/array-map-fixed). +func IsNested(p types.Param) bool { + return p.IsNested +} + +// IsNestedList возвращает true для array-map-fixed (список объектов). +func IsNestedList(p types.Param) bool { + return p.IsNested && 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, Computed: true" + } + return fmt.Sprintf("schema.%sAttribute{%s,\n\t\t\tAttributes: map[string]schema.Attribute{", t, req) + } +} + +// NestedSchemaEnd возвращает закрывающую строку для nested-атрибута. +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. +// 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 { + 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)) + } + return fmt.Sprintf("resources_core.BuildJSON(map[string]string{%s})", strings.Join(pairs, ", ")) +} diff --git a/TOOLS/resource-generator/internal/templates/instance.go b/TOOLS/resource-generator/internal/templates/instance.go index 9c93bcc..2b6d2f3 100644 --- a/TOOLS/resource-generator/internal/templates/instance.go +++ b/TOOLS/resource-generator/internal/templates/instance.go @@ -38,6 +38,17 @@ type {{ToCamel .Name}}Resource struct { client *core.UniversalClient } +{{- range .SchemaParams }} +{{- if .IsNested }} +// {{NestedModelName $.Name .Code}} — вложенная модель для map-fixed параметра {{.Code}}. +type {{NestedModelName $.Name .Code}} struct { + {{- range .SubParams }} + {{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}" json:"{{.Code}}"` + "`" + ` + {{- end }} +} +{{- end }} +{{- end }} + type {{ToCamel .Name}}Model struct { ID types.String ` + "`" + `tfsdk:"id"` + "`" + ` ResourceName types.String ` + "`" + `tfsdk:"resource_name"` + "`" + ` @@ -50,8 +61,12 @@ type {{ToCamel .Name}}Model struct { GitRevision types.String ` + "`" + `tfsdk:"git_revision"` + "`" + ` {{- end }} {{- range .SchemaParams }} + {{- if .IsNested }} + {{ToCamel .Code}} {{NestedTfType . $.Name}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + ` + {{- else }} {{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + ` {{- end }} + {{- end }} SuspendOnDestroy types.Bool ` + "`" + `tfsdk:"suspend_on_destroy"` + "`" + ` AdoptExistingOnCreate types.Bool ` + "`" + `tfsdk:"adopt_existing_on_create"` + "`" + ` {{- range .OutputParams }} @@ -74,6 +89,16 @@ 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 }} + "{{ToSnake .Code}}": {{NestedSchemaBlock .}} + {{- range .SubParams }} + "{{ToSnake .Code}}": schema.{{SubSchemaType .}}Attribute{ + {{- if .Default }}Optional: true, Computed: true, Default: {{SubDefaultExpr .}},{{else if .Required}}Required: true,{{else}}Optional: true,{{end}} + {{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}} + }, + {{- end }} + {{NestedSchemaEnd .}} + {{- else }} "{{ToSnake .Code}}": schema.{{if eq (ParamType .) "types.Bool"}}Bool{{else if eq (ParamType .) "types.Int64"}}Int64{{else}}String{{end}}Attribute{ {{- if and .Required (eq (ParamDefaultExpr .) "") (eq .RefSvcId 0) }}Required: true,{{else}}Optional: true,{{end}} {{- if ne (ParamDefaultExpr .) "" }}Computed: true, Default: {{ParamDefaultExpr .}},{{- else if or .IsJson (gt .RefSvcId 0) }}Computed: true,{{- end }} @@ -82,6 +107,7 @@ func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.Sch {{- if .IsJson }}PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()},{{- end }} }, {{- end }} + {{- end }} {{- if .HasRedeploy }} // --- Redeploy support (ARCHITECTURE.md) --- // При изменении git_revision вызывается redeploy вместо modify. @@ -204,8 +230,12 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource {{- end }} params := map[int]string{ {{- range .CreateParams }} + {{- if .IsNested }} + {{.ID}}: {{NestedJSONExpr . "config"}}, + {{- else }} {{.ID}}: {{ParamFormat . (printf "config.%s" (ToCamel .Code))}}, {{- end }} + {{- end }} } desiredDomain := "" {{- if .HasDomainParam }} @@ -290,8 +320,12 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre params := map[int]string{ {{- range .CreateParams }} + {{- if .IsNested }} + {{.ID}}: {{NestedJSONExpr . "data"}}, + {{- else }} {{.ID}}: {{ParamFormat . (printf "data.%s" (ToCamel .Code))}}, {{- end }} + {{- end }} } operationTimeout := "" @@ -506,8 +540,12 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd {{- end }} params := map[int]string{ {{- range .ModifyParams }} + {{- if .IsNested }} + {{.ID}}: {{NestedJSONExpr . "plan"}}, + {{- else }} {{.ID}}: {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}}, {{- end }} + {{- end }} } if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) diff --git a/TOOLS/resource-generator/internal/writers/writers.go b/TOOLS/resource-generator/internal/writers/writers.go index efc0b4c..de22969 100644 --- a/TOOLS/resource-generator/internal/writers/writers.go +++ b/TOOLS/resource-generator/internal/writers/writers.go @@ -27,18 +27,28 @@ func WriteInstanceResource(outDir string, svc types.GenResource) error { filePath := filepath.Join(outDir, fileName) tpl, err := template.New("resource").Funcs(template.FuncMap{ - "ToCamel": helpers.ToCamel, - "ToSnake": helpers.ToSnake, - "ParamType": helpers.ParamType, - "ParamDefaultExpr": helpers.ParamDefaultExpr, - "ParamFormat": helpers.ParamFormat, - "ParamParse": helpers.ParamParse, - "ParamDescription": helpers.ParamDescription, - "OutputType": helpers.OutputParamType, - "OutputIsMap": helpers.OutputParamIsMap, - "OutputIsList": helpers.OutputParamIsList, - "OutputSensitive": helpers.OutputParamSensitive, - "bt": func() string { return "`" }, + "ToCamel": helpers.ToCamel, + "ToSnake": helpers.ToSnake, + "ParamType": helpers.ParamType, + "ParamDefaultExpr": helpers.ParamDefaultExpr, + "ParamFormat": helpers.ParamFormat, + "ParamParse": helpers.ParamParse, + "ParamDescription": helpers.ParamDescription, + "OutputType": helpers.OutputParamType, + "OutputIsMap": helpers.OutputParamIsMap, + "OutputIsList": helpers.OutputParamIsList, + "OutputSensitive": helpers.OutputParamSensitive, + "IsNested": helpers.IsNested, + "IsNestedList": helpers.IsNestedList, + "NestedModelName": helpers.NestedModelName, + "NestedTfType": helpers.NestedTfType, + "NestedSchemaType": helpers.NestedSchemaType, + "NestedSchemaBlock": helpers.NestedSchemaBlock, + "NestedSchemaEnd": helpers.NestedSchemaEnd, + "NestedJSONExpr": helpers.NestedJSONExpr, + "SubSchemaType": helpers.SubSchemaType, + "SubDefaultExpr": helpers.SubDefaultExpr, + "bt": func() string { return "`" }, }).Parse(templates.Instance) if err != nil { return err diff --git a/provider/internal/resources_core/helpers.go b/provider/internal/resources_core/helpers.go index 2c3a0d4..9b7975e 100644 --- a/provider/internal/resources_core/helpers.go +++ b/provider/internal/resources_core/helpers.go @@ -78,3 +78,16 @@ func CompactParams(params map[string]string) map[string]string { } return out } + +// BuildJSON строит JSON-строку из map[string]string значений. +// Ключи не кавычатся если они уже в кавычках. +func BuildJSON(parts map[string]string) string { + if len(parts) == 0 { + return "{}" + } + pairs := make([]string, 0, len(parts)) + for k, v := range parts { + pairs = append(pairs, fmt.Sprintf(`"%s":%s`, k, v)) + } + return "{" + strings.Join(pairs, ",") + "}" +} diff --git a/provider/main.go b/provider/main.go index f381513..56c70be 100644 --- a/provider/main.go +++ b/provider/main.go @@ -17,7 +17,7 @@ import ( ) var ( - version string = "5.0.72" + version string = "5.0.73" ) func main() {