package netbox import ( "context" "fmt" nb "github.com/netbox-community/go-netbox/v4" ) // ParseCustomFields maps NetBox's map[string]interface{} custom fields response // to the typed CustomFields struct. NetBox returns values as interface{} — we // perform safe type assertions for each expected field. func ParseCustomFields(raw map[string]interface{}) CustomFields { cf := CustomFields{} if raw == nil { return cf } if v, ok := raw["hw_id"].(string); ok { cf.HWID = v } if v, ok := raw["catalog_status"].(string); ok { cf.CatalogStatus = v } if v, ok := raw["product_url"].(string); ok { cf.ProductURL = v } if v, ok := raw["firmware_version"].(string); ok { cf.FirmwareVersion = v } if v, ok := raw["test_date"].(string); ok { cf.TestDate = v } if v, ok := raw["test_data"].(string); ok { cf.TestData = v } if v, ok := raw["ai_notes"].(string); ok { cf.AINotes = v } // photo_urls is a multi-value field — NetBox returns []interface{} if v, ok := raw["photo_urls"].([]interface{}); ok { urls := make([]string, 0, len(v)) for _, u := range v { if s, ok := u.(string); ok { urls = append(urls, s) } } cf.PhotoURLs = urls } return cf } // BuildCustomFieldsPatch constructs the flat map[string]interface{} payload // required by NetBox PATCH endpoints. Only include fields that are non-empty // to avoid accidentally clearing existing values. // // NetBox custom field write format differs from read format: // - Text/URL/date fields: send string value directly // - Selection fields (catalog_status): send the choice value as string // - Multi-value fields (photo_urls): send []string directly func BuildCustomFieldsPatch(hwID, catalogStatus string, photoURLs []string) map[string]interface{} { patch := make(map[string]interface{}) if hwID != "" { patch["hw_id"] = hwID } if catalogStatus != "" { patch["catalog_status"] = catalogStatus } if len(photoURLs) > 0 { patch["photo_urls"] = photoURLs } return patch } // BuildFullCustomFieldsPatch constructs a patch with all custom fields. // Use for initial record creation where all fields should be set. func BuildFullCustomFieldsPatch(cf CustomFields) map[string]interface{} { patch := make(map[string]interface{}) if cf.HWID != "" { patch["hw_id"] = cf.HWID } if cf.CatalogStatus != "" { patch["catalog_status"] = cf.CatalogStatus } if cf.ProductURL != "" { patch["product_url"] = cf.ProductURL } if cf.FirmwareVersion != "" { patch["firmware_version"] = cf.FirmwareVersion } if cf.TestDate != "" { patch["test_date"] = cf.TestDate } if cf.TestData != "" { patch["test_data"] = cf.TestData } if cf.AINotes != "" { patch["ai_notes"] = cf.AINotes } if len(cf.PhotoURLs) > 0 { patch["photo_urls"] = cf.PhotoURLs } return patch } // PatchCustomFields updates the custom fields of a device identified by deviceID. // Uses go-netbox v4 PatchedWritableDeviceWithConfigContextRequest to send a partial update. func (c *Client) PatchCustomFields(ctx context.Context, deviceID int64, patch map[string]interface{}) error { patchReq := nb.PatchedWritableDeviceWithConfigContextRequest{} patchReq.SetCustomFields(patch) _, _, err := c.api.DcimAPI.DcimDevicesPartialUpdate(ctx, int32(deviceID)). PatchedWritableDeviceWithConfigContextRequest(patchReq).Execute() if err != nil { return fmt.Errorf("patch custom fields for device %d: %w", deviceID, err) } return nil }