opcotech/elemo

View on GitHub
internal/model/issue.go

Summary

Maintainability
B
5 hrs
Test Coverage
package model

import (
    "errors"
    "time"

    "github.com/opcotech/elemo/internal/pkg/validate"
)

const (
    IssueKindEpic  IssueKind = iota + 1 // an epic
    IssueKindStory                      // a story
    IssueKindTask                       // a task
    IssueKindBug                        // a bug
)

const (
    IssueStatusOpen       IssueStatus = iota + 1 // open
    IssueStatusInProgress                        // in progress
    IssueStatusBlocked                           // blocked
    IssueStatusReview                            // review
    IssueStatusDone                              // done
    IssueStatusClosed                            // closed
)

const (
    IssueResolutionNone            IssueResolution = iota + 1 // none
    IssueResolutionFixed                                      // fixed
    IssueResolutionDuplicate                                  // duplicate
    IssueResolutionWontFix                                    // won't fix
    IssueResolutionInvalid                                    // invalid
    IssueResolutionIncomplete                                 // incomplete
    IssueResolutionCannotReproduce                            // cannot reproduce
)

const (
    IssuePriorityLow      IssuePriority = iota + 1 // low
    IssuePriorityMedium                            // medium
    IssuePriorityHigh                              // high
    IssuePriorityCritical                          // critical
)

const (
    IssueRelationKindBlockedBy    IssueRelationKind = iota + 1 // blocked by
    IssueRelationKindBlocks                                    // blocks
    IssueRelationKindDependsOn                                 // depends on
    IssueRelationKindDuplicatedBy                              // duplicated by
    IssueRelationKindDuplicates                                // duplicates
    IssueRelationKindRelatedTo                                 // related to
    IssueRelationKindSubtaskOf                                 // subtask of
)

var (
    issueKindKeys = map[IssueKind]string{
        IssueKindEpic:  "epic",
        IssueKindStory: "story",
        IssueKindTask:  "task",
        IssueKindBug:   "bug",
    }
    issueKindValues = map[string]IssueKind{
        "epic":  IssueKindEpic,
        "story": IssueKindStory,
        "task":  IssueKindTask,
        "bug":   IssueKindBug,
    }

    issueStatusKeys = map[IssueStatus]string{
        IssueStatusOpen:       "open",
        IssueStatusInProgress: "in progress",
        IssueStatusBlocked:    "blocked",
        IssueStatusReview:     "review",
        IssueStatusDone:       "done",
        IssueStatusClosed:     "closed",
    }
    issueStatusValues = map[string]IssueStatus{
        "open":        IssueStatusOpen,
        "in progress": IssueStatusInProgress,
        "blocked":     IssueStatusBlocked,
        "review":      IssueStatusReview,
        "done":        IssueStatusDone,
        "closed":      IssueStatusClosed,
    }

    issueResolutionKeys = map[IssueResolution]string{
        IssueResolutionNone:            "none",
        IssueResolutionFixed:           "fixed",
        IssueResolutionDuplicate:       "duplicate",
        IssueResolutionWontFix:         "won't fix",
        IssueResolutionInvalid:         "invalid",
        IssueResolutionIncomplete:      "incomplete",
        IssueResolutionCannotReproduce: "cannot reproduce",
    }
    issueResolutionValues = map[string]IssueResolution{
        "none":             IssueResolutionNone,
        "fixed":            IssueResolutionFixed,
        "duplicate":        IssueResolutionDuplicate,
        "won't fix":        IssueResolutionWontFix,
        "invalid":          IssueResolutionInvalid,
        "incomplete":       IssueResolutionIncomplete,
        "cannot reproduce": IssueResolutionCannotReproduce,
    }

    issuePriorityKeys = map[IssuePriority]string{
        IssuePriorityLow:      "low",
        IssuePriorityMedium:   "medium",
        IssuePriorityHigh:     "high",
        IssuePriorityCritical: "critical",
    }
    issuePriorityValues = map[string]IssuePriority{
        "low":      IssuePriorityLow,
        "medium":   IssuePriorityMedium,
        "high":     IssuePriorityHigh,
        "critical": IssuePriorityCritical,
    }

    issueRelationKindKeys = map[IssueRelationKind]string{
        IssueRelationKindBlockedBy:    "blocked by",
        IssueRelationKindBlocks:       "blocks",
        IssueRelationKindDependsOn:    "depends on",
        IssueRelationKindDuplicatedBy: "duplicated by",
        IssueRelationKindDuplicates:   "duplicates",
        IssueRelationKindRelatedTo:    "related to",
        IssueRelationKindSubtaskOf:    "subtask of",
    }
    issueRelationKindValues = map[string]IssueRelationKind{
        "blocked by":    IssueRelationKindBlockedBy,
        "blocks":        IssueRelationKindBlocks,
        "depends on":    IssueRelationKindDependsOn,
        "duplicated by": IssueRelationKindDuplicatedBy,
        "duplicates":    IssueRelationKindDuplicates,
        "related to":    IssueRelationKindRelatedTo,
        "subtask of":    IssueRelationKindSubtaskOf,
    }
)

