254 lines
6.4 KiB
Go
254 lines
6.4 KiB
Go
package workitem
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/nomadcode/nomadcode-core/internal/workflow"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidRef = errors.New("invalid workitem ref")
|
|
ErrNotFound = errors.New("workitem not found")
|
|
)
|
|
|
|
type ProviderID string
|
|
|
|
type Capability string
|
|
|
|
const (
|
|
CapabilityLookup Capability = "lookup"
|
|
CapabilityComment Capability = "comment"
|
|
CapabilityStatusProjection Capability = "status_projection"
|
|
CapabilityLabelProjection Capability = "label_projection"
|
|
CapabilityCreate Capability = "create"
|
|
CapabilityBodyProjection Capability = "body_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 Creator interface {
|
|
CreateWorkItem(ctx context.Context, input CreateInput) (CreateResult, error)
|
|
}
|
|
|
|
// BodyProjector updates an existing work item's title and/or body. It is an
|
|
// additive projection capability separate from status projection so a body
|
|
// rewrite and a status move stay independent steps.
|
|
type BodyProjector interface {
|
|
ProjectBody(ctx context.Context, input BodyProjection) 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"`
|
|
}
|
|
|
|
func (w WorkItem) HasAssignee(id string) bool {
|
|
target := strings.TrimSpace(id)
|
|
if target == "" {
|
|
return false
|
|
}
|
|
for _, a := range w.AssigneeIDs {
|
|
if strings.TrimSpace(a) == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
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 BodyProjection struct {
|
|
Ref Ref `json:"ref"`
|
|
Title string `json:"title,omitempty"`
|
|
DescriptionHTML string `json:"description_html,omitempty"`
|
|
}
|
|
|
|
type CreateInput struct {
|
|
Provider ProviderID `json:"provider"`
|
|
Tenant string `json:"tenant"`
|
|
Project string `json:"project"`
|
|
Title string `json:"title"`
|
|
DescriptionText string `json:"description_text,omitempty"`
|
|
DescriptionHTML string `json:"description_html,omitempty"`
|
|
}
|
|
|
|
type CreateResult struct {
|
|
Ref Ref `json:"ref"`
|
|
}
|
|
|
|
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 != ""
|
|
}
|
|
|
|
type TaskCreateInput struct {
|
|
Ref Ref
|
|
StateID string
|
|
Comment string
|
|
}
|
|
|
|
func firstNonEmpty(ss ...string) string {
|
|
for _, s := range ss {
|
|
if trimmed := strings.TrimSpace(s); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func BuildCreateTaskInput(input TaskCreateInput, item WorkItem) (workflow.CreateTaskInput, error) {
|
|
ref, err := NormalizeRef(input.Ref)
|
|
if err != nil {
|
|
return workflow.CreateTaskInput{}, err
|
|
}
|
|
|
|
title := strings.TrimSpace(item.Title)
|
|
if title == "" {
|
|
title = ref.ID
|
|
}
|
|
|
|
message := firstNonEmpty(input.Comment, item.DescriptionText, item.DescriptionHTML, title)
|
|
|
|
metadata := map[string]string{
|
|
"provider": string(ref.Provider),
|
|
"tenant": ref.Tenant,
|
|
"project": ref.Project,
|
|
"id": ref.ID,
|
|
"state_id": strings.TrimSpace(input.StateID),
|
|
"external_url": ref.URL,
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(map[string]any{
|
|
"message": message,
|
|
"work_item": metadata,
|
|
})
|
|
if err != nil {
|
|
return workflow.CreateTaskInput{}, err
|
|
}
|
|
|
|
metadataBytes, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
return workflow.CreateTaskInput{}, err
|
|
}
|
|
|
|
return workflow.CreateTaskInput{
|
|
Title: title,
|
|
Source: string(ref.Provider),
|
|
Payload: payloadBytes,
|
|
External: &workflow.ExternalRefInput{
|
|
Provider: string(ref.Provider),
|
|
ID: ref.ID,
|
|
URL: ref.URL,
|
|
Metadata: metadataBytes,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
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
|
|
}
|