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, "content": content,
} }
var commentID int var commentID IntOrFalse
if err := c.call(ctx, "createComment", params, &commentID); err != nil { if err := c.call(ctx, "createComment", params, &commentID); err != nil {
return nil, fmt.Errorf("createComment: %w", err) 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 // 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. // 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), "blob": base64.StdEncoding.EncodeToString(content),
} }
var result int var result IntOrFalse
if err := c.call(ctx, "createTaskFile", params, &result); err != nil { if err := c.call(ctx, "createTaskFile", params, &result); err != nil {
return 0, fmt.Errorf("createTaskFile: %w", err) 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 0, fmt.Errorf("createTaskFile: failed to upload file")
} }
return result, nil return int(result), nil
} }
// DownloadTaskFile downloads a file's content by its ID. // 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, "link_id": linkID,
} }
var result int var result IntOrFalse
if err := c.call(ctx, "createTaskLink", params, &result); err != nil { if err := c.call(ctx, "createTaskLink", params, &result); err != nil {
return 0, fmt.Errorf("createTaskLink: %w", err) 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 0, fmt.Errorf("createTaskLink: failed to create link")
} }
return result, nil return int(result), nil
} }
// RemoveTaskLink deletes a task link. // 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 params["color_id"] = colorID
} }
var result int var result IntOrFalse
if err := c.call(ctx, "createTag", params, &result); err != nil { if err := c.call(ctx, "createTag", params, &result); err != nil {
return 0, fmt.Errorf("createTag: %w", err) return 0, fmt.Errorf("createTag: %w", err)
} }
return result, nil return int(result), nil
} }
// UpdateTag updates an existing tag's name and/or color. // 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. // CreateTask creates a new task and returns the created task.
func (c *Client) CreateTask(ctx context.Context, req CreateTaskRequest) (*Task, error) { 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 { if err := c.call(ctx, "createTask", req, &taskID); err != nil {
return nil, fmt.Errorf("createTask: %w", err) 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 // Fetch the created task to return full details
return c.GetTask(ctx, taskID) return c.GetTask(ctx, int(taskID))
} }
// UpdateTask updates an existing task. // UpdateTask updates an existing task.

View file

@ -2,6 +2,7 @@ package kanboard
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strconv" "strconv"
) )
@ -105,6 +106,33 @@ func (i *StringInt64) UnmarshalJSON(data []byte) error {
return nil 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). // Project represents a Kanboard project (board).
type Project struct { type Project struct {
ID StringInt `json:"id"` 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) { func TestProject_UnmarshalJSON(t *testing.T) {
jsonData := `{ jsonData := `{
"id": "1", "id": "1",