From 996062f8129e45c2613b2c4000cd1f9e18710921 Mon Sep 17 00:00:00 2001 From: Artur Sabirov Date: Sun, 29 Mar 2026 22:01:25 +0200 Subject: [PATCH] fix: use int type for RoutineFolder ID and Routine FolderID fields The Hevy API returns routine folder IDs as JSON numbers, but the Go structs declared them as strings, causing json.Unmarshal to fail with "cannot unmarshal number into Go struct field RoutineFolder.id of type string". Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/folder/create.go | 2 +- cmd/folder/delete.go | 15 ++++++++++----- cmd/folder/get.go | 17 +++++++++++------ cmd/folder/list.go | 2 +- cmd/folder/update.go | 15 ++++++++++----- cmd/routine/get.go | 2 +- cmd/routine/list.go | 9 +++++++-- internal/api/client.go | 12 ++++++------ internal/api/types.go | 4 ++-- 9 files changed, 49 insertions(+), 29 deletions(-) diff --git a/cmd/folder/create.go b/cmd/folder/create.go index 044eb82..96652d0 100644 --- a/cmd/folder/create.go +++ b/cmd/folder/create.go @@ -94,7 +94,7 @@ func runFolderCreate(cmd *cobra.Command, args []string) error { fmt.Println(out) } else { fmt.Println("Folder created successfully!") - fmt.Printf("ID: %s\n", folder.ID) + fmt.Printf("ID: %d\n", folder.ID) fmt.Printf("Title: %s\n", folder.Title) } diff --git a/cmd/folder/delete.go b/cmd/folder/delete.go index a864e7e..0e61926 100644 --- a/cmd/folder/delete.go +++ b/cmd/folder/delete.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "os" + "strconv" "strings" "github.com/spf13/cobra" @@ -54,9 +55,13 @@ func runFolderDelete(cmd *cobra.Command, args []string) error { client := api.NewClient(apiKey) - var folderID string + var folderID int if len(args) > 0 { - folderID = args[0] + var err error + folderID, err = strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid folder ID: %s", args[0]) + } } else { // Interactive mode - let user select from folders selected, err := prompt.SearchSelect(prompt.SearchSelectConfig{ @@ -71,7 +76,7 @@ func runFolderDelete(cmd *cobra.Command, args []string) error { options := make([]prompt.SelectOption, len(folders.RoutineFolders)) for i, f := range folders.RoutineFolders { options[i] = prompt.SelectOption{ - ID: f.ID, + ID: fmt.Sprintf("%d", f.ID), Title: f.Title, Description: fmt.Sprintf("Index: %d", f.Index), } @@ -82,7 +87,7 @@ func runFolderDelete(cmd *cobra.Command, args []string) error { if err != nil { return err } - folderID = selected.ID + folderID, _ = strconv.Atoi(selected.ID) } // Get folder details first to show what we're deleting @@ -93,7 +98,7 @@ func runFolderDelete(cmd *cobra.Command, args []string) error { // Confirm deletion unless --force is used if !deleteForce { - fmt.Printf("Are you sure you want to delete folder '%s' (%s)?\n", folder.Title, folder.ID) + fmt.Printf("Are you sure you want to delete folder '%s' (%d)?\n", folder.Title, folder.ID) fmt.Printf("This action cannot be undone. Routines inside will become unorganized.\n") fmt.Print("Type 'yes' to confirm: ") diff --git a/cmd/folder/get.go b/cmd/folder/get.go index 84f67c3..8cf9288 100644 --- a/cmd/folder/get.go +++ b/cmd/folder/get.go @@ -3,6 +3,7 @@ package folder import ( "fmt" "os" + "strconv" "github.com/spf13/cobra" @@ -38,9 +39,13 @@ func runGet(cmd *cobra.Command, args []string) error { client := api.NewClient(apiKey) - var folderID string + var folderID int if len(args) > 0 { - folderID = args[0] + var err error + folderID, err = strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid folder ID: %s", args[0]) + } } else { // Interactive mode - let user select from folders selected, err := prompt.SearchSelect(prompt.SearchSelectConfig{ @@ -55,7 +60,7 @@ func runGet(cmd *cobra.Command, args []string) error { options := make([]prompt.SelectOption, len(folders.RoutineFolders)) for i, f := range folders.RoutineFolders { options[i] = prompt.SelectOption{ - ID: f.ID, + ID: fmt.Sprintf("%d", f.ID), Title: f.Title, Description: fmt.Sprintf("Index: %d", f.Index), } @@ -66,7 +71,7 @@ func runGet(cmd *cobra.Command, args []string) error { if err != nil { return err } - folderID = selected.ID + folderID, _ = strconv.Atoi(selected.ID) } // Search for the folder in the list @@ -93,7 +98,7 @@ func runGet(cmd *cobra.Command, args []string) error { } if folder == nil { - return fmt.Errorf("folder not found: %s", folderID) + return fmt.Errorf("folder not found: %d", folderID) } // Determine output format @@ -123,7 +128,7 @@ func runGet(cmd *cobra.Command, args []string) error { func printFolderDetails(f *api.RoutineFolder, cfg *config.Config) { fmt.Printf("Folder: %s\n", f.Title) - fmt.Printf("ID: %s\n", f.ID) + fmt.Printf("ID: %d\n", f.ID) fmt.Printf("Index: %d\n", f.Index) fmt.Printf("Created: %s\n", f.CreatedAt.Format(cfg.Display.DateFormat)) fmt.Printf("Updated: %s\n", f.UpdatedAt.Format(cfg.Display.DateFormat)) diff --git a/cmd/folder/list.go b/cmd/folder/list.go index b4d7784..f838c47 100644 --- a/cmd/folder/list.go +++ b/cmd/folder/list.go @@ -110,7 +110,7 @@ func runList(cmd *cobra.Command, args []string) error { for _, f := range allFolders { updated := f.UpdatedAt.Format(cfg.Display.DateFormat) table.AddRow( - f.ID, + fmt.Sprintf("%d", f.ID), f.Title, fmt.Sprintf("%d", f.Index), updated, diff --git a/cmd/folder/update.go b/cmd/folder/update.go index 124811d..8054269 100644 --- a/cmd/folder/update.go +++ b/cmd/folder/update.go @@ -3,6 +3,7 @@ package folder import ( "fmt" "os" + "strconv" "github.com/spf13/cobra" @@ -48,9 +49,13 @@ func runFolderUpdate(cmd *cobra.Command, args []string) error { client := api.NewClient(apiKey) - var folderID string + var folderID int if len(args) > 0 { - folderID = args[0] + var err error + folderID, err = strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid folder ID: %s", args[0]) + } } else { // Interactive mode - let user select from folders selected, err := prompt.SearchSelect(prompt.SearchSelectConfig{ @@ -65,7 +70,7 @@ func runFolderUpdate(cmd *cobra.Command, args []string) error { options := make([]prompt.SelectOption, len(folders.RoutineFolders)) for i, f := range folders.RoutineFolders { options[i] = prompt.SelectOption{ - ID: f.ID, + ID: fmt.Sprintf("%d", f.ID), Title: f.Title, Description: fmt.Sprintf("Index: %d", f.Index), } @@ -76,7 +81,7 @@ func runFolderUpdate(cmd *cobra.Command, args []string) error { if err != nil { return err } - folderID = selected.ID + folderID, _ = strconv.Atoi(selected.ID) } // Determine output format @@ -112,7 +117,7 @@ func runFolderUpdate(cmd *cobra.Command, args []string) error { fmt.Println(out) } else { fmt.Println("Folder updated successfully!") - fmt.Printf("ID: %s\n", folder.ID) + fmt.Printf("ID: %d\n", folder.ID) fmt.Printf("Title: %s\n", folder.Title) } diff --git a/cmd/routine/get.go b/cmd/routine/get.go index 2fb283c..4931513 100644 --- a/cmd/routine/get.go +++ b/cmd/routine/get.go @@ -105,7 +105,7 @@ func printRoutineDetails(r *api.Routine, cfg *config.Config, formatter output.Fo fmt.Printf("ID: %s\n", r.ID) if r.FolderID != nil { - fmt.Printf("Folder: %s\n", *r.FolderID) + fmt.Printf("Folder: %d\n", *r.FolderID) } fmt.Printf("Created: %s\n", r.CreatedAt.Format(cfg.Display.DateFormat)) diff --git a/cmd/routine/list.go b/cmd/routine/list.go index ea2fd5f..727a7cb 100644 --- a/cmd/routine/list.go +++ b/cmd/routine/list.go @@ -3,6 +3,7 @@ package routine import ( "fmt" "os" + "strconv" "github.com/spf13/cobra" @@ -94,9 +95,13 @@ func runList(cmd *cobra.Command, args []string) error { // Filter by folder if specified if listFolder != "" { + folderID, err := strconv.Atoi(listFolder) + if err != nil { + return fmt.Errorf("invalid folder ID: %s", listFolder) + } var filtered []api.Routine for _, r := range allRoutines { - if r.FolderID != nil && *r.FolderID == listFolder { + if r.FolderID != nil && *r.FolderID == folderID { filtered = append(filtered, r) } } @@ -120,7 +125,7 @@ func runList(cmd *cobra.Command, args []string) error { for _, r := range allRoutines { folder := "-" if r.FolderID != nil { - folder = *r.FolderID + folder = fmt.Sprintf("%d", *r.FolderID) } updated := r.UpdatedAt.Format(cfg.Display.DateFormat) diff --git a/internal/api/client.go b/internal/api/client.go index 425dd4c..57007a8 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -403,11 +403,11 @@ func (c *Client) CreateRoutineFolder(req *CreateRoutineFolderRequest) (*RoutineF } // GetRoutineFolder fetches a single routine folder by ID -func (c *Client) GetRoutineFolder(id string) (*RoutineFolder, error) { +func (c *Client) GetRoutineFolder(id int) (*RoutineFolder, error) { var result RoutineFolderResponse resp, err := c.httpClient.R(). SetResult(&result). - Get("/routine_folders/" + id) + Get(fmt.Sprintf("/routine_folders/%d", id)) if err != nil { return nil, &APIError{ @@ -532,12 +532,12 @@ func (c *Client) DeleteRoutine(id string) error { } // UpdateRoutineFolder updates an existing routine folder -func (c *Client) UpdateRoutineFolder(id string, req *UpdateRoutineFolderRequest) (*RoutineFolder, error) { +func (c *Client) UpdateRoutineFolder(id int, req *UpdateRoutineFolderRequest) (*RoutineFolder, error) { var result RoutineFolderResponse resp, err := c.httpClient.R(). SetBody(req). SetResult(&result). - Put("/routine_folders/" + id) + Put(fmt.Sprintf("/routine_folders/%d", id)) if err != nil { return nil, &APIError{ @@ -554,9 +554,9 @@ func (c *Client) UpdateRoutineFolder(id string, req *UpdateRoutineFolderRequest) } // DeleteRoutineFolder deletes a routine folder by ID -func (c *Client) DeleteRoutineFolder(id string) error { +func (c *Client) DeleteRoutineFolder(id int) error { resp, err := c.httpClient.R(). - Delete("/routine_folders/" + id) + Delete(fmt.Sprintf("/routine_folders/%d", id)) if err != nil { return &APIError{ diff --git a/internal/api/types.go b/internal/api/types.go index 463102f..20eaf8a 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -68,7 +68,7 @@ const ( type Routine struct { ID string `json:"id"` Title string `json:"title"` - FolderID *string `json:"folder_id,omitempty"` + FolderID *int `json:"folder_id,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Exercises []Exercise `json:"exercises"` @@ -76,7 +76,7 @@ type Routine struct { // RoutineFolder represents a folder for organizing routines type RoutineFolder struct { - ID string `json:"id"` + ID int `json:"id"` Title string `json:"title"` Index int `json:"index"` CreatedAt time.Time `json:"created_at"`