@@ -5,6 +5,11 @@ import (
55 "encoding/json"
66 "fmt"
77 "strings"
8+
9+ "github.com/hashicorp/terraform-plugin-framework/attr"
10+ rsschema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
11+ "github.com/hashicorp/terraform-plugin-framework/types"
12+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
813)
914
1015type Locationer interface {
@@ -107,3 +112,169 @@ func CreateXpathForAttributeWithAncestors(ancestors []Ancestor, attribute string
107112 xpath = append (xpath , "/" + attribute )
108113 return strings .Join (xpath , "" ), nil
109114}
115+
116+ // TypesObjectToMap converts a Terraform types.Object to a map[string]interface{} for JSON marshaling.
117+ // This allows marshaling partial objects without requiring all struct fields.
118+ // If schemaAttr is provided, validates the object against the schema to catch unknown fields.
119+ func TypesObjectToMap (obj types.Object , schemaAttr ... rsschema.Attribute ) (interface {}, error ) {
120+ if obj .IsNull () {
121+ return nil , nil
122+ }
123+
124+ attrs := obj .Attributes ()
125+ result := make (map [string ]interface {})
126+
127+ // Validate against schema if provided
128+ if len (schemaAttr ) > 0 && schemaAttr [0 ] != nil {
129+ schema , ok := schemaAttr [0 ].(rsschema.SingleNestedAttribute )
130+ if ok {
131+ // Validate no unknown fields
132+ for key := range attrs {
133+ if _ , exists := schema .Attributes [key ]; ! exists {
134+ return nil , fmt .Errorf ("unknown field %q in location object" , key )
135+ }
136+ }
137+ // Validate not empty
138+ if len (attrs ) == 0 {
139+ return nil , fmt .Errorf ("location object cannot be empty" )
140+ }
141+ }
142+ }
143+
144+ for key , val := range attrs {
145+ switch v := val .(type ) {
146+ case types.Object :
147+ // For nested objects, don't validate (no schema available)
148+ nested , err := TypesObjectToMap (v )
149+ if err != nil {
150+ return nil , err
151+ }
152+ result [key ] = nested
153+ case types.String :
154+ if ! v .IsNull () {
155+ result [key ] = v .ValueString ()
156+ }
157+ case types.Bool :
158+ if ! v .IsNull () {
159+ result [key ] = v .ValueBool ()
160+ }
161+ case types.Int64 :
162+ if ! v .IsNull () {
163+ result [key ] = v .ValueInt64 ()
164+ }
165+ case types.Float64 :
166+ if ! v .IsNull () {
167+ result [key ] = v .ValueFloat64 ()
168+ }
169+ case types.List :
170+ if ! v .IsNull () {
171+ var list []interface {}
172+ for _ , elem := range v .Elements () {
173+ switch e := elem .(type ) {
174+ case types.Object :
175+ nested , err := TypesObjectToMap (e )
176+ if err != nil {
177+ return nil , err
178+ }
179+ list = append (list , nested )
180+ case types.String :
181+ if ! e .IsNull () {
182+ list = append (list , e .ValueString ())
183+ }
184+ default :
185+ list = append (list , elem )
186+ }
187+ }
188+ result [key ] = list
189+ }
190+ case types.Map :
191+ if ! v .IsNull () {
192+ mapResult := make (map [string ]interface {})
193+ for k , mapVal := range v .Elements () {
194+ switch mv := mapVal .(type ) {
195+ case types.String :
196+ if ! mv .IsNull () {
197+ mapResult [k ] = mv .ValueString ()
198+ }
199+ default :
200+ mapResult [k ] = mapVal
201+ }
202+ }
203+ result [key ] = mapResult
204+ }
205+ }
206+ }
207+
208+ return result , nil
209+ }
210+
211+ // MapToTypesObject converts a map[string]interface{} to a Terraform types.Object using the provided schema.
212+ // This automatically handles missing fields by creating typed null values.
213+ // Validates that the map doesn't contain unknown fields or is empty.
214+ func MapToTypesObject (data map [string ]interface {}, schemaAttr rsschema.Attribute ) (types.Object , error ) {
215+ schema , ok := schemaAttr .(rsschema.SingleNestedAttribute )
216+ if ! ok {
217+ return types .ObjectNull (nil ), fmt .Errorf ("schema attribute is not a SingleNestedAttribute" )
218+ }
219+
220+ attrTypes := make (map [string ]attr.Type )
221+ attrValues := make (map [string ]attr.Value )
222+
223+ for name , schemaField := range schema .Attributes {
224+ attrTypes [name ] = schemaField .GetType ()
225+
226+ if val , exists := data [name ]; exists && val != nil {
227+ // Convert JSON value to appropriate terraform type
228+ switch schemaField .GetType ().(type ) {
229+ case basetypes.ObjectType :
230+ // Recursively handle nested objects
231+ nestedMap , ok := val .(map [string ]interface {})
232+ if ! ok {
233+ return types .ObjectNull (attrTypes ), fmt .Errorf ("expected map for nested object %s" , name )
234+ }
235+ nestedObj , err := MapToTypesObject (nestedMap , schemaField )
236+ if err != nil {
237+ return types .ObjectNull (attrTypes ), err
238+ }
239+ attrValues [name ] = nestedObj
240+ case basetypes.StringType :
241+ strVal , ok := val .(string )
242+ if ! ok {
243+ return types .ObjectNull (attrTypes ), fmt .Errorf ("expected string for field %s" , name )
244+ }
245+ attrValues [name ] = types .StringValue (strVal )
246+ default :
247+ // For other types, try to set them as-is
248+ attrValues [name ] = types .StringValue (fmt .Sprintf ("%v" , val ))
249+ }
250+ } else {
251+ // Create typed null for missing fields
252+ switch schemaField .GetType ().(type ) {
253+ case basetypes.ObjectType :
254+ objType := schemaField .GetType ().(basetypes.ObjectType )
255+ attrValues [name ] = types .ObjectNull (objType .AttrTypes )
256+ default :
257+ // For non-object types, create a generic null
258+ attrValues [name ] = types .StringNull ()
259+ }
260+ }
261+ }
262+
263+ // Validate no unknown fields in input
264+ for key := range data {
265+ if _ , exists := schema .Attributes [key ]; ! exists {
266+ return types .ObjectNull (attrTypes ), fmt .Errorf ("unknown field %q in location object" , key )
267+ }
268+ }
269+
270+ // Validate not empty
271+ if len (data ) == 0 {
272+ return types .ObjectNull (attrTypes ), fmt .Errorf ("location object cannot be empty" )
273+ }
274+
275+ obj , diags := types .ObjectValue (attrTypes , attrValues )
276+ if diags .HasError () {
277+ return types .ObjectNull (attrTypes ), fmt .Errorf ("failed to create object: %v" , diags .Errors ())
278+ }
279+ return obj , nil
280+ }
0 commit comments