From f94d857620bea401b9bba053c7598a9e9a783a50 Mon Sep 17 00:00:00 2001 From: edwardfeng-db Date: Thu, 25 Jul 2024 10:22:43 +0200 Subject: [PATCH 1/5] update --- .../customizable_schema_plugin_framework.go | 131 ++++++++++++++++++ ...stomizable_schema_plugin_framework_test.go | 23 +++ common/reflect_resource_plugin_framework.go | 11 +- .../reflect_resource_plugin_framework_test.go | 2 +- 4 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 common/customizable_schema_plugin_framework.go create mode 100644 common/customizable_schema_plugin_framework_test.go diff --git a/common/customizable_schema_plugin_framework.go b/common/customizable_schema_plugin_framework.go new file mode 100644 index 0000000000..6d2376d8d7 --- /dev/null +++ b/common/customizable_schema_plugin_framework.go @@ -0,0 +1,131 @@ +package common + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/provider/schema" +) + +type CustomizableSchemaPluginFramework struct { + attr schema.Attribute +} + +func ConstructCustomizableSchema(attributes map[string]schema.Attribute) *CustomizableSchemaPluginFramework { + attr := schema.Attribute(schema.SingleNestedAttribute{Attributes: attributes}) + return &CustomizableSchemaPluginFramework{attr: attr} +} + +func (s *CustomizableSchemaPluginFramework) SchemaPath(path ...string) *CustomizableSchemaPluginFramework { + attr, err := navigateSchema(s.attr, path...) + if err != nil { + panic(err) + } + + return &CustomizableSchemaPluginFramework{attr} +} + +// Converts CustomizableSchema into a map from string to Attribute. +func (s *CustomizableSchemaPluginFramework) ToAttributeMap() map[string]schema.Attribute { + return attributeToMap(s.attr) +} + +func attributeToMap(attr schema.Attribute) map[string]schema.Attribute { + var m map[string]schema.Attribute + + switch attr := attr.(type) { + case schema.SingleNestedAttribute: + m = attr.Attributes + case schema.ListNestedAttribute: + m = attr.NestedObject.Attributes + case schema.MapNestedAttribute: + m = attr.NestedObject.Attributes + default: + panic(fmt.Errorf("cannot convert to map, attribute is not nested")) + } + + return m +} + +func (s *CustomizableSchemaPluginFramework) AddNewField(key string, newField schema.Attribute) *CustomizableSchemaPluginFramework { + switch attr := s.attr.(type) { + case schema.SingleNestedAttribute: + _, exists := attr.Attributes[key] + if exists { + panic("Cannot add new field, " + key + " already exists in the schema") + } + attr.Attributes[key] = newField + attr.Required = true + s.attr = attr + case schema.ListNestedAttribute: + _, exists := attr.NestedObject.Attributes[key] + if exists { + panic("Cannot add new field, " + key + " already exists in the schema") + } + attr.NestedObject.Attributes[key] = newField + s.attr = attr + case schema.MapNestedAttribute: + _, exists := attr.NestedObject.Attributes[key] + if exists { + panic("Cannot add new field, " + key + " already exists in the schema") + } + attr.NestedObject.Attributes[key] = newField + s.attr = attr + default: + panic("attribute is not nested, cannot add field") + } + + return s +} + +func (s *CustomizableSchemaPluginFramework) RemoveField(key string) *CustomizableSchemaPluginFramework { + switch attr := s.attr.(type) { + case schema.SingleNestedAttribute: + _, exists := attr.Attributes[key] + if !exists { + panic("Cannot remove field, " + key + " does not exist in the schema") + } + delete(attr.Attributes, key) + s.attr = attr + case schema.ListNestedAttribute: + _, exists := attr.NestedObject.Attributes[key] + if !exists { + panic("Cannot remove field, " + key + " does not exist in the schema") + } + delete(attr.NestedObject.Attributes, key) + s.attr = attr + case schema.MapNestedAttribute: + _, exists := attr.NestedObject.Attributes[key] + if !exists { + panic("Cannot remove field, " + key + " does not exist in the schema") + } + delete(attr.NestedObject.Attributes, key) + s.attr = attr + default: + panic("attribute is not nested, cannot add field") + } + + return s +} + +// Given a attribute map, navigate through the given path, panics if the path is not valid. +func MustSchemaAttributePath(attrs map[string]schema.Attribute, path ...string) schema.Attribute { + return ConstructCustomizableSchema(attrs).SchemaPath(path...).attr +} + +// Helper function for navigating through schema attributes, panics if path does not exist or invalid. +func navigateSchema(s schema.Attribute, path ...string) (schema.Attribute, error) { + cs := s + for i, p := range path { + m := attributeToMap(cs) + + v, ok := m[p] + if !ok { + return nil, fmt.Errorf("missing key %s", p) + } + if i == len(path)-1 { + return v, nil + } + cs = v + } + return nil, fmt.Errorf("path %v is incomplete", path) +} diff --git a/common/customizable_schema_plugin_framework_test.go b/common/customizable_schema_plugin_framework_test.go new file mode 100644 index 0000000000..11327d152d --- /dev/null +++ b/common/customizable_schema_plugin_framework_test.go @@ -0,0 +1,23 @@ +package common + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/stretchr/testify/assert" +) + +func TestCustomizeSchema(t *testing.T) { + scm := pluginFrameworkStructToSchema(DummyTfSdk{}, func(c CustomizableSchemaPluginFramework) CustomizableSchemaPluginFramework { + c.AddNewField("new_field", schema.StringAttribute{Required: true}) + c.SchemaPath("nested").AddNewField("new_field", schema.StringAttribute{Required: true}) + c.SchemaPath("nested").AddNewField("to_be_removed", schema.StringAttribute{Required: true}) + c.SchemaPath("nested").RemoveField("to_be_removed") + return c + }) + assert.True(t, scm.Attributes["new_field"].IsRequired()) + assert.True(t, MustSchemaAttributePath(scm.Attributes, "nested", "new_field").IsRequired()) + attr := MustSchemaAttributePath(scm.Attributes, "nested").(schema.SingleNestedAttribute).Attributes + _, ok := attr["to_be_removed"] + assert.True(t, !ok) +} diff --git a/common/reflect_resource_plugin_framework.go b/common/reflect_resource_plugin_framework.go index 0504cbc3be..340096045e 100644 --- a/common/reflect_resource_plugin_framework.go +++ b/common/reflect_resource_plugin_framework.go @@ -391,8 +391,13 @@ func fieldIsOptional(field reflect.StructField) bool { return strings.Contains(tagValue, "optional") } -func pluginFrameworkStructToSchema(v any) schema.Schema { - return schema.Schema{ - Attributes: pluginFrameworkTypeToSchema(reflect.ValueOf(v)), +func pluginFrameworkStructToSchema(v any, customizeSchema func(CustomizableSchemaPluginFramework) CustomizableSchemaPluginFramework) schema.Schema { + attributes := pluginFrameworkTypeToSchema(reflect.ValueOf(v)) + + if customizeSchema != nil { + cs := customizeSchema(*ConstructCustomizableSchema(attributes)) + return schema.Schema{Attributes: cs.ToAttributeMap()} + } else { + return schema.Schema{Attributes: attributes} } } diff --git a/common/reflect_resource_plugin_framework_test.go b/common/reflect_resource_plugin_framework_test.go index ba40444d23..f1a4325bd8 100644 --- a/common/reflect_resource_plugin_framework_test.go +++ b/common/reflect_resource_plugin_framework_test.go @@ -129,7 +129,7 @@ var tfSdkStruct = DummyTfSdk{ func TestGetAndSetPluginFramework(t *testing.T) { // Also test StructToSchema. - scm := pluginFrameworkStructToSchema(DummyTfSdk{}) + scm := pluginFrameworkStructToSchema(DummyTfSdk{}, nil) state := tfsdk.State{ Schema: scm, } From 80968a762f3d0cacfef716fd7abea9e23c0ad6ce Mon Sep 17 00:00:00 2001 From: edwardfeng-db Date: Thu, 25 Jul 2024 22:20:33 +0200 Subject: [PATCH 2/5] update --- .../customizable_schema_plugin_framework.go | 337 +++++++++++++++--- ...stomizable_schema_plugin_framework_test.go | 50 ++- 2 files changed, 325 insertions(+), 62 deletions(-) diff --git a/common/customizable_schema_plugin_framework.go b/common/customizable_schema_plugin_framework.go index 6d2376d8d7..b3e5e7c815 100644 --- a/common/customizable_schema_plugin_framework.go +++ b/common/customizable_schema_plugin_framework.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) type CustomizableSchemaPluginFramework struct { @@ -15,24 +16,24 @@ func ConstructCustomizableSchema(attributes map[string]schema.Attribute) *Custom return &CustomizableSchemaPluginFramework{attr: attr} } -func (s *CustomizableSchemaPluginFramework) SchemaPath(path ...string) *CustomizableSchemaPluginFramework { - attr, err := navigateSchema(s.attr, path...) - if err != nil { - panic(err) - } +// func (s *CustomizableSchemaPluginFramework) SchemaPath(path ...string) *CustomizableSchemaPluginFramework { +// // The attr returned here is not the original object, it's a copy. +// attr, err := navigateSchema(&s.attr, path...) +// if err != nil { +// panic(err) +// } - return &CustomizableSchemaPluginFramework{attr} -} +// return &CustomizableSchemaPluginFramework{attr} +// } // Converts CustomizableSchema into a map from string to Attribute. func (s *CustomizableSchemaPluginFramework) ToAttributeMap() map[string]schema.Attribute { - return attributeToMap(s.attr) + return attributeToMap(&s.attr) } -func attributeToMap(attr schema.Attribute) map[string]schema.Attribute { +func attributeToMap(attr *schema.Attribute) map[string]schema.Attribute { var m map[string]schema.Attribute - - switch attr := attr.(type) { + switch attr := (*attr).(type) { case schema.SingleNestedAttribute: m = attr.Attributes case schema.ListNestedAttribute: @@ -46,74 +47,271 @@ func attributeToMap(attr schema.Attribute) map[string]schema.Attribute { return m } -func (s *CustomizableSchemaPluginFramework) AddNewField(key string, newField schema.Attribute) *CustomizableSchemaPluginFramework { - switch attr := s.attr.(type) { - case schema.SingleNestedAttribute: - _, exists := attr.Attributes[key] - if exists { - panic("Cannot add new field, " + key + " already exists in the schema") +func (s *CustomizableSchemaPluginFramework) AddNewField(key string, newField schema.Attribute, path ...string) *CustomizableSchemaPluginFramework { + cb := func(a schema.Attribute) schema.Attribute { + switch attr := a.(type) { + case schema.SingleNestedAttribute: + _, exists := attr.Attributes[key] + if exists { + panic("Cannot add new field, " + key + " already exists in the schema") + } + attr.Attributes[key] = newField + return attr + case schema.ListNestedAttribute: + _, exists := attr.NestedObject.Attributes[key] + if exists { + panic("Cannot add new field, " + key + " already exists in the schema") + } + attr.NestedObject.Attributes[key] = newField + return attr + case schema.MapNestedAttribute: + _, exists := attr.NestedObject.Attributes[key] + if exists { + panic("Cannot add new field, " + key + " already exists in the schema") + } + attr.NestedObject.Attributes[key] = newField + return attr + default: + panic("attribute is not nested, cannot add field") } - attr.Attributes[key] = newField - attr.Required = true - s.attr = attr - case schema.ListNestedAttribute: - _, exists := attr.NestedObject.Attributes[key] - if exists { - panic("Cannot add new field, " + key + " already exists in the schema") + } + + if len(path) == 0 { + s.attr = cb(s.attr) + } else { + navigateSchemaWithCallback(&s.attr, cb, path...) + } + return s +} + +func (s *CustomizableSchemaPluginFramework) RemoveField(key string, path ...string) *CustomizableSchemaPluginFramework { + cb := func(a schema.Attribute) schema.Attribute { + switch attr := a.(type) { + case schema.SingleNestedAttribute: + _, exists := attr.Attributes[key] + if !exists { + panic("Cannot remove field, " + key + " does not exist in the schema") + } + delete(attr.Attributes, key) + return attr + case schema.ListNestedAttribute: + _, exists := attr.NestedObject.Attributes[key] + if !exists { + panic("Cannot remove field, " + key + " does not exist in the schema") + } + delete(attr.NestedObject.Attributes, key) + return attr + case schema.MapNestedAttribute: + _, exists := attr.NestedObject.Attributes[key] + if !exists { + panic("Cannot remove field, " + key + " does not exist in the schema") + } + delete(attr.NestedObject.Attributes, key) + return attr + default: + panic("attribute is not nested, cannot add field") } - attr.NestedObject.Attributes[key] = newField - s.attr = attr - case schema.MapNestedAttribute: - _, exists := attr.NestedObject.Attributes[key] - if exists { - panic("Cannot add new field, " + key + " already exists in the schema") + } + + if len(path) == 0 { + s.attr = cb(s.attr) + } else { + navigateSchemaWithCallback(&s.attr, cb, path...) + } + return s +} + +func (s *CustomizableSchemaPluginFramework) AddValidator(v any, path ...string) *CustomizableSchemaPluginFramework { + cb := func(a schema.Attribute) schema.Attribute { + switch attr := a.(type) { + case schema.SingleNestedAttribute: + attr.Validators = append(attr.Validators, v.(validator.Object)) + return attr + case schema.ListNestedAttribute: + attr.Validators = append(attr.Validators, v.(validator.List)) + return attr + case schema.MapNestedAttribute: + attr.Validators = append(attr.Validators, v.(validator.Map)) + return attr + case schema.BoolAttribute: + attr.Validators = append(attr.Validators, v.(validator.Bool)) + return attr + case schema.Float64Attribute: + attr.Validators = append(attr.Validators, v.(validator.Float64)) + return attr + case schema.StringAttribute: + attr.Validators = append(attr.Validators, v.(validator.String)) + return attr + case schema.Int64Attribute: + attr.Validators = append(attr.Validators, v.(validator.Int64)) + return attr + case schema.ListAttribute: + attr.Validators = append(attr.Validators, v.(validator.List)) + return attr + case schema.MapAttribute: + attr.Validators = append(attr.Validators, v.(validator.Map)) + return attr + default: + panic(fmt.Sprintf("Unsupported type %T", s.attr)) } - attr.NestedObject.Attributes[key] = newField - s.attr = attr - default: - panic("attribute is not nested, cannot add field") } + navigateSchemaWithCallback(&s.attr, cb, path...) + return s } -func (s *CustomizableSchemaPluginFramework) RemoveField(key string) *CustomizableSchemaPluginFramework { - switch attr := s.attr.(type) { - case schema.SingleNestedAttribute: - _, exists := attr.Attributes[key] - if !exists { - panic("Cannot remove field, " + key + " does not exist in the schema") +func (s *CustomizableSchemaPluginFramework) SetOptional(path ...string) *CustomizableSchemaPluginFramework { + cb := func(a schema.Attribute) schema.Attribute { + switch attr := a.(type) { + case schema.SingleNestedAttribute: + attr.Optional = true + attr.Required = false + return attr + case schema.ListNestedAttribute: + attr.Optional = true + attr.Required = false + return attr + case schema.MapNestedAttribute: + attr.Optional = true + attr.Required = false + return attr + case schema.BoolAttribute: + attr.Optional = true + attr.Required = false + return attr + case schema.Float64Attribute: + attr.Optional = true + attr.Required = false + return attr + case schema.StringAttribute: + attr.Optional = true + attr.Required = false + return attr + case schema.Int64Attribute: + attr.Optional = true + attr.Required = false + return attr + case schema.ListAttribute: + attr.Optional = true + attr.Required = false + return attr + case schema.MapAttribute: + attr.Optional = true + attr.Required = false + return attr + default: + panic(fmt.Sprintf("Unsupported type %T", s.attr)) } - delete(attr.Attributes, key) - s.attr = attr - case schema.ListNestedAttribute: - _, exists := attr.NestedObject.Attributes[key] - if !exists { - panic("Cannot remove field, " + key + " does not exist in the schema") + } + + navigateSchemaWithCallback(&s.attr, cb, path...) + + return s +} + +func (s *CustomizableSchemaPluginFramework) SetRequired(path ...string) *CustomizableSchemaPluginFramework { + cb := func(a schema.Attribute) schema.Attribute { + switch attr := a.(type) { + case schema.SingleNestedAttribute: + attr.Optional = false + attr.Required = true + return attr + case schema.ListNestedAttribute: + attr.Optional = false + attr.Required = true + return attr + case schema.MapNestedAttribute: + attr.Optional = false + attr.Required = true + return attr + case schema.BoolAttribute: + attr.Optional = false + attr.Required = true + return attr + case schema.Float64Attribute: + attr.Optional = false + attr.Required = true + return attr + case schema.StringAttribute: + attr.Optional = false + attr.Required = true + return attr + case schema.Int64Attribute: + attr.Optional = false + attr.Required = true + return attr + case schema.ListAttribute: + attr.Optional = false + attr.Required = true + return attr + case schema.MapAttribute: + attr.Optional = false + attr.Required = true + return attr + default: + panic(fmt.Sprintf("Unsupported type %T", s.attr)) } - delete(attr.NestedObject.Attributes, key) - s.attr = attr - case schema.MapNestedAttribute: - _, exists := attr.NestedObject.Attributes[key] - if !exists { - panic("Cannot remove field, " + key + " does not exist in the schema") + } + + navigateSchemaWithCallback(&s.attr, cb, path...) + + return s +} + +func (s *CustomizableSchemaPluginFramework) SetSensitive(path ...string) *CustomizableSchemaPluginFramework { + cb := func(a schema.Attribute) schema.Attribute { + switch attr := a.(type) { + case schema.SingleNestedAttribute: + attr.Sensitive = true + return attr + case schema.ListNestedAttribute: + attr.Sensitive = true + return attr + case schema.MapNestedAttribute: + attr.Sensitive = true + return attr + case schema.BoolAttribute: + attr.Sensitive = true + return attr + case schema.Float64Attribute: + attr.Sensitive = true + return attr + case schema.StringAttribute: + attr.Sensitive = true + return attr + case schema.Int64Attribute: + attr.Sensitive = true + return attr + case schema.ListAttribute: + attr.Sensitive = true + return attr + case schema.MapAttribute: + attr.Sensitive = true + return attr + default: + panic(fmt.Sprintf("Unsupported type %T", s.attr)) } - delete(attr.NestedObject.Attributes, key) - s.attr = attr - default: - panic("attribute is not nested, cannot add field") } + navigateSchemaWithCallback(&s.attr, cb, path...) return s } // Given a attribute map, navigate through the given path, panics if the path is not valid. func MustSchemaAttributePath(attrs map[string]schema.Attribute, path ...string) schema.Attribute { - return ConstructCustomizableSchema(attrs).SchemaPath(path...).attr + attr := ConstructCustomizableSchema(attrs).attr + + res, err := navigateSchema(&attr, path...) + if err != nil { + panic(err) + } + + return res } // Helper function for navigating through schema attributes, panics if path does not exist or invalid. -func navigateSchema(s schema.Attribute, path ...string) (schema.Attribute, error) { +func navigateSchema(s *schema.Attribute, path ...string) (schema.Attribute, error) { cs := s for i, p := range path { m := attributeToMap(cs) @@ -122,10 +320,31 @@ func navigateSchema(s schema.Attribute, path ...string) (schema.Attribute, error if !ok { return nil, fmt.Errorf("missing key %s", p) } + if i == len(path)-1 { return v, nil } - cs = v + cs = &v + } + return nil, fmt.Errorf("path %v is incomplete", path) +} + +// Helper function for navigating through schema attributes, panics if path does not exist or invalid. +func navigateSchemaWithCallback(s *schema.Attribute, cb func(schema.Attribute) schema.Attribute, path ...string) (schema.Attribute, error) { + cs := s + for i, p := range path { + m := attributeToMap(cs) + + v, ok := m[p] + if !ok { + return nil, fmt.Errorf("missing key %s", p) + } + + if i == len(path)-1 { + m[p] = cb(v) + return m[p], nil + } + cs = &v } return nil, fmt.Errorf("path %v is incomplete", path) } diff --git a/common/customizable_schema_plugin_framework_test.go b/common/customizable_schema_plugin_framework_test.go index 11327d152d..c764235b95 100644 --- a/common/customizable_schema_plugin_framework_test.go +++ b/common/customizable_schema_plugin_framework_test.go @@ -1,23 +1,67 @@ package common import ( + "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/stretchr/testify/assert" ) +type stringLengthBetweenValidator struct { + Max int + Min int +} + +// Description returns a plain text description of the validator's behavior, suitable for a practitioner to understand its impact. +func (v stringLengthBetweenValidator) Description(ctx context.Context) string { + return fmt.Sprintf("string length must be between %d and %d", v.Min, v.Max) +} + +// MarkdownDescription returns a markdown formatted description of the validator's behavior, suitable for a practitioner to understand its impact. +func (v stringLengthBetweenValidator) MarkdownDescription(ctx context.Context) string { + return fmt.Sprintf("string length must be between `%d` and `%d`", v.Min, v.Max) +} + +// Validate runs the main validation logic of the validator, reading configuration data out of `req` and updating `resp` with diagnostics. +func (v stringLengthBetweenValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + // If the value is unknown or null, there is nothing to validate. + if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() { + return + } + + strLen := len(req.ConfigValue.ValueString()) + + if strLen < v.Min || strLen > v.Max { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid String Length", + fmt.Sprintf("String length must be between %d and %d, got: %d.", v.Min, v.Max, strLen), + ) + + return + } +} + func TestCustomizeSchema(t *testing.T) { scm := pluginFrameworkStructToSchema(DummyTfSdk{}, func(c CustomizableSchemaPluginFramework) CustomizableSchemaPluginFramework { c.AddNewField("new_field", schema.StringAttribute{Required: true}) - c.SchemaPath("nested").AddNewField("new_field", schema.StringAttribute{Required: true}) - c.SchemaPath("nested").AddNewField("to_be_removed", schema.StringAttribute{Required: true}) - c.SchemaPath("nested").RemoveField("to_be_removed") + c.AddNewField("new_field", schema.StringAttribute{Required: true}, "nested") + c.AddNewField("to_be_removed", schema.StringAttribute{Required: true}, "nested") + c.RemoveField("to_be_removed", "nested") + c.SetRequired("nested", "enabled") + c.SetSensitive("nested", "name") + c.AddValidator(stringLengthBetweenValidator{}, "description") return c }) assert.True(t, scm.Attributes["new_field"].IsRequired()) assert.True(t, MustSchemaAttributePath(scm.Attributes, "nested", "new_field").IsRequired()) + assert.True(t, MustSchemaAttributePath(scm.Attributes, "nested", "enabled").IsRequired()) + assert.True(t, MustSchemaAttributePath(scm.Attributes, "nested", "name").IsSensitive()) attr := MustSchemaAttributePath(scm.Attributes, "nested").(schema.SingleNestedAttribute).Attributes _, ok := attr["to_be_removed"] + assert.True(t, len(MustSchemaAttributePath(scm.Attributes, "description").(schema.StringAttribute).Validators) == 1) assert.True(t, !ok) } From 94c67dbc3fb9a81e7af4a9d5fa0cd98ccbce77a3 Mon Sep 17 00:00:00 2001 From: edwardfeng-db Date: Thu, 25 Jul 2024 22:22:14 +0200 Subject: [PATCH 3/5] update --- common/customizable_schema_plugin_framework.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/common/customizable_schema_plugin_framework.go b/common/customizable_schema_plugin_framework.go index b3e5e7c815..377f06af95 100644 --- a/common/customizable_schema_plugin_framework.go +++ b/common/customizable_schema_plugin_framework.go @@ -16,16 +16,6 @@ func ConstructCustomizableSchema(attributes map[string]schema.Attribute) *Custom return &CustomizableSchemaPluginFramework{attr: attr} } -// func (s *CustomizableSchemaPluginFramework) SchemaPath(path ...string) *CustomizableSchemaPluginFramework { -// // The attr returned here is not the original object, it's a copy. -// attr, err := navigateSchema(&s.attr, path...) -// if err != nil { -// panic(err) -// } - -// return &CustomizableSchemaPluginFramework{attr} -// } - // Converts CustomizableSchema into a map from string to Attribute. func (s *CustomizableSchemaPluginFramework) ToAttributeMap() map[string]schema.Attribute { return attributeToMap(&s.attr) From b5a65de24d6fb16320d898946f20cb5c0ce6ba0a Mon Sep 17 00:00:00 2001 From: edwardfeng-db Date: Fri, 26 Jul 2024 14:26:46 +0200 Subject: [PATCH 4/5] update --- .../customizable_schema_plugin_framework.go | 40 +++++++++++++++++++ ...stomizable_schema_plugin_framework_test.go | 2 + 2 files changed, 42 insertions(+) diff --git a/common/customizable_schema_plugin_framework.go b/common/customizable_schema_plugin_framework.go index 377f06af95..8522b1023b 100644 --- a/common/customizable_schema_plugin_framework.go +++ b/common/customizable_schema_plugin_framework.go @@ -288,6 +288,46 @@ func (s *CustomizableSchemaPluginFramework) SetSensitive(path ...string) *Custom return s } +func (s *CustomizableSchemaPluginFramework) SetDeprecated(msg string, path ...string) *CustomizableSchemaPluginFramework { + cb := func(a schema.Attribute) schema.Attribute { + switch attr := a.(type) { + case schema.SingleNestedAttribute: + attr.DeprecationMessage = msg + return attr + case schema.ListNestedAttribute: + attr.DeprecationMessage = msg + return attr + case schema.MapNestedAttribute: + attr.DeprecationMessage = msg + return attr + case schema.BoolAttribute: + attr.DeprecationMessage = msg + return attr + case schema.Float64Attribute: + attr.DeprecationMessage = msg + return attr + case schema.StringAttribute: + attr.DeprecationMessage = msg + return attr + case schema.Int64Attribute: + attr.DeprecationMessage = msg + return attr + case schema.ListAttribute: + attr.DeprecationMessage = msg + return attr + case schema.MapAttribute: + attr.DeprecationMessage = msg + return attr + default: + panic(fmt.Sprintf("Unsupported type %T", s.attr)) + } + } + + navigateSchemaWithCallback(&s.attr, cb, path...) + + return s +} + // Given a attribute map, navigate through the given path, panics if the path is not valid. func MustSchemaAttributePath(attrs map[string]schema.Attribute, path ...string) schema.Attribute { attr := ConstructCustomizableSchema(attrs).attr diff --git a/common/customizable_schema_plugin_framework_test.go b/common/customizable_schema_plugin_framework_test.go index c764235b95..ed571a80a8 100644 --- a/common/customizable_schema_plugin_framework_test.go +++ b/common/customizable_schema_plugin_framework_test.go @@ -53,6 +53,7 @@ func TestCustomizeSchema(t *testing.T) { c.RemoveField("to_be_removed", "nested") c.SetRequired("nested", "enabled") c.SetSensitive("nested", "name") + c.SetDeprecated("deprecated", "map") c.AddValidator(stringLengthBetweenValidator{}, "description") return c }) @@ -60,6 +61,7 @@ func TestCustomizeSchema(t *testing.T) { assert.True(t, MustSchemaAttributePath(scm.Attributes, "nested", "new_field").IsRequired()) assert.True(t, MustSchemaAttributePath(scm.Attributes, "nested", "enabled").IsRequired()) assert.True(t, MustSchemaAttributePath(scm.Attributes, "nested", "name").IsSensitive()) + assert.True(t, MustSchemaAttributePath(scm.Attributes, "map").GetDeprecationMessage() == "deprecated") attr := MustSchemaAttributePath(scm.Attributes, "nested").(schema.SingleNestedAttribute).Attributes _, ok := attr["to_be_removed"] assert.True(t, len(MustSchemaAttributePath(scm.Attributes, "description").(schema.StringAttribute).Validators) == 1) From a6b66bd02ed4698348f40e9207293cd2404abf3d Mon Sep 17 00:00:00 2001 From: edwardfeng-db Date: Sat, 27 Jul 2024 19:19:39 +0200 Subject: [PATCH 5/5] update --- .../customizable_schema_plugin_framework.go | 319 ++++++++---------- ...stomizable_schema_plugin_framework_test.go | 2 + 2 files changed, 141 insertions(+), 180 deletions(-) diff --git a/common/customizable_schema_plugin_framework.go b/common/customizable_schema_plugin_framework.go index 8522b1023b..ae0571b827 100644 --- a/common/customizable_schema_plugin_framework.go +++ b/common/customizable_schema_plugin_framework.go @@ -2,9 +2,9 @@ package common import ( "fmt" + "reflect" "github.com/hashicorp/terraform-plugin-framework/provider/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) type CustomizableSchemaPluginFramework struct { @@ -112,38 +112,37 @@ func (s *CustomizableSchemaPluginFramework) RemoveField(key string, path ...stri } func (s *CustomizableSchemaPluginFramework) AddValidator(v any, path ...string) *CustomizableSchemaPluginFramework { - cb := func(a schema.Attribute) schema.Attribute { - switch attr := a.(type) { - case schema.SingleNestedAttribute: - attr.Validators = append(attr.Validators, v.(validator.Object)) - return attr - case schema.ListNestedAttribute: - attr.Validators = append(attr.Validators, v.(validator.List)) - return attr - case schema.MapNestedAttribute: - attr.Validators = append(attr.Validators, v.(validator.Map)) - return attr - case schema.BoolAttribute: - attr.Validators = append(attr.Validators, v.(validator.Bool)) - return attr - case schema.Float64Attribute: - attr.Validators = append(attr.Validators, v.(validator.Float64)) - return attr - case schema.StringAttribute: - attr.Validators = append(attr.Validators, v.(validator.String)) - return attr - case schema.Int64Attribute: - attr.Validators = append(attr.Validators, v.(validator.Int64)) - return attr - case schema.ListAttribute: - attr.Validators = append(attr.Validators, v.(validator.List)) - return attr - case schema.MapAttribute: - attr.Validators = append(attr.Validators, v.(validator.Map)) - return attr - default: - panic(fmt.Sprintf("Unsupported type %T", s.attr)) + cb := func(attr schema.Attribute) schema.Attribute { + val := reflect.ValueOf(attr) + + // Make a copy of the existing attr. + newAttr := reflect.New(val.Type()).Elem() + newAttr.Set(val) + val = newAttr + + field := val.FieldByName("Validators") + if !field.IsValid() { + panic(fmt.Sprintf("Validators field not found in %T", attr)) + } + if field.Kind() != reflect.Slice { + panic(fmt.Sprintf("Validators field is not a slice in %T", attr)) + } + if !field.CanSet() { + panic(fmt.Sprintf("Validators field cannot be set in %T", attr)) } + + elemType := field.Type().Elem() + value := reflect.ValueOf(v) + + if !value.Type().AssignableTo(elemType) { + panic(fmt.Sprintf("Value of type %T is not assignable to slice of %s", v, elemType)) + } + + // Append the value + newSlice := reflect.Append(field, value) + field.Set(newSlice) + + return val.Interface().(schema.Attribute) } navigateSchemaWithCallback(&s.attr, cb, path...) @@ -152,47 +151,38 @@ func (s *CustomizableSchemaPluginFramework) AddValidator(v any, path ...string) } func (s *CustomizableSchemaPluginFramework) SetOptional(path ...string) *CustomizableSchemaPluginFramework { - cb := func(a schema.Attribute) schema.Attribute { - switch attr := a.(type) { - case schema.SingleNestedAttribute: - attr.Optional = true - attr.Required = false - return attr - case schema.ListNestedAttribute: - attr.Optional = true - attr.Required = false - return attr - case schema.MapNestedAttribute: - attr.Optional = true - attr.Required = false - return attr - case schema.BoolAttribute: - attr.Optional = true - attr.Required = false - return attr - case schema.Float64Attribute: - attr.Optional = true - attr.Required = false - return attr - case schema.StringAttribute: - attr.Optional = true - attr.Required = false - return attr - case schema.Int64Attribute: - attr.Optional = true - attr.Required = false - return attr - case schema.ListAttribute: - attr.Optional = true - attr.Required = false - return attr - case schema.MapAttribute: - attr.Optional = true - attr.Required = false - return attr - default: - panic(fmt.Sprintf("Unsupported type %T", s.attr)) + cb := func(attr schema.Attribute) schema.Attribute { + // Get the concrete value stored in the interface + v := reflect.ValueOf(attr) + + // Make a new addressable value and copy the original value into it + newAttr := reflect.New(v.Type()).Elem() + newAttr.Set(v) + v = newAttr + + field := v.FieldByName("Required") + if field.IsValid() && field.CanSet() { + if field.Kind() == reflect.Bool { + field.SetBool(false) + } else { + panic(fmt.Sprintf("Required is not a bool field in %T", attr)) + } + } else { + panic(fmt.Sprintf("Required field not found or cannot be set in %T", attr)) + } + + field = v.FieldByName("Optional") + if field.IsValid() && field.CanSet() { + if field.Kind() == reflect.Bool { + field.SetBool(true) + } else { + panic(fmt.Sprintf("Optional is not a bool field in %T", attr)) + } + } else { + panic(fmt.Sprintf("Optional field not found or cannot be set in %T", attr)) } + + return v.Interface().(schema.Attribute) } navigateSchemaWithCallback(&s.attr, cb, path...) @@ -201,47 +191,38 @@ func (s *CustomizableSchemaPluginFramework) SetOptional(path ...string) *Customi } func (s *CustomizableSchemaPluginFramework) SetRequired(path ...string) *CustomizableSchemaPluginFramework { - cb := func(a schema.Attribute) schema.Attribute { - switch attr := a.(type) { - case schema.SingleNestedAttribute: - attr.Optional = false - attr.Required = true - return attr - case schema.ListNestedAttribute: - attr.Optional = false - attr.Required = true - return attr - case schema.MapNestedAttribute: - attr.Optional = false - attr.Required = true - return attr - case schema.BoolAttribute: - attr.Optional = false - attr.Required = true - return attr - case schema.Float64Attribute: - attr.Optional = false - attr.Required = true - return attr - case schema.StringAttribute: - attr.Optional = false - attr.Required = true - return attr - case schema.Int64Attribute: - attr.Optional = false - attr.Required = true - return attr - case schema.ListAttribute: - attr.Optional = false - attr.Required = true - return attr - case schema.MapAttribute: - attr.Optional = false - attr.Required = true - return attr - default: - panic(fmt.Sprintf("Unsupported type %T", s.attr)) + cb := func(attr schema.Attribute) schema.Attribute { + // Get the concrete value stored in the interface + v := reflect.ValueOf(attr) + + // Make a new addressable value and copy the original value into it + newAttr := reflect.New(v.Type()).Elem() + newAttr.Set(v) + v = newAttr + + field := v.FieldByName("Required") + if field.IsValid() && field.CanSet() { + if field.Kind() == reflect.Bool { + field.SetBool(true) + } else { + panic(fmt.Sprintf("Required is not a bool field in %T", attr)) + } + } else { + panic(fmt.Sprintf("Required field not found or cannot be set in %T", attr)) } + + field = v.FieldByName("Optional") + if field.IsValid() && field.CanSet() { + if field.Kind() == reflect.Bool { + field.SetBool(false) + } else { + panic(fmt.Sprintf("Optional is not a bool field in %T", attr)) + } + } else { + panic(fmt.Sprintf("Optional field not found or cannot be set in %T", attr)) + } + + return v.Interface().(schema.Attribute) } navigateSchemaWithCallback(&s.attr, cb, path...) @@ -250,38 +231,27 @@ func (s *CustomizableSchemaPluginFramework) SetRequired(path ...string) *Customi } func (s *CustomizableSchemaPluginFramework) SetSensitive(path ...string) *CustomizableSchemaPluginFramework { - cb := func(a schema.Attribute) schema.Attribute { - switch attr := a.(type) { - case schema.SingleNestedAttribute: - attr.Sensitive = true - return attr - case schema.ListNestedAttribute: - attr.Sensitive = true - return attr - case schema.MapNestedAttribute: - attr.Sensitive = true - return attr - case schema.BoolAttribute: - attr.Sensitive = true - return attr - case schema.Float64Attribute: - attr.Sensitive = true - return attr - case schema.StringAttribute: - attr.Sensitive = true - return attr - case schema.Int64Attribute: - attr.Sensitive = true - return attr - case schema.ListAttribute: - attr.Sensitive = true - return attr - case schema.MapAttribute: - attr.Sensitive = true - return attr - default: - panic(fmt.Sprintf("Unsupported type %T", s.attr)) + cb := func(attr schema.Attribute) schema.Attribute { + // Get the concrete value stored in the interface + v := reflect.ValueOf(attr) + + // Make a new addressable value and copy the original value into it + newAttr := reflect.New(v.Type()).Elem() + newAttr.Set(v) + v = newAttr + + field := v.FieldByName("Sensitive") + if field.IsValid() && field.CanSet() { + if field.Kind() == reflect.Bool { + field.SetBool(true) + } else { + panic(fmt.Sprintf("Sensitive is not a bool field in %T", attr)) + } + } else { + panic(fmt.Sprintf("Sensitive field not found or cannot be set in %T", attr)) } + + return v.Interface().(schema.Attribute) } navigateSchemaWithCallback(&s.attr, cb, path...) @@ -289,38 +259,27 @@ func (s *CustomizableSchemaPluginFramework) SetSensitive(path ...string) *Custom } func (s *CustomizableSchemaPluginFramework) SetDeprecated(msg string, path ...string) *CustomizableSchemaPluginFramework { - cb := func(a schema.Attribute) schema.Attribute { - switch attr := a.(type) { - case schema.SingleNestedAttribute: - attr.DeprecationMessage = msg - return attr - case schema.ListNestedAttribute: - attr.DeprecationMessage = msg - return attr - case schema.MapNestedAttribute: - attr.DeprecationMessage = msg - return attr - case schema.BoolAttribute: - attr.DeprecationMessage = msg - return attr - case schema.Float64Attribute: - attr.DeprecationMessage = msg - return attr - case schema.StringAttribute: - attr.DeprecationMessage = msg - return attr - case schema.Int64Attribute: - attr.DeprecationMessage = msg - return attr - case schema.ListAttribute: - attr.DeprecationMessage = msg - return attr - case schema.MapAttribute: - attr.DeprecationMessage = msg - return attr - default: - panic(fmt.Sprintf("Unsupported type %T", s.attr)) + cb := func(attr schema.Attribute) schema.Attribute { + // Get the concrete value stored in the interface + v := reflect.ValueOf(attr) + + // Make a new addressable value and copy the original value into it + newAttr := reflect.New(v.Type()).Elem() + newAttr.Set(v) + v = newAttr + + field := v.FieldByName("DeprecationMessage") + if field.IsValid() && field.CanSet() { + if field.Kind() == reflect.String { + field.SetString(msg) + } else { + panic(fmt.Sprintf("DeprecationMessage is not a string field in %T", attr)) + } + } else { + panic(fmt.Sprintf("DeprecationMessage field not found or cannot be set in %T", attr)) } + + return v.Interface().(schema.Attribute) } navigateSchemaWithCallback(&s.attr, cb, path...) @@ -342,9 +301,9 @@ func MustSchemaAttributePath(attrs map[string]schema.Attribute, path ...string) // Helper function for navigating through schema attributes, panics if path does not exist or invalid. func navigateSchema(s *schema.Attribute, path ...string) (schema.Attribute, error) { - cs := s + current_scm := s for i, p := range path { - m := attributeToMap(cs) + m := attributeToMap(current_scm) v, ok := m[p] if !ok { @@ -354,16 +313,16 @@ func navigateSchema(s *schema.Attribute, path ...string) (schema.Attribute, erro if i == len(path)-1 { return v, nil } - cs = &v + current_scm = &v } return nil, fmt.Errorf("path %v is incomplete", path) } // Helper function for navigating through schema attributes, panics if path does not exist or invalid. func navigateSchemaWithCallback(s *schema.Attribute, cb func(schema.Attribute) schema.Attribute, path ...string) (schema.Attribute, error) { - cs := s + current_scm := s for i, p := range path { - m := attributeToMap(cs) + m := attributeToMap(current_scm) v, ok := m[p] if !ok { @@ -374,7 +333,7 @@ func navigateSchemaWithCallback(s *schema.Attribute, cb func(schema.Attribute) s m[p] = cb(v) return m[p], nil } - cs = &v + current_scm = &v } return nil, fmt.Errorf("path %v is incomplete", path) } diff --git a/common/customizable_schema_plugin_framework_test.go b/common/customizable_schema_plugin_framework_test.go index ed571a80a8..3d0b31b0e9 100644 --- a/common/customizable_schema_plugin_framework_test.go +++ b/common/customizable_schema_plugin_framework_test.go @@ -52,6 +52,7 @@ func TestCustomizeSchema(t *testing.T) { c.AddNewField("to_be_removed", schema.StringAttribute{Required: true}, "nested") c.RemoveField("to_be_removed", "nested") c.SetRequired("nested", "enabled") + c.SetOptional("description") c.SetSensitive("nested", "name") c.SetDeprecated("deprecated", "map") c.AddValidator(stringLengthBetweenValidator{}, "description") @@ -62,6 +63,7 @@ func TestCustomizeSchema(t *testing.T) { assert.True(t, MustSchemaAttributePath(scm.Attributes, "nested", "enabled").IsRequired()) assert.True(t, MustSchemaAttributePath(scm.Attributes, "nested", "name").IsSensitive()) assert.True(t, MustSchemaAttributePath(scm.Attributes, "map").GetDeprecationMessage() == "deprecated") + assert.True(t, scm.Attributes["description"].IsOptional()) attr := MustSchemaAttributePath(scm.Attributes, "nested").(schema.SingleNestedAttribute).Attributes _, ok := attr["to_be_removed"] assert.True(t, len(MustSchemaAttributePath(scm.Attributes, "description").(schema.StringAttribute).Validators) == 1)