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.
This commit is contained in:
Oliver Jakoubek 2026-01-15 20:23:53 +01:00
commit a34a40cb12
7 changed files with 63 additions and 10 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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"`

View file

@ -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",