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

     1  package trigger
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"math/rand"
     8  	"sync"
     9  	"time"
    10  
    11  	golog "github.com/ipfs/go-log"
    12  	"github.com/qri-io/qri/profile"
    13  )
    14  
    15  var (
    16  	log = golog.Logger("trigger")
    17  
    18  	// ErrUnexpectedType indicates the trigger type is unexpected
    19  	ErrUnexpectedType = fmt.Errorf("unexpected trigger type")
    20  	// ErrTypeMismatch indicates the given TriggerType does not match the expected TriggerType
    21  	ErrTypeMismatch = fmt.Errorf("TriggerType mismatch")
    22  	// ErrEmptyOwnerID indicates the given Source has an empty ScopeID, known in other systems as the OwnerID
    23  	ErrEmptyOwnerID = fmt.Errorf("empty OwnerID")
    24  	// ErrEmptyWorkflowID indicates the given Source has an empty WorkflowID
    25  	ErrEmptyWorkflowID = fmt.Errorf("empty WorkflowID")
    26  	// ErrNotFound indicates that the trigger cannot be found
    27  	ErrNotFound = fmt.Errorf("trigger not found")
    28  )
    29  
    30  const charset = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    31  
    32  var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
    33  
    34  // NewID returns a random string ID of alphanumeric characters
    35  // These IDs only have to be unique within a single workflow
    36  // This can be replaced with a determinate `NewID` function for testing
    37  var NewID = func() string {
    38  	b := make([]byte, 5)
    39  	for i := range b {
    40  		b[i] = charset[seededRand.Intn(len(charset))]
    41  	}
    42  	return string(b)
    43  }
    44  
    45  // A Trigger determines under what circumstances an `event.ETAutomationWorkflowTrigger`
    46  // should be emitted on the given event.Bus. It knows how to `Advance` itself.
    47  type Trigger interface {
    48  	json.Marshaler
    49  	json.Unmarshaler
    50  	// ID returns the Trigger ID
    51  	ID() string
    52  	// Active returns whether the Trigger is enabled
    53  	Active() bool
    54  	// SetActive sets the enabled status
    55  	SetActive(active bool) error
    56  	// Type returns the Type of this Trigger
    57  	Type() string
    58  	// Advance adjusts the Trigger once it has been triggered
    59  	Advance() error
    60  	// ToMap returns the trigger as a map[string]interface
    61  	ToMap() map[string]interface{}
    62  }
    63  
    64  // Constructor is a function that creates a Trigger from a
    65  // map[string]interface{}
    66  type Constructor func(opts map[string]interface{}) (Trigger, error)
    67  
    68  // A Listener emits a `event.ETTriggerWorkflow` event when a specific stimulus
    69  // is triggered
    70  type Listener interface {
    71  	// ConstructTrigger returns a Trigger of the associated Type
    72  	ConstructTrigger(opts map[string]interface{}) (Trigger, error)
    73  	// Listen takes a list of sources and adds or updates the Listener's
    74  	// store to include all the active triggers of the correct type
    75  	Listen(source ...Source) error
    76  	// Type returns the type of Trigger that this Listener listens for
    77  	Type() string
    78  	// Start puts the Listener in an active state of listening for triggers
    79  	Start(ctx context.Context) error
    80  	// Stop stops the Listener from listening for triggers
    81  	Stop() error
    82  }
    83  
    84  // Source is an abstraction for a `workflow.Workflow`
    85  type Source interface {
    86  	WorkflowID() string
    87  	ActiveTriggers(triggerType string) []map[string]interface{}
    88  	Owner() profile.ID
    89  }
    90  
    91  // Set stores triggers of a common type, uniquely identified by ownerID and
    92  // workflowID
    93  type Set struct {
    94  	triggerType      string
    95  	activeLock       sync.Mutex
    96  	active           map[profile.ID]map[string][]Trigger
    97  	constructTrigger func(opt map[string]interface{}) (Trigger, error)
    98  }
    99  
   100  // NewSet creates a Set with types matched to a given listener
   101  func NewSet(triggerType string, ctor Constructor) *Set {
   102  	return &Set{
   103  		activeLock:       sync.Mutex{},
   104  		active:           map[profile.ID]map[string][]Trigger{},
   105  		triggerType:      triggerType,
   106  		constructTrigger: ctor,
   107  	}
   108  }
   109  
   110  // Add popuates the set with Triggers from a Source whos type matches the set's
   111  // trigger type
   112  func (t *Set) Add(sources ...Source) error {
   113  	t.activeLock.Lock()
   114  	defer t.activeLock.Unlock()
   115  	for _, s := range sources {
   116  		workflowID := s.WorkflowID()
   117  		if workflowID == "" {
   118  			return ErrEmptyWorkflowID
   119  		}
   120  		ownerID := s.Owner()
   121  		if ownerID == "" {
   122  			return ErrEmptyOwnerID
   123  		}
   124  		triggerOpts := s.ActiveTriggers(t.triggerType)
   125  		triggers := []Trigger{}
   126  		for _, triggerOpt := range triggerOpts {
   127  			t, err := t.constructTrigger(triggerOpt)
   128  			if err != nil {
   129  				return err
   130  			}
   131  			triggers = append(triggers, t)
   132  		}
   133  		// either we are not adding triggers or we are removing them
   134  		if len(triggers) == 0 {
   135  			wfs, ok := t.active[ownerID]
   136  			if !ok {
   137  				continue
   138  			}
   139  			if len(wfs) == 0 {
   140  				delete(t.active, ownerID)
   141  				continue
   142  			}
   143  			_, ok = wfs[workflowID]
   144  			if !ok {
   145  				continue
   146  			}
   147  			if ok && len(wfs) == 1 {
   148  				delete(t.active, ownerID)
   149  				continue
   150  			}
   151  			delete(t.active[ownerID], workflowID)
   152  			continue
   153  		}
   154  		_, ok := t.active[ownerID]
   155  		if !ok {
   156  			t.active[ownerID] = map[string][]Trigger{
   157  				workflowID: triggers,
   158  			}
   159  			continue
   160  		}
   161  		t.active[ownerID][workflowID] = triggers
   162  	}
   163  	return nil
   164  }
   165  
   166  // Exists returns true if all the triggers from the Source exist in the store
   167  func (t *Set) Exists(source Source) bool {
   168  	t.activeLock.Lock()
   169  	defer t.activeLock.Unlock()
   170  
   171  	ownerID := source.Owner()
   172  	workflowID := source.WorkflowID()
   173  	wids, ok := t.active[ownerID]
   174  	if !ok {
   175  		return false
   176  	}
   177  	triggers, ok := wids[workflowID]
   178  	if !ok {
   179  		return false
   180  	}
   181  	triggerOpts := source.ActiveTriggers(t.triggerType)
   182  	if len(triggerOpts) != len(triggers) {
   183  		return false
   184  	}
   185  	for i, opt := range triggerOpts {
   186  		sourceTrigger, err := t.constructTrigger(opt)
   187  		if err != nil {
   188  			log.Errorw("runtimeListener.TriggersExist", "error", err)
   189  			return false
   190  		}
   191  		if sourceTrigger.ID() != triggers[i].ID() {
   192  			return false
   193  		}
   194  	}
   195  	return true
   196  }
   197  
   198  // Active returns the map of active triggers, organized by OwnerID and WorkflowID
   199  func (t *Set) Active() map[profile.ID]map[string][]Trigger {
   200  	return t.active
   201  }