nomadcode/services/core/internal/workitem/provider.go
toki 401488f299 add workitem provider and update plane client integration
- Add workitem package with provider interface for workspace item management
- Update plane client to support workspace item operations
- Add tests for workitem provider
- Update roadmap current.md and plane-task-pipeline-design milestone
2026-05-23 18:47:30 +09:00

141 lines
3.7 KiB
Go

package workitem
import (
"context"
"encoding/json"
"errors"
"strings"
)
var ErrInvalidRef = errors.New("invalid workitem ref")
type ProviderID string
type Capability string
const (
CapabilityLookup Capability = "lookup"
CapabilityComment Capability = "comment"
CapabilityStatusProjection Capability = "status_projection"
CapabilityLabelProjection Capability = "label_projection"
)
type Capabilities map[Capability]bool
func (c Capabilities) Supports(capability Capability) bool {
return c[capability]
}
type Provider interface {
ProviderID() ProviderID
Capabilities() Capabilities
}
type Reader interface {
FetchWorkItem(ctx context.Context, ref Ref) (WorkItem, error)
}
type Commenter interface {
AppendComment(ctx context.Context, input CommentInput) error
}
type StatusProjector interface {
ProjectStatus(ctx context.Context, input StatusProjection) error
}
type LabelProjector interface {
ProjectLabels(ctx context.Context, input LabelProjection) error
}
type Ref struct {
Provider ProviderID `json:"provider"`
Tenant string `json:"tenant"`
Project string `json:"project"`
ID string `json:"id"`
URL string `json:"url,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
type WorkItem struct {
Ref Ref `json:"ref"`
Title string `json:"title"`
DescriptionText string `json:"description_text,omitempty"`
DescriptionHTML string `json:"description_html,omitempty"`
State string `json:"state,omitempty"`
AssigneeIDs []string `json:"assignee_ids,omitempty"`
Labels []string `json:"labels,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
type CommentFormat string
const (
CommentFormatText CommentFormat = "text"
CommentFormatMarkdown CommentFormat = "markdown"
CommentFormatHTML CommentFormat = "html"
)
type CommentInput struct {
Ref Ref `json:"ref"`
Body string `json:"body"`
Format CommentFormat `json:"format"`
ExternalSource string `json:"external_source,omitempty"`
ExternalID string `json:"external_id,omitempty"`
Visibility string `json:"visibility,omitempty"`
}
type StatusProjection struct {
Ref Ref `json:"ref"`
CanonicalState string `json:"canonical_state"`
ProviderState string `json:"provider_state"`
}
type LabelTarget struct {
Canonical string `json:"canonical"`
Provider string `json:"provider"`
}
type LabelProjection struct {
Ref Ref `json:"ref"`
Add []LabelTarget `json:"add,omitempty"`
Remove []LabelTarget `json:"remove,omitempty"`
}
type Mapping struct {
States map[string]string `json:"states,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
}
func (m Mapping) StateTarget(canonical string) (string, bool) {
target := strings.TrimSpace(m.States[strings.TrimSpace(canonical)])
return target, target != ""
}
func (m Mapping) LabelTarget(canonical string) (string, bool) {
target := strings.TrimSpace(m.Labels[strings.TrimSpace(canonical)])
return target, target != ""
}
func NormalizeRef(ref Ref) (Ref, error) {
provider := ProviderID(strings.TrimSpace(string(ref.Provider)))
id := strings.TrimSpace(ref.ID)
if provider == "" || id == "" {
return Ref{}, ErrInvalidRef
}
meta := ref.Metadata
if len(meta) == 0 {
meta = json.RawMessage("{}")
} else if !json.Valid(meta) {
return Ref{}, ErrInvalidRef
}
return Ref{
Provider: provider,
Tenant: strings.TrimSpace(ref.Tenant),
Project: strings.TrimSpace(ref.Project),
ID: id,
URL: strings.TrimSpace(ref.URL),
Metadata: meta,
}, nil
}