github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/automation/workflow/workflow.go (about)

     1  package workflow
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"sort"
     8  	"time"
     9  
    10  	"github.com/google/uuid"
    11  	golog "github.com/ipfs/go-log"
    12  	"github.com/qri-io/qri/profile"
    13  )
    14  
    15  var (
    16  	log = golog.Logger("workflow")
    17  	// ErrNilWorkflow indicates that the given workflow is nil
    18  	ErrNilWorkflow = fmt.Errorf("nil workflow")
    19  	// ErrNoWorkflowID indicates the workflow is invalid because the ID field is empty
    20  	ErrNoWorkflowID = fmt.Errorf("invalid workflow: empty ID")
    21  	// ErrNoInitID indicates the workflow is invalid because the InitID field is empty
    22  	ErrNoInitID = fmt.Errorf("invalid workflow: empty InitID")
    23  	// ErrNoOwnerID indicates the workflow is invalid because the OwnerID field is empty
    24  	ErrNoOwnerID = fmt.Errorf("invalid workflow: empty OwnerID")
    25  	// ErrNilCreated indicates the workflow is invalid because the Created field is empty
    26  	ErrNilCreated = fmt.Errorf("invalid workflow: nil Created")
    27  )
    28  
    29  // ID is a string identifier for a workflow
    30  type ID string
    31  
    32  // NewID creates a new workflow identifier
    33  func NewID() ID {
    34  	return ID(uuid.New().String())
    35  }
    36  
    37  // String returns the underlying id string
    38  func (id ID) String() string { return string(id) }
    39  
    40  // SetIDRand sets the random reader that NewID uses as a source of random bytes
    41  // passing in nil will default to crypto.Rand. This can be used to make ID
    42  // generation deterministic for tests. eg:
    43  //    myString := "SomeRandomStringThatIsLong-SoYouCanCallItAsMuchAsNeeded..."
    44  //    workflow.SetIDRand(strings.NewReader(myString))
    45  //    a := NewID()
    46  //    workflow.SetIDRand(strings.NewReader(myString))
    47  //    b := NewID()
    48  func SetIDRand(r io.Reader) {
    49  	uuid.SetRand(r)
    50  }
    51  
    52  // A Workflow associates automation with a dataset
    53  type Workflow struct {
    54  	ID       ID                       `json:"id"`
    55  	InitID   string                   `json:"initID"`
    56  	OwnerID  profile.ID               `json:"ownerID"`
    57  	Created  *time.Time               `json:"created"`
    58  	Active   bool                     `json:"active"`
    59  	Triggers []map[string]interface{} `json:"triggers"`
    60  	Hooks    []map[string]interface{} `json:"hooks"`
    61  }
    62  
    63  // Validate errors if the workflow is not valid
    64  func (w *Workflow) Validate() error {
    65  	if w == nil {
    66  		return ErrNilWorkflow
    67  	}
    68  	if w.ID == "" {
    69  		return ErrNoWorkflowID
    70  	}
    71  	if w.InitID == "" {
    72  		return ErrNoInitID
    73  	}
    74  	if w.OwnerID == "" {
    75  		return ErrNoOwnerID
    76  	}
    77  	if w.Created == nil {
    78  		return ErrNilCreated
    79  	}
    80  	return nil
    81  }
    82  
    83  // Copy returns a shallow copy of the receiver
    84  func (w *Workflow) Copy() *Workflow {
    85  	if w == nil {
    86  		return nil
    87  	}
    88  	workflow := &Workflow{
    89  		ID:       w.ID,
    90  		InitID:   w.InitID,
    91  		OwnerID:  w.OwnerID,
    92  		Created:  w.Created,
    93  		Active:   w.Active,
    94  		Triggers: w.Triggers,
    95  		Hooks:    w.Hooks,
    96  	}
    97  	return workflow
    98  }
    99  
   100  // Owner returns the owner id
   101  func (w *Workflow) Owner() profile.ID {
   102  	return w.OwnerID
   103  }
   104  
   105  // WorkflowID returns the workflow id as a string
   106  func (w *Workflow) WorkflowID() string {
   107  	return w.ID.String()
   108  }
   109  
   110  // ActiveTriggers returns a list of triggers that are currently enabled
   111  // an undeployed workflow, by definition, has no active triggers
   112  // Any misshaped trigger options will be ignored
   113  func (w *Workflow) ActiveTriggers(triggerType string) []map[string]interface{} {
   114  	activeTriggers := []map[string]interface{}{}
   115  	if !w.Active {
   116  		return activeTriggers
   117  	}
   118  	for i, t := range w.Triggers {
   119  		id, ok := t["id"].(string)
   120  		if !ok {
   121  			log.Debugw("workflow.ActiveTriggers trigger not added to ActiveTriggers list - does not have string 'id' field", "trigger index", i)
   122  			continue
   123  		}
   124  		active, ok := t["active"].(bool)
   125  		if !ok {
   126  			log.Debugw("workflow.ActiveTriggers trigger not added to ActiveTriggers list - does not have boolean 'active' field", "trigger id", id)
   127  			continue
   128  		}
   129  		trigType, ok := t["type"].(string)
   130  		if !ok {
   131  			log.Debugw("workflow.ActiveTriggers trigger not added to ActiveTriggers list - does not have string 'type' field", "trigger id", id)
   132  			continue
   133  		}
   134  		if active && trigType == triggerType {
   135  			activeTriggers = append(activeTriggers, t)
   136  		}
   137  	}
   138  	return activeTriggers
   139  }
   140  
   141  // Set is a collection of Workflows that implements the sort.Interface,
   142  // sorting a list of Set in reverse-chronological-then-alphabetical order
   143  type Set struct {
   144  	set []*Workflow
   145  }
   146  
   147  // NewSet constructs a workflow set.
   148  func NewSet() *Set {
   149  	return &Set{}
   150  }
   151  
   152  // Len part of the `sort.Interface`
   153  func (s Set) Len() int { return len(s.set) }
   154  
   155  // Less part of the `sort.Interface`
   156  func (s Set) Less(i, j int) bool {
   157  	return lessNilTime(s.set[i].Created, s.set[j].Created)
   158  }
   159  
   160  // Swap is part of the `sort.Interface`
   161  func (s Set) Swap(i, j int) { s.set[i], s.set[j] = s.set[j], s.set[i] }
   162  
   163  // Add adds a Workflow to a Set
   164  func (s *Set) Add(j *Workflow) {
   165  	if s == nil {
   166  		*s = Set{set: []*Workflow{j}}
   167  		return
   168  	}
   169  
   170  	for i, workflow := range s.set {
   171  		if workflow.ID == j.ID {
   172  			s.set[i] = j
   173  			return
   174  		}
   175  	}
   176  	s.set = append(s.set, j)
   177  	sort.Sort(s)
   178  }
   179  
   180  // Remove removes a Workflow from a Set
   181  func (s *Set) Remove(id ID) (removed bool) {
   182  	for i, workflow := range s.set {
   183  		if workflow.ID == id {
   184  			if i+1 == len(s.set) {
   185  				s.set = s.set[:i]
   186  				return true
   187  			}
   188  
   189  			s.set = append(s.set[:i], s.set[i+1:]...)
   190  			return true
   191  		}
   192  	}
   193  	return false
   194  }
   195  
   196  // Slice returns a slice of Workflows from position `start` to position `end`
   197  func (s *Set) Slice(start, end int) []*Workflow {
   198  	if start < 0 || end < 0 {
   199  		return []*Workflow{}
   200  	}
   201  	if end > s.Len() {
   202  		end = s.Len()
   203  	}
   204  	return s.set[start:end]
   205  }
   206  
   207  // MarshalJSON satisfies the `json.Marshaller` interface
   208  func (s Set) MarshalJSON() ([]byte, error) {
   209  	return json.Marshal(s.set)
   210  }
   211  
   212  // UnmarshalJSON satisfies the `json.Unmarshaller` interface
   213  func (s *Set) UnmarshalJSON(data []byte) error {
   214  	set := []*Workflow{}
   215  	if err := json.Unmarshal(data, &set); err != nil {
   216  		return err
   217  	}
   218  	s.set = set
   219  	return nil
   220  }
   221  
   222  func lessNilTime(a, b *time.Time) bool {
   223  	if a == nil && b != nil {
   224  		return true
   225  	} else if a != nil && b == nil {
   226  		return false
   227  	} else if a == nil && b == nil {
   228  		return false
   229  	}
   230  	return a.After(*b)
   231  }