Files
mrcharles 20dbcf604d Add the Redmine REST client with a fake-server test suite
Issues (create/update/partial PUT, notes-as-journals), versions,
categories, relations, and name-to-id resolution over trackers/
statuses/priorities. Sentinel errors carry http codes one-line; response
bodies are never surfaced so the API key cannot leak through error
strings (the fake deliberately echoes the presented key to prove it).
2026-08-29 06:35:20 -05:00

123 lines
3.4 KiB
Go

package redmine
import (
"context"
"fmt"
"strings"
)
// enumeration endpoints: trackers, statuses, priorities. The CLI maps
// human names ("done", "feature", "immediate") onto ids through these;
// the harness gets the same surface so there is no copy-paste temptation.
type trackerList struct {
Trackers []Ref `json:"trackers"`
}
type statusList struct {
Statuses []StatusRef `json:"issue_statuses"`
}
type priorityList struct {
Priorities []Ref `json:"issue_priorities"`
}
// ListTrackers returns the server's issue trackers.
func (c *Client) ListTrackers(ctx context.Context) ([]Ref, error) {
var body trackerList
if err := c.do(ctx, "GET", "/trackers.json", nil, nil, &body); err != nil {
return nil, err
}
return body.Trackers, nil
}
// ListStatuses returns the server's issue statuses (with is_closed).
func (c *Client) ListStatuses(ctx context.Context) ([]StatusRef, error) {
var body statusList
if err := c.do(ctx, "GET", "/issue_statuses.json", nil, nil, &body); err != nil {
return nil, err
}
return body.Statuses, nil
}
// ListPriorities returns the server's issue priorities.
func (c *Client) ListPriorities(ctx context.Context) ([]Ref, error) {
var body priorityList
if err := c.do(ctx, "GET", "/enumerations/issue_priorities.json", nil, nil, &body); err != nil {
return nil, err
}
return body.Priorities, nil
}
// StatusIDByName resolves a status name case-insensitively ("done" ->
// Done's id). Unknown names are ErrNotFound; the name, never any server
// body, appears in the message.
func (c *Client) StatusIDByName(ctx context.Context, name string) (int, error) {
list, err := c.ListStatuses(ctx)
if err != nil {
return 0, err
}
for _, s := range list {
if strings.EqualFold(s.Name, name) {
return s.ID, nil
}
}
return 0, fmt.Errorf("%w: status %q", ErrNotFound, name)
}
// TrackerIDByName resolves a tracker name case-insensitively.
func (c *Client) TrackerIDByName(ctx context.Context, name string) (int, error) {
list, err := c.ListTrackers(ctx)
if err != nil {
return 0, err
}
for _, t := range list {
if strings.EqualFold(t.Name, name) {
return t.ID, nil
}
}
return 0, fmt.Errorf("%w: tracker %q", ErrNotFound, name)
}
// PriorityIDByName resolves a priority name case-insensitively.
func (c *Client) PriorityIDByName(ctx context.Context, name string) (int, error) {
list, err := c.ListPriorities(ctx)
if err != nil {
return 0, err
}
for _, p := range list {
if strings.EqualFold(p.Name, name) {
return p.ID, nil
}
}
return 0, fmt.Errorf("%w: priority %q", ErrNotFound, name)
}
// VersionIDByName resolves a version name within a project,
// case-insensitively.
func (c *Client) VersionIDByName(ctx context.Context, project, name string) (int, error) {
list, err := c.ListVersions(ctx, project)
if err != nil {
return 0, err
}
for _, v := range list {
if strings.EqualFold(v.Name, name) {
return v.ID, nil
}
}
return 0, fmt.Errorf("%w: version %q in %s", ErrNotFound, name, project)
}
// CategoryIDByName resolves a category name within a project,
// case-insensitively.
func (c *Client) CategoryIDByName(ctx context.Context, project, name string) (int, error) {
list, err := c.ListCategories(ctx, project)
if err != nil {
return 0, err
}
for _, cat := range list {
if strings.EqualFold(cat.Name, name) {
return cat.ID, nil
}
}
return 0, fmt.Errorf("%w: category %q in %s", ErrNotFound, name, project)
}