// IssueKind represents a kind of issue.
type IssueKind uint8

// String returns the string representation of IssueKind.
func (k IssueKind) String() string {
    return issueKindKeys[k]
}

// MarshalText implements the encoding.TextMarshaler interface.
func (k IssueKind) MarshalText() (text []byte, err error) {
    if k < 1 || k > 4 {
        return nil, ErrInvalidIssueKind
    }
    return []byte(k.String()), nil
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (k *IssueKind) UnmarshalText(text []byte) error {
    if v, ok := issueKindValues[string(text)]; ok {
        *k = v
        return nil
    }
    return ErrInvalidIssueKind
}

// IssueStatus represents the status of an issue.
type IssueStatus uint8

// String returns the string representation of IssueStatus.
func (s IssueStatus) String() string {
    return issueStatusKeys[s]
}

// MarshalText implements the encoding.TextMarshaler interface.
func (s IssueStatus) MarshalText() (text []byte, err error) {
    if s < 1 || s > 6 {
        return nil, ErrInvalidIssueStatus
    }
    return []byte(s.String()), nil
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (s *IssueStatus) UnmarshalText(text []byte) error {
    if v, ok := issueStatusValues[string(text)]; ok {
        *s = v
        return nil
    }
    return ErrInvalidIssueStatus
}

// IssueResolution represents the resolution of an issue.
type IssueResolution uint8

// String returns the string representation of IssueResolution.
func (r IssueResolution) String() string {
    return issueResolutionKeys[r]
}

// MarshalText implements the encoding.TextMarshaler interface.
func (r IssueResolution) MarshalText() (text []byte, err error) {
    if r < 1 || r > 7 {
        return nil, ErrInvalidIssueResolution
    }
    return []byte(r.String()), nil
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (r *IssueResolution) UnmarshalText(text []byte) error {
    if v, ok := issueResolutionValues[string(text)]; ok {
        *r = v
        return nil
    }
    return ErrInvalidIssueResolution
}

// IssuePriority represents the priority of an issue.
type IssuePriority uint8

// String returns the string representation of IssuePriority.
func (p IssuePriority) String() string {
    return issuePriorityKeys[p]
}

// MarshalText implements the encoding.TextMarshaler interface.
func (p IssuePriority) MarshalText() (text []byte, err error) {
    if p < 1 || p > 5 {
        return nil, ErrInvalidIssuePriority
    }
    return []byte(p.String()), nil
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (p *IssuePriority) UnmarshalText(text []byte) error {
    if v, ok := issuePriorityValues[string(text)]; ok {
        *p = v
        return nil
    }
    return ErrInvalidIssuePriority
}

// IssueRelationKind represents the kind of relation between two issues.
type IssueRelationKind uint8

// String returns the string representation of IssueKind.
func (r IssueRelationKind) String() string {
    return issueRelationKindKeys[r]
}

// MarshalText implements the encoding.TextMarshaler interface.
func (r IssueRelationKind) MarshalText() (text []byte, err error) {
    if r < 1 || r > 7 {
        return nil, ErrInvalidIssueRelationKind
    }
    return []byte(r.String()), nil
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (r *IssueRelationKind) UnmarshalText(text []byte) error {
    if v, ok := issueRelationKindValues[string(text)]; ok {
        *r = v
        return nil
    }
    return ErrInvalidIssueRelationKind
}

// IssueRelation represents a relation between two issues.
type IssueRelation struct {
    ID        ID                `json:"id" validate:"required"`
    Source    ID                `json:"source" validate:"required"`
    Target    ID                `json:"target" validate:"required"`
    Kind      IssueRelationKind `json:"kind" validate:"required,min=1,max=7"`
    CreatedAt *time.Time        `json:"created_at" validate:"omitempty"`
    UpdatedAt *time.Time        `json:"updated_at" validate:"omitempty"`
}

// Validate validates the issue relation details.
func (i *IssueRelation) Validate() error {
    if err := validate.Struct(i); err != nil {
        return errors.Join(ErrInvalidIssueRelationDetails, err)
    }
    if err := i.ID.Validate(); err != nil {
        return errors.Join(ErrInvalidIssueRelationDetails, err)
    }
    if err := i.Source.Validate(); err != nil {
        return errors.Join(ErrInvalidIssueRelationDetails, err)
    }
    if err := i.Target.Validate(); err != nil {
        return errors.Join(ErrInvalidIssueRelationDetails, err)
    }
    return nil
}

// Issue represents an issue in the system that can be assigned to a
// user and belong to a project or another Issue.
type Issue struct {
    ID          ID              `json:"id" validate:"required"`
    NumericID   uint            `json:"numeric_id" validate:"required"`
    Parent      *ID             `json:"parent" validate:"omitempty"`
    Kind        IssueKind       `json:"kind" validate:"required,min=1,max=4"`
    Title       string          `json:"title" validate:"required,min=3,max=120"`
    Description string          `json:"description" validate:"omitempty,min=10"`
    Status      IssueStatus     `json:"status" validate:"required,min=1,max=6"`
    Priority    IssuePriority   `json:"priority" validate:"required,min=1,max=5"`
    Resolution  IssueResolution `json:"resolution" validate:"required,min=1,max=7"`
    ReportedBy  ID              `json:"reported_by" validate:"required"`
    Assignees   []ID            `json:"assignees" validate:"omitempty,dive"`
    Labels      []ID            `json:"labels" validate:"omitempty,dive"`
    Comments    []ID            `json:"comments" validate:"omitempty,dive"`
    Attachments []ID            `json:"attachments" validate:"omitempty,dive"`
    Watchers    []ID            `json:"watchers" validate:"omitempty,dive"`
    Relations   []ID            `json:"relations" validate:"omitempty,dive"`
    Links       []string        `json:"links" validate:"omitempty,dive,url"`
    DueDate     *time.Time      `json:"due_date" validate:"omitempty"`
    CreatedAt   *time.Time      `json:"created_at" validate:"omitempty"`
    UpdatedAt   *time.Time      `json:"updated_at" validate:"omitempty"`
}

// Validate validates the issue details.
func (i *Issue) Validate() error {
    if err := validate.Struct(i); err != nil {
        return errors.Join(ErrInvalidIssueDetails, err)
    }
    if err := i.ID.Validate(); err != nil {
        return errors.Join(ErrInvalidIssueDetails, err)
    }
    if err := i.ReportedBy.Validate(); err != nil {
        return errors.Join(ErrInvalidIssueDetails, err)
    }
    if i.Parent != nil {
        if err := i.Parent.Validate(); err != nil {
            return errors.Join(ErrInvalidIssueDetails, err)
        }
    }
    for _, id := range i.Assignees {
        if err := id.Validate(); err != nil {
            return errors.Join(ErrInvalidIssueDetails, err)
        }
    }
    for _, id := range i.Labels {
        if err := id.Validate(); err != nil {
            return errors.Join(ErrInvalidIssueDetails, err)
        }
    }
    for _, id := range i.Comments {
        if err := id.Validate(); err != nil {
            return errors.Join(ErrInvalidIssueDetails, err)
        }
    }
    for _, id := range i.Attachments {
        if err := id.Validate(); err != nil {
            return errors.Join(ErrInvalidIssueDetails, err)
        }
    }
    for _, id := range i.Watchers {
        if err := id.Validate(); err != nil {
            return errors.Join(ErrInvalidIssueDetails, err)
        }
    }
    for _, id := range i.Relations {
        if err := id.Validate(); err != nil {
            return errors.Join(ErrInvalidIssueDetails, err)
        }
    }
    return nil
}

// NewIssueRelation creates a relation between issues.
func NewIssueRelation(source, target ID, kind IssueRelationKind) (*IssueRelation, error) {
    issueRelation := &IssueRelation{
        ID:     MustNewNilID(ResourceTypeIssueRelation),
        Source: source,
        Target: target,
        Kind:   kind,
    }

    if err := issueRelation.Validate(); err != nil {
        return nil, err
    }
    return issueRelation, nil
}

// NewIssue creates a new issue with the given details.
func NewIssue(numericID uint, title string, kind IssueKind, reportedBy ID) (*Issue, error) {
    issue := &Issue{
        ID:          MustNewNilID(ResourceTypeIssue),
        NumericID:   numericID,
        Kind:        kind,
        Title:       title,
        Status:      IssueStatusOpen,
        Priority:    IssuePriorityMedium,
        Resolution:  IssueResolutionNone,
        ReportedBy:  reportedBy,
        Assignees:   make([]ID, 0),
        Labels:      make([]ID, 0),
        Comments:    make([]ID, 0),
        Attachments: make([]ID, 0),
        Watchers:    make([]ID, 0),
        Relations:   make([]ID, 0),
        Links:       make([]string, 0),
    }

    if err := issue.Validate(); err != nil {
        return nil, err
    }

    return issue, nil
}