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 }