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).
138 lines
4.1 KiB
Go
138 lines
4.1 KiB
Go
package redmine
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
)
|
|
|
|
// Version is a roadmap milestone ("target version") of a project.
|
|
type Version struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
DueDate string `json:"due_date,omitempty"`
|
|
Status string `json:"status"` // open|locked|closed
|
|
Sharing string `json:"sharing,omitempty"`
|
|
}
|
|
|
|
// VersionParams is the write shape for CreateVersion.
|
|
type VersionParams struct {
|
|
Name string
|
|
DueDate string
|
|
Status string // "" defaults to open on the server
|
|
Sharing string // "" omits; "descendants" mirrors the PMO roadmap scripts
|
|
}
|
|
|
|
// ListVersions lists the versions of a project (by identifier or id).
|
|
func (c *Client) ListVersions(ctx context.Context, project string) ([]Version, error) {
|
|
var body struct {
|
|
Versions []Version `json:"versions"`
|
|
}
|
|
if err := c.do(ctx, "GET", "/projects/"+esc(project)+"/versions.json", nil, nil, &body); err != nil {
|
|
return nil, err
|
|
}
|
|
return body.Versions, nil
|
|
}
|
|
|
|
// CreateVersion creates a roadmap milestone in a project.
|
|
func (c *Client) CreateVersion(ctx context.Context, project string, p VersionParams) (*Version, error) {
|
|
if p.Name == "" {
|
|
return nil, fmt.Errorf("%w: version name required", ErrValidation)
|
|
}
|
|
payload := map[string]any{"version": map[string]any{
|
|
"name": p.Name,
|
|
"due_date": optStr(p.DueDate),
|
|
"status": optStr(p.Status),
|
|
"sharing": optStr(p.Sharing),
|
|
}}
|
|
var body struct {
|
|
Version Version `json:"version"`
|
|
}
|
|
if err := c.do(ctx, "POST", "/projects/"+esc(project)+"/versions.json", nil, payload, &body); err != nil {
|
|
return nil, err
|
|
}
|
|
return &body.Version, nil
|
|
}
|
|
|
|
// Category is an issue category within a project.
|
|
type Category struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// ListCategories lists the issue categories of a project.
|
|
func (c *Client) ListCategories(ctx context.Context, project string) ([]Category, error) {
|
|
var body struct {
|
|
Categories []Category `json:"issue_categories"`
|
|
}
|
|
if err := c.do(ctx, "GET", "/projects/"+esc(project)+"/issue_categories.json", nil, nil, &body); err != nil {
|
|
return nil, err
|
|
}
|
|
return body.Categories, nil
|
|
}
|
|
|
|
// CreateCategory creates an issue category in a project.
|
|
func (c *Client) CreateCategory(ctx context.Context, project, name string) (*Category, error) {
|
|
if name == "" {
|
|
return nil, fmt.Errorf("%w: category name required", ErrValidation)
|
|
}
|
|
payload := map[string]any{"issue_category": map[string]any{"name": name}}
|
|
var body struct {
|
|
Category Category `json:"issue_category"`
|
|
}
|
|
if err := c.do(ctx, "POST", "/projects/"+esc(project)+"/issue_categories.json", nil, payload, &body); err != nil {
|
|
return nil, err
|
|
}
|
|
return &body.Category, nil
|
|
}
|
|
|
|
// Relation ties two issues together.
|
|
type Relation struct {
|
|
ID int `json:"id"`
|
|
IssueID int `json:"issue_id"`
|
|
IssueToID int `json:"issue_to_id"`
|
|
RelationType string `json:"relation_type"`
|
|
Delay int `json:"delay,omitempty"`
|
|
}
|
|
|
|
// ValidRelationType reports whether t is a Redmine relation type.
|
|
func ValidRelationType(t string) bool {
|
|
switch t {
|
|
case "relates", "duplicates", "duplicated", "blocks", "blocked", "precedes", "follows", "copied_to", "copied_from":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CreateRelation creates issue_from -> issue_to of the given type
|
|
// ("blocks", "relates", ...).
|
|
func (c *Client) CreateRelation(ctx context.Context, from, to int, relationType string) (*Relation, error) {
|
|
if !ValidRelationType(relationType) {
|
|
return nil, fmt.Errorf("%w: relation type %q not allowed", ErrValidation, relationType)
|
|
}
|
|
payload := map[string]any{"relation": map[string]any{
|
|
"issue_to_id": to,
|
|
"relation_type": relationType,
|
|
}}
|
|
var body struct {
|
|
Relation Relation `json:"relation"`
|
|
}
|
|
path := "/issues/" + strconv.Itoa(from) + "/relations.json"
|
|
if err := c.do(ctx, "POST", path, nil, payload, &body); err != nil {
|
|
return nil, err
|
|
}
|
|
return &body.Relation, nil
|
|
}
|
|
|
|
// esc path-escapes a project identifier or id string.
|
|
func esc(s string) string { return url.PathEscape(s) }
|
|
|
|
// optStr maps "" to nil for omittable JSON string fields.
|
|
func optStr(s string) any {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return s
|
|
}
|