mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
244 lines
6.7 KiB
Go
244 lines
6.7 KiB
Go
package approvals
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
)
|
|
|
|
type ApprovalStep uint16
|
|
|
|
type ApprovalWorkflowKey string
|
|
|
|
func (k ApprovalWorkflowKey) String() string {
|
|
return string(k)
|
|
}
|
|
|
|
type NextStepCallback func(current ApprovalStep, decision entity.ApprovalAction) (ApprovalStep, bool)
|
|
|
|
var (
|
|
approvalActions = map[entity.ApprovalAction]struct{}{
|
|
entity.ApprovalActionApproved: {},
|
|
entity.ApprovalActionRejected: {},
|
|
entity.ApprovalActionCreated: {},
|
|
entity.ApprovalActionUpdated: {},
|
|
}
|
|
|
|
approvalWorkflows = make(map[ApprovalWorkflowKey]map[ApprovalStep]string)
|
|
approvalWorkflowsMu sync.RWMutex
|
|
)
|
|
|
|
// WorkflowConstants prepares the registered workflows for exposure via constants endpoints.
|
|
func WorkflowConstants() map[string]map[string]string {
|
|
approvalWorkflowsMu.RLock()
|
|
defer approvalWorkflowsMu.RUnlock()
|
|
|
|
if len(approvalWorkflows) == 0 {
|
|
return nil
|
|
}
|
|
|
|
result := make(map[string]map[string]string, len(approvalWorkflows))
|
|
for workflow, steps := range approvalWorkflows {
|
|
if len(steps) == 0 {
|
|
continue
|
|
}
|
|
stepMap := make(map[string]string, len(steps))
|
|
for step, label := range steps {
|
|
stepMap[fmt.Sprintf("%d", step)] = label
|
|
}
|
|
result[workflow.String()] = stepMap
|
|
}
|
|
if len(result) == 0 {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
// RegisterWorkflowSteps stores the available steps for a workflow key (usually matching approvable type).
|
|
func RegisterWorkflowSteps(workflow ApprovalWorkflowKey, steps map[ApprovalStep]string) error {
|
|
workflowStr := strings.TrimSpace(workflow.String())
|
|
if workflowStr == "" {
|
|
return errors.New("workflow key is required")
|
|
}
|
|
if len(steps) == 0 {
|
|
return fmt.Errorf("no steps defined for workflow %q", workflowStr)
|
|
}
|
|
|
|
copied := make(map[ApprovalStep]string, len(steps))
|
|
for step, label := range steps {
|
|
if step == 0 {
|
|
return fmt.Errorf("workflow %q contains step 0 which is not allowed", workflowStr)
|
|
}
|
|
trimmed := strings.TrimSpace(label)
|
|
if trimmed == "" {
|
|
return fmt.Errorf("workflow %q contains empty label for step %d", workflowStr, step)
|
|
}
|
|
copied[step] = trimmed
|
|
}
|
|
|
|
approvalWorkflowsMu.Lock()
|
|
defer approvalWorkflowsMu.Unlock()
|
|
approvalWorkflows[ApprovalWorkflowKey(workflowStr)] = copied
|
|
return nil
|
|
}
|
|
|
|
// WorkflowSteps returns the steps registered for the given workflow key.
|
|
func WorkflowSteps(workflow ApprovalWorkflowKey) map[ApprovalStep]string {
|
|
approvalWorkflowsMu.RLock()
|
|
defer approvalWorkflowsMu.RUnlock()
|
|
|
|
workflowStr := strings.TrimSpace(workflow.String())
|
|
if workflowStr == "" {
|
|
return nil
|
|
}
|
|
|
|
steps, ok := approvalWorkflows[ApprovalWorkflowKey(workflowStr)]
|
|
if !ok || len(steps) == 0 {
|
|
return nil
|
|
}
|
|
|
|
copied := make(map[ApprovalStep]string, len(steps))
|
|
for step, label := range steps {
|
|
copied[step] = label
|
|
}
|
|
return copied
|
|
}
|
|
|
|
// ApprovalStepName fetches the label for the target step inside the workflow.
|
|
func ApprovalStepName(workflow ApprovalWorkflowKey, step ApprovalStep) (string, bool) {
|
|
steps := WorkflowSteps(workflow)
|
|
if len(steps) == 0 {
|
|
return "", false
|
|
}
|
|
label, ok := steps[step]
|
|
return label, ok
|
|
}
|
|
|
|
// ValidateApprovalStep ensures the workflow contains the provided step.
|
|
func ValidateApprovalStep(workflow ApprovalWorkflowKey, step ApprovalStep) error {
|
|
if _, ok := ApprovalStepName(workflow, step); ok {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("invalid approval step %d for workflow %s", step, workflow)
|
|
}
|
|
|
|
// IsValidApprovalAction reports whether the action is supported.
|
|
func IsValidApprovalAction(action entity.ApprovalAction) bool {
|
|
_, ok := approvalActions[action]
|
|
return ok
|
|
}
|
|
|
|
// NewApproval creates an approval record for the given approvable target.
|
|
func NewApproval(workflow ApprovalWorkflowKey, approvableId uint, step ApprovalStep, action *entity.ApprovalAction, actorId uint, note *string) (*entity.Approval, error) {
|
|
if approvableId == 0 {
|
|
return nil, errors.New("approvable id is required")
|
|
}
|
|
|
|
workflowStr := strings.TrimSpace(workflow.String())
|
|
if workflowStr == "" {
|
|
return nil, errors.New("approval workflow key is required")
|
|
}
|
|
|
|
key := ApprovalWorkflowKey(workflowStr)
|
|
|
|
if err := ValidateApprovalStep(key, step); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var actionPtr *entity.ApprovalAction
|
|
if action != nil {
|
|
if !IsValidApprovalAction(*action) {
|
|
return nil, fmt.Errorf("invalid approval action %q", *action)
|
|
}
|
|
actionCopy := *action
|
|
actionPtr = &actionCopy
|
|
}
|
|
|
|
if actorId == 0 {
|
|
return nil, errors.New("actor id is required")
|
|
}
|
|
|
|
var notes *string
|
|
if note != nil {
|
|
trimmed := strings.TrimSpace(*note)
|
|
if trimmed != "" {
|
|
notes = &trimmed
|
|
}
|
|
}
|
|
|
|
actor := actorId
|
|
var stepName string
|
|
if label, ok := ApprovalStepName(key, step); ok {
|
|
labelCopy := label
|
|
stepName = labelCopy
|
|
}
|
|
|
|
return &entity.Approval{
|
|
ApprovableType: workflowStr,
|
|
ApprovableId: approvableId,
|
|
StepNumber: uint16(step),
|
|
StepName: stepName,
|
|
Action: actionPtr,
|
|
Notes: notes,
|
|
ActionBy: &actor,
|
|
}, nil
|
|
}
|
|
|
|
// SetApprovalAction updates the approval action, notes, and optionally advances to another step.
|
|
func SetApprovalAction(approval *entity.Approval, action entity.ApprovalAction, actorId uint, note *string, nextStep NextStepCallback) error {
|
|
if approval == nil {
|
|
return errors.New("approval is nil")
|
|
}
|
|
if !IsValidApprovalAction(action) {
|
|
return fmt.Errorf("invalid approval action %q", action)
|
|
}
|
|
if actorId == 0 {
|
|
return errors.New("actor id is required for approval decision")
|
|
}
|
|
|
|
act := action
|
|
approval.Action = &act
|
|
approval.ActionBy = &actorId
|
|
|
|
if note != nil {
|
|
trimmed := strings.TrimSpace(*note)
|
|
if trimmed == "" {
|
|
approval.Notes = nil
|
|
} else {
|
|
approval.Notes = &trimmed
|
|
}
|
|
} else {
|
|
approval.Notes = nil
|
|
}
|
|
|
|
if nextStep != nil {
|
|
current := ApprovalStep(approval.StepNumber)
|
|
if proposed, ok := nextStep(current, action); ok {
|
|
if err := ValidateApprovalStep(ApprovalWorkflowKey(approval.ApprovableType), proposed); err != nil {
|
|
return err
|
|
}
|
|
approval.StepNumber = uint16(proposed)
|
|
}
|
|
}
|
|
|
|
if label, ok := ApprovalStepName(ApprovalWorkflowKey(approval.ApprovableType), ApprovalStep(approval.StepNumber)); ok {
|
|
labelCopy := label
|
|
approval.StepName = labelCopy
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Approve marks the approval as approved by the given actor, applying the optional step callback.
|
|
func Approve(approval *entity.Approval, actorId uint, note *string, nextStep NextStepCallback) error {
|
|
return SetApprovalAction(approval, entity.ApprovalActionApproved, actorId, note, nextStep)
|
|
}
|
|
|
|
// Reject marks the approval as rejected by the given actor, applying the optional step callback.
|
|
func Reject(approval *entity.Approval, actorId uint, note *string, nextStep NextStepCallback) error {
|
|
return SetApprovalAction(approval, entity.ApprovalActionRejected, actorId, note, nextStep)
|
|
}
|