package main import ( "bytes" "fmt" "go/format" "io/fs" "os" "path/filepath" "sort" "strconv" "strings" "text/template" "unicode" "gopkg.in/yaml.v3" ) type Param struct { ID int `yaml:"id"` Code string `yaml:"code"` Type string `yaml:"type"` Required bool `yaml:"required"` Default string `yaml:"default"` // NOTE(name-to-uuid): ref_svc_id marks params that accept UI names, resolved in core. RefSvcId int `yaml:"ref_svc_id,omitempty"` } type OutputParam struct { Code string `yaml:"code"` Type string `yaml:"type"` Sensitive bool `yaml:"sensitive,omitempty"` } type ServiceYAML struct { Name string `yaml:"name"` ServiceID int `yaml:"service_id"` Create struct { Params []Param `yaml:"params"` } `yaml:"create"` Modify struct { Params []Param `yaml:"params"` } `yaml:"modify"` Outputs struct { Params []OutputParam `yaml:"params"` } `yaml:"outputs"` Lifecycle struct { DeleteModeDefault string `yaml:"delete_mode_default"` ResumeIfExistsDefault bool `yaml:"resume_if_exists_default"` } `yaml:"lifecycle"` } type GenResource struct { Name string ServiceID int CreateParams []Param CreateFixedParams []FixedParam ModifyParams []Param SchemaParams []Param OutputParams []OutputParam DeleteMode string ResumeIfExists bool UsesBool bool UsesInt64 bool UsesString bool HasDefaults bool NeedsBoolDefault bool NeedsInt64Default bool NeedsStringDefault bool } func main() { root, err := os.Getwd() if err != nil { panic(err) } resourcesDir := filepath.Join(root, "resources_yaml") extraParamsDir := filepath.Join(root, "extra-params") outDir := filepath.Join(root, "internal", "resources_gen") services, err := loadServices(resourcesDir, extraParamsDir) if err != nil { panic(err) } for _, svc := range services { if err := writeResource(outDir, svc); err != nil { panic(err) } } if err := writeRegistry(outDir, services); err != nil { panic(err) } } func loadServices(dir string, extraDir string) ([]GenResource, error) { var services []GenResource walkErr := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { return nil } b, err := os.ReadFile(path) if err != nil { return err } var svc ServiceYAML if err := yaml.Unmarshal(b, &svc); err != nil { return err } createParams, createFixed := splitCreateParams(svc.Name, svc.Create.Params) modifyParams := svc.Modify.Params modifyParams = normalizeModifyParams(createParams, modifyParams) schemaParams, err := buildSchemaParams(extraDir, svc.ServiceID, createParams, modifyParams) if err != nil { return err } outputParams := mergeOutputParams(commonOutputParams(), normalizeOutputParams(svc.Outputs.Params)) gr := GenResource{ Name: svc.Name, ServiceID: svc.ServiceID, CreateParams: createParams, CreateFixedParams: createFixed, ModifyParams: modifyParams, SchemaParams: schemaParams, OutputParams: outputParams, DeleteMode: svc.Lifecycle.DeleteModeDefault, ResumeIfExists: svc.Lifecycle.ResumeIfExistsDefault, } analyzeParams := func(p Param) { switch strings.ToLower(p.Type) { case "bool": gr.UsesBool = true if p.Default != "" && !p.Required { gr.NeedsBoolDefault = true gr.HasDefaults = true } case "int", "int64", "number": gr.UsesInt64 = true if p.Default != "" && !p.Required { gr.NeedsInt64Default = true gr.HasDefaults = true } default: gr.UsesString = true if p.Default != "" && !p.Required { gr.NeedsStringDefault = true gr.HasDefaults = true } } } for _, p := range gr.SchemaParams { analyzeParams(p) } // Schema defaults apply only to create params gr.NeedsBoolDefault = false gr.NeedsInt64Default = false gr.NeedsStringDefault = false for _, p := range gr.SchemaParams { if p.Default == "" || p.Required { continue } switch strings.ToLower(p.Type) { case "bool": gr.NeedsBoolDefault = true case "int", "int64", "number": gr.NeedsInt64Default = true default: gr.NeedsStringDefault = true } } services = append(services, gr) return nil }) if walkErr != nil { return nil, walkErr } sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name }) return services, nil } func writeResource(outDir string, svc GenResource) error { fileName := fmt.Sprintf("%d_%s_resource.go", svc.ServiceID, svc.Name) filePath := filepath.Join(outDir, fileName) tpl, err := template.New("resource").Funcs(template.FuncMap{ "ToCamel": toCamel, "ToSnake": toSnake, "ParamType": paramType, "ParamDefault": paramDefault, "ParamDefaultExpr": paramDefaultExpr, "ParamFormat": paramFormat, "ParamParse": paramParse, "ParamDescription": paramDescription, "OutputType": outputParamType, "OutputIsMap": outputParamIsMap, "OutputIsList": outputParamIsList, "OutputSensitive": outputParamSensitive, "FixedParamExpr": fixedParamExpr, "bt": func() string { return "`" }, }).Parse(resourceTemplate) if err != nil { return err } var buf bytes.Buffer if err := tpl.Execute(&buf, svc); err != nil { return err } formatted, err := format.Source(buf.Bytes()) if err != nil { formatted = buf.Bytes() } return os.WriteFile(filePath, formatted, 0644) } func writeRegistry(outDir string, services []GenResource) error { var buf bytes.Buffer buf.WriteString("package resources_gen\n\n") buf.WriteString("import \"github.com/hashicorp/terraform-plugin-framework/resource\"\n\n") buf.WriteString("// Code generated by tools/gen. DO NOT EDIT.\n") buf.WriteString("func AllResources() []func() resource.Resource {\n") buf.WriteString("\treturn []func() resource.Resource{\n") for _, svc := range services { buf.WriteString(fmt.Sprintf("\t\tNew%[1]sResource,\n", toCamel(svc.Name))) } buf.WriteString("\t}\n") buf.WriteString("}\n") formatted, err := format.Source(buf.Bytes()) if err != nil { formatted = buf.Bytes() } return os.WriteFile(filepath.Join(outDir, "registry.go"), formatted, 0644) } 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) } 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) } func paramType(p Param) string { switch strings.ToLower(p.Type) { case "bool": return "types.Bool" case "int", "int64", "number": return "types.Int64" default: return "types.String" } } func paramDefault(p Param) string { return p.Default } func ensureGoIdent(s string) string { if s == "" { return "R" } first := rune(s[0]) if unicode.IsLetter(first) || first == '_' { return s } return "R" + s } func paramDefaultExpr(p 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) } } func outputParamType(p OutputParam) string { switch strings.ToLower(p.Type) { case "map": return "types.Map" case "list": return "types.List" default: return "types.String" } } func outputParamIsMap(p OutputParam) bool { return strings.EqualFold(strings.TrimSpace(p.Type), "map") } func outputParamIsList(p OutputParam) bool { return strings.EqualFold(strings.TrimSpace(p.Type), "list") } func outputParamSensitive(p OutputParam) bool { return p.Sensitive } func paramFormat(p 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) } } func paramDescription(p Param) string { if p.RefSvcId <= 0 { return "" } // refSvcId marks name-based references that must be resolved to UUID in core. return fmt.Sprintf("\"UI display_name for service_id=%d (mapped to UUID in core)\"", p.RefSvcId) } const resourceTemplate = `package resources_gen import ( "context" "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" {{- if .NeedsInt64Default }} "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" {{- end }} "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/types" ) // Code generated by tools/gen. DO NOT EDIT. // Service: {{.Name}} // Service ID: {{.ServiceID}} var _ resource.Resource = &{{ToCamel .Name}}Resource{} var _ resource.ResourceWithModifyPlan = &{{ToCamel .Name}}Resource{} type {{ToCamel .Name}}Resource struct { client *core.UniversalClient } type {{ToCamel .Name}}Model struct { ID types.String {{bt}}tfsdk:"id"{{bt}} ResourceName types.String {{bt}}tfsdk:"resource_name"{{bt}} {{- range .SchemaParams }} {{ToCamel .Code}} {{ParamType .}} {{bt}}tfsdk:"{{ToSnake .Code}}"{{bt}} {{- end }} DeleteMode types.String {{bt}}tfsdk:"delete_mode"{{bt}} ResumeIfExists types.Bool {{bt}}tfsdk:"resume_if_exists"{{bt}} {{- range .OutputParams }} {{ToCamel .Code}} {{OutputType .}} {{bt}}tfsdk:"{{ToSnake .Code}}"{{bt}} {{- end }} } func New{{ToCamel .Name}}Resource() resource.Resource { return &{{ToCamel .Name}}Resource{} } func (r *{{ToCamel .Name}}Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_{{.Name}}" } func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { attrs := map[string]schema.Attribute{ "id": schema.StringAttribute{Computed: true}, "resource_name": schema.StringAttribute{Required: true}, {{- range .SchemaParams }} "{{ToSnake .Code}}": schema.{{if eq (ParamType .) "types.Bool"}}Bool{{else if eq (ParamType .) "types.Int64"}}Int64{{else}}String{{end}}Attribute{ {{if .Required}}Required: true,{{else}}Optional: true,{{end}} {{if and (ParamDefault .) (not .Required)}}Computed: true,{{end}} {{if and (ParamDefault .) (not .Required)}}Default: {{ParamDefaultExpr .}},{{end}} {{- if ParamDescription . }}MarkdownDescription: {{ParamDescription .}},{{end}} }, {{- end }} "delete_mode": schema.StringAttribute{ Optional: true, Computed: true, Default: stringdefault.StaticString("{{if .DeleteMode}}{{.DeleteMode}}{{else}}state_only{{end}}"), }, "resume_if_exists": schema.BoolAttribute{ Optional: true, Computed: true, Default: booldefault.StaticBool({{if .ResumeIfExists}}true{{else}}false{{end}}), }, {{- range .OutputParams }} "{{ToSnake .Code}}": schema.{{if OutputIsMap .}}Map{{else if OutputIsList .}}List{{else}}String{{end}}Attribute{ Computed: true, {{- if or (OutputIsMap .) (OutputIsList .) }}ElementType: types.StringType,{{- end }} {{- if OutputSensitive . }}Sensitive: true,{{- end }} }, {{- end }} } resp.Schema = schema.Schema{Attributes: attrs} } func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { if r.client == nil { return } var config *{{ToCamel .Name}}Model resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) if resp.Diagnostics.HasError() { return } if config == nil { return } var state *{{ToCamel .Name}}Model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { return } if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { return } resumeIfExists := true if !config.ResumeIfExists.IsNull() && !config.ResumeIfExists.IsUnknown() { resumeIfExists = config.ResumeIfExists.ValueBool() } resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnostics(ctx, r.client, {{.ServiceID}}, config.ResourceName.ValueString(), resumeIfExists)...) } func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data {{ToCamel .Name}}Model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } resourceName := data.ResourceName.ValueString() resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnostics(ctx, r.client, {{.ServiceID}}, resourceName, data.ResumeIfExists.ValueBool())...) params := map[int]string{ {{- range .CreateFixedParams }} {{.ID}}: {{FixedParamExpr .}}, {{- end }} {{- range .CreateParams }} {{.ID}}: {{ParamFormat . (printf "data.%s" (ToCamel .Code))}}, {{- end }} } id, err := resources_core.CreateResource(ctx, r.client, {{.ServiceID}}, resourceName, data.ResumeIfExists.ValueBool(), params) if err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return } data.ID = types.StringValue(id) if r.client != nil { outputs, outDiags := resources_core.FetchInstanceOutputs(ctx, r.client, data.ID.ValueString()) resp.Diagnostics.Append(outDiags...) if resp.Diagnostics.HasError() { return } {{- range .OutputParams }} data.{{ToCamel .Code}} = outputs.{{ToCamel .Code}} {{- end }} {{- if .SchemaParams }} // Sync input params from state_params to avoid drift (ex: git_path). paramsMap, paramsDiags := resources_core.StringMapFromTypesMap(ctx, outputs.StateParams) resp.Diagnostics.Append(paramsDiags...) if resp.Diagnostics.HasError() { return } // Map refSvcId UUIDs back to display_name for stable state. paramsMap, paramsDiags = resources_core.ResolveRefSvcParamDisplayNames(ctx, r.client, {{.ServiceID}}, paramsMap) resp.Diagnostics.Append(paramsDiags...) if resp.Diagnostics.HasError() { return } {{- range .SchemaParams }} if val, ok := paramsMap["{{.Code}}"] ; ok { data.{{ToCamel .Code}} = {{ParamParse .}}(val) } {{- end }} {{- end }} } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *{{ToCamel .Name}}Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data *{{ToCamel .Name}}Model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } if data == nil { return } if r.client != nil && !data.ID.IsNull() && !data.ID.IsUnknown() { remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, data.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return } if remove { resp.State.RemoveResource(ctx) return } outputs, outDiags := resources_core.FetchInstanceOutputs(ctx, r.client, data.ID.ValueString()) resp.Diagnostics.Append(outDiags...) if resp.Diagnostics.HasError() { return } {{- range .OutputParams }} data.{{ToCamel .Code}} = outputs.{{ToCamel .Code}} {{- end }} {{- if .SchemaParams }} // Sync input params from state_params to avoid drift (ex: git_path). paramsMap, paramsDiags := resources_core.StringMapFromTypesMap(ctx, outputs.StateParams) resp.Diagnostics.Append(paramsDiags...) if resp.Diagnostics.HasError() { return } // Map refSvcId UUIDs back to display_name for stable state. paramsMap, paramsDiags = resources_core.ResolveRefSvcParamDisplayNames(ctx, r.client, {{.ServiceID}}, paramsMap) resp.Diagnostics.Append(paramsDiags...) if resp.Diagnostics.HasError() { return } {{- range .SchemaParams }} if val, ok := paramsMap["{{.Code}}"] ; ok { data.{{ToCamel .Code}} = {{ParamParse .}}(val) } {{- end }} {{- end }} } resp.Diagnostics.Append(resp.State.Set(ctx, data)...) } func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var plan {{ToCamel .Name}}Model var state {{ToCamel .Name}}Model resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } instanceID := state.ID if instanceID.IsNull() || instanceID.IsUnknown() { instanceID = plan.ID } if instanceID.IsNull() || instanceID.IsUnknown() { resp.Diagnostics.AddError("Ошибка клиента", "отсутствует id инстанса для изменения") return } params := map[int]string{ {{- range .ModifyParams }} {{.ID}}: {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}}, {{- end }} } if err := resources_core.UpdateResource(ctx, r.client, instanceID.ValueString(), params); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return } plan.ID = instanceID if r.client != nil && !instanceID.IsNull() && !instanceID.IsUnknown() { outputs, outDiags := resources_core.FetchInstanceOutputs(ctx, r.client, instanceID.ValueString()) resp.Diagnostics.Append(outDiags...) if resp.Diagnostics.HasError() { return } {{- range .OutputParams }} plan.{{ToCamel .Code}} = outputs.{{ToCamel .Code}} {{- end }} {{- if .SchemaParams }} // Sync input params from state_params to avoid drift (ex: git_path). paramsMap, paramsDiags := resources_core.StringMapFromTypesMap(ctx, outputs.StateParams) resp.Diagnostics.Append(paramsDiags...) if resp.Diagnostics.HasError() { return } // Map refSvcId UUIDs back to display_name for stable state. paramsMap, paramsDiags = resources_core.ResolveRefSvcParamDisplayNames(ctx, r.client, {{.ServiceID}}, paramsMap) resp.Diagnostics.Append(paramsDiags...) if resp.Diagnostics.HasError() { return } {{- range .SchemaParams }} if val, ok := paramsMap["{{.Code}}"] ; ok { plan.{{ToCamel .Code}} = {{ParamParse .}}(val) } {{- end }} {{- end }} } resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } func (r *{{ToCamel .Name}}Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state *{{ToCamel .Name}}Model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } if state == nil || state.ID.IsNull() || state.ID.IsUnknown() { return } if err := resources_core.DeleteResource(ctx, r.client, state.ID.ValueString(), state.DeleteMode.ValueString()); err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return } } func (r *{{ToCamel .Name}}Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { if req.ProviderData == nil { return } client, ok := req.ProviderData.(*core.UniversalClient) if !ok { resp.Diagnostics.AddError("Ошибка", "Неверный тип клиента, ожидается *core.UniversalClient") return } r.client = client } // format helpers live in resources_core/helpers.go ` type FixedParam struct { ID int Value string } func splitCreateParams(serviceName string, params []Param) ([]Param, []FixedParam) { if len(params) == 0 { return nil, nil } return params, nil } func isResourceRealmParam(p Param) bool { return strings.EqualFold(strings.TrimSpace(p.Code), "resourceRealm") } func fixedParamExpr(p FixedParam) string { return fmt.Sprintf("resources_core.FormatString(types.StringValue(%q))", p.Value) } func mergeParams(createParams []Param, modifyParams []Param) []Param { if len(createParams) == 0 && len(modifyParams) == 0 { return nil } byCode := make(map[string]Param) order := make([]string, 0, len(createParams)+len(modifyParams)) for _, p := range createParams { key := strings.ToLower(strings.TrimSpace(p.Code)) if key == "" { continue } byCode[key] = p order = append(order, key) } for _, p := range modifyParams { key := strings.ToLower(strings.TrimSpace(p.Code)) if key == "" { continue } if _, exists := byCode[key]; !exists { p.Required = false byCode[key] = p order = append(order, key) } } merged := make([]Param, 0, len(order)) seen := make(map[string]struct{}, len(order)) for _, key := range order { if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} merged = append(merged, byCode[key]) } return merged } func normalizeModifyParams(createParams []Param, modifyParams []Param) []Param { if len(modifyParams) == 0 { return nil } createByCode := make(map[string]Param) for _, p := range createParams { key := strings.ToLower(strings.TrimSpace(p.Code)) if key == "" { continue } createByCode[key] = p } normalized := make([]Param, 0, len(modifyParams)) for _, p := range modifyParams { key := strings.ToLower(strings.TrimSpace(p.Code)) if key == "" { continue } if base, ok := createByCode[key]; ok { p.Type = base.Type } normalized = append(normalized, p) } return normalized } // ===== extra params support ===== type ExtraParamSpec struct { Type string `yaml:"type"` Required bool `yaml:"required"` } func buildSchemaParams(extraDir string, serviceID int, createParams []Param, modifyParams []Param) ([]Param, error) { base := mergeParams(createParams, modifyParams) extra, err := loadExtraParams(extraDir, serviceID) if err != nil { return nil, err } if len(extra) == 0 { return base, nil } return mergeExtraParams(base, extra) } func loadExtraParams(extraDir string, serviceID int) ([]Param, error) { if extraDir == "" { return nil, nil } entries, err := os.ReadDir(extraDir) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var params []Param for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") { continue } fileSvcID, err := parseServiceID(entry.Name()) if err != nil { return nil, err } if fileSvcID != serviceID { continue } path := filepath.Join(extraDir, entry.Name()) fileParams, err := parseExtraParamsFile(path) if err != nil { return nil, err } params = append(params, fileParams...) } return params, nil } func parseExtraParamsFile(path string) ([]Param, error) { b, err := os.ReadFile(path) if err != nil { return nil, err } var raw []map[string]ExtraParamSpec if err := yaml.Unmarshal(b, &raw); err != nil { return nil, fmt.Errorf("invalid extra params yaml %s: %w", path, err) } if len(raw) == 0 { return nil, nil } params := make([]Param, 0, len(raw)) for _, item := range raw { if len(item) != 1 { return nil, fmt.Errorf("invalid extra params entry in %s: expected single key", path) } for code, spec := range item { code = strings.TrimSpace(code) if code == "" { return nil, fmt.Errorf("invalid extra params entry in %s: empty code", path) } paramType := normalizeExtraType(spec.Type) params = append(params, Param{ Code: code, Type: paramType, Required: spec.Required, }) } } return params, nil } func normalizeExtraType(value string) string { value = strings.ToLower(strings.TrimSpace(value)) if value == "" { return "string" } switch value { case "bool", "boolean": return "bool" case "int", "int64", "number": return "int64" default: return "string" } } func mergeExtraParams(base []Param, extra []Param) ([]Param, error) { if len(extra) == 0 { return base, nil } byCode := make(map[string]struct{}, len(base)) for _, p := range base { key := strings.ToLower(strings.TrimSpace(p.Code)) if key == "" { continue } byCode[key] = struct{}{} } for _, p := range extra { key := strings.ToLower(strings.TrimSpace(p.Code)) if key == "" { return nil, fmt.Errorf("invalid extra param: empty code") } if _, exists := byCode[key]; exists { return nil, fmt.Errorf("extra param %q conflicts with existing API param", p.Code) } byCode[key] = struct{}{} base = append(base, p) } return base, nil } func normalizeOutputParams(params []OutputParam) []OutputParam { normalized := make([]OutputParam, 0, len(params)) for _, p := range params { code := strings.TrimSpace(p.Code) if code == "" { continue } p.Code = code p.Type = normalizeOutputType(p.Type) normalized = append(normalized, p) } return normalized } func normalizeOutputType(value string) string { value = strings.ToLower(strings.TrimSpace(value)) if value == "" { return "string" } switch value { case "map", "list", "string": return value default: return "string" } } func commonOutputParams() []OutputParam { return []OutputParam{ {Code: "state_params", Type: "map"}, {Code: "state_out", Type: "map"}, {Code: "state_params_flat", Type: "map"}, {Code: "state_out_flat", Type: "map"}, {Code: "vault_secrets", Type: "map", Sensitive: true}, {Code: "vault_url", Type: "string"}, {Code: "vault_user_path", Type: "string"}, {Code: "vault_fields", Type: "list"}, } } func paramParse(p 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" } } func mergeOutputParams(base []OutputParam, extra []OutputParam) []OutputParam { if len(extra) == 0 { return base } byCode := make(map[string]struct{}, len(base)) for _, p := range base { key := strings.ToLower(strings.TrimSpace(p.Code)) if key == "" { continue } byCode[key] = struct{}{} } for _, p := range extra { key := strings.ToLower(strings.TrimSpace(p.Code)) if key == "" { continue } if _, exists := byCode[key]; exists { continue } byCode[key] = struct{}{} base = append(base, p) } return base } func parseServiceID(name string) (int, error) { trimmed := strings.TrimSpace(name) var digits []rune for _, r := range trimmed { if r < '0' || r > '9' { break } digits = append(digits, r) } if len(digits) == 0 { return 0, fmt.Errorf("extra params file %q must start with service id", name) } value, err := strconv.Atoi(string(digits)) if err != nil { return 0, fmt.Errorf("invalid service id in extra params file %q", name) } return value, nil }