From a34a40cb12eaf99a7c24f89b450f5121410324f1 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Thu, 15 Jan 2026 20:23:53 +0100 Subject: [PATCH] Add IntOrFalse type to handle polymorphic API responses Kanboard API returns int (ID) on success and false (bool) on failure for create operations. Add IntOrFalse type that handles both cases and update CreateTask, CreateComment, CreateTag, CreateTaskFile, and CreateTaskLink to use it. --- comments.go | 4 ++-- files.go | 4 ++-- links.go | 4 ++-- tags.go | 4 ++-- tasks.go | 4 ++-- types.go | 28 ++++++++++++++++++++++++++++ types_test.go | 25 +++++++++++++++++++++++++ 7 files changed, 63 insertions(+), 10 deletions(-) diff --git a/comments.go b/comments.go index 53a1a0f..72f3b3f 100644 --- a/comments.go +++ b/comments.go @@ -42,7 +42,7 @@ func (c *Client) CreateComment(ctx context.Context, taskID, userID int, content "content": content, } - var commentID int + var commentID IntOrFalse if err := c.call(ctx, "createComment", params, &commentID); err != nil { return nil, fmt.Errorf("createComment: %w", err) } @@ -52,7 +52,7 @@ func (c *Client) CreateComment(ctx context.Context, taskID, userID int, content } // Fetch the created comment to return full details - return c.GetComment(ctx, commentID) + return c.GetComment(ctx, int(commentID)) } // UpdateComment updates the content of a comment. diff --git a/files.go b/files.go index 929db4e..5ed3d8f 100644 --- a/files.go +++ b/files.go @@ -29,7 +29,7 @@ func (c *Client) CreateTaskFile(ctx context.Context, projectID, taskID int, file "blob": base64.StdEncoding.EncodeToString(content), } - var result int + var result IntOrFalse if err := c.call(ctx, "createTaskFile", params, &result); err != nil { return 0, fmt.Errorf("createTaskFile: %w", err) } @@ -38,7 +38,7 @@ func (c *Client) CreateTaskFile(ctx context.Context, projectID, taskID int, file return 0, fmt.Errorf("createTaskFile: failed to upload file") } - return result, nil + return int(result), nil } // DownloadTaskFile downloads a file's content by its ID. diff --git a/links.go b/links.go index 1d26007..dc1e65c 100644 --- a/links.go +++ b/links.go @@ -27,7 +27,7 @@ func (c *Client) CreateTaskLink(ctx context.Context, taskID, oppositeTaskID, lin "link_id": linkID, } - var result int + var result IntOrFalse if err := c.call(ctx, "createTaskLink", params, &result); err != nil { return 0, fmt.Errorf("createTaskLink: %w", err) } @@ -36,7 +36,7 @@ func (c *Client) CreateTaskLink(ctx context.Context, taskID, oppositeTaskID, lin return 0, fmt.Errorf("createTaskLink: failed to create link") } - return result, nil + return int(result), nil } // RemoveTaskLink deletes a task link. diff --git a/tags.go b/tags.go index 4f40b20..de5b21e 100644 --- a/tags.go +++ b/tags.go @@ -75,11 +75,11 @@ func (c *Client) CreateTag(ctx context.Context, projectID int, name, colorID str params["color_id"] = colorID } - var result int + var result IntOrFalse if err := c.call(ctx, "createTag", params, &result); err != nil { return 0, fmt.Errorf("createTag: %w", err) } - return result, nil + return int(result), nil } // UpdateTag updates an existing tag's name and/or color. diff --git a/tasks.go b/tasks.go index 4d8211c..866e444 100644 --- a/tasks.go +++ b/tasks.go @@ -41,7 +41,7 @@ func (c *Client) GetAllTasks(ctx context.Context, projectID int, status TaskStat // CreateTask creates a new task and returns the created task. func (c *Client) CreateTask(ctx context.Context, req CreateTaskRequest) (*Task, error) { - var taskID int + var taskID IntOrFalse if err := c.call(ctx, "createTask", req, &taskID); err != nil { return nil, fmt.Errorf("createTask: %w", err) } @@ -51,7 +51,7 @@ func (c *Client) CreateTask(ctx context.Context, req CreateTaskRequest) (*Task, } // Fetch the created task to return full details - return c.GetTask(ctx, taskID) + return c.GetTask(ctx, int(taskID)) } // UpdateTask updates an existing task. diff --git a/types.go b/types.go index 3e0160d..c729dae 100644 --- a/types.go +++ b/types.go @@ -2,6 +2,7 @@ package kanboard import ( "encoding/json" + "fmt" "strconv" ) @@ -105,6 +106,33 @@ func (i *StringInt64) UnmarshalJSON(data []byte) error { return nil } +// IntOrFalse is an int that can be unmarshaled from a JSON int or false. +// Kanboard API returns false on failure, int (ID) on success for create operations. +type IntOrFalse int + +// UnmarshalJSON implements json.Unmarshaler. +func (i *IntOrFalse) UnmarshalJSON(data []byte) error { + // Try as int first (success case) + var n int + if err := json.Unmarshal(data, &n); err == nil { + *i = IntOrFalse(n) + return nil + } + + // Try as bool (failure case: false) + var b bool + if err := json.Unmarshal(data, &b); err == nil { + if b { + *i = 1 // true shouldn't happen, but handle it + } else { + *i = 0 + } + return nil + } + + return fmt.Errorf("cannot unmarshal %s into IntOrFalse", data) +} + // Project represents a Kanboard project (board). type Project struct { ID StringInt `json:"id"` diff --git a/types_test.go b/types_test.go index 6b3e9e3..1bc1d73 100644 --- a/types_test.go +++ b/types_test.go @@ -97,6 +97,31 @@ func TestStringInt64_UnmarshalJSON(t *testing.T) { } } +func TestIntOrFalse_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + input string + expected int + }{ + {"int value", `42`, 42}, + {"int zero", `0`, 0}, + {"false", `false`, 0}, + {"true", `true`, 1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var i IntOrFalse + if err := json.Unmarshal([]byte(tt.input), &i); err != nil { + t.Fatalf("unmarshal error: %v", err) + } + if int(i) != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, i) + } + }) + } +} + func TestProject_UnmarshalJSON(t *testing.T) { jsonData := `{ "id": "1",