github.com/gocaveman/caveman@v0.0.0-20191211162744-0ddf99dbdf6e/uifiles/uiregistry/uiregistry.go (about)

     1  // FIXME: crap, this should really be "uifilesregistry" to follow the convention...
     2  // A registry of CSS, JS files and their dependency chain.
     3  package uiregistry
     4  
     5  import (
     6  	"errors"
     7  	"fmt"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/gocaveman/caveman/webutil"
    12  )
    13  
    14  var ErrNotFound = errors.New("not found")
    15  
    16  // Global is the global registry instance, modules which provide libraries should register them
    17  // here in their init() function.
    18  // FIXME: Instead of a variable, I think we need two function calls - one to get something that implements
    19  // an interface for registering, and another that returns an interface that can read.  In the read call we
    20  // can verify it is called from main - webutil.MainOnly()
    21  var global = NewUIRegistry()
    22  
    23  func Contents() *UIRegistry {
    24  	return global
    25  }
    26  
    27  func ParseName(s string) (typ, name string, err error) {
    28  	parts := strings.Split(s, ":")
    29  	if len(parts) != 2 {
    30  		return "", "", fmt.Errorf("expected exactly two colon separated parts but instead found %d part(s)", len(parts))
    31  	}
    32  	return parts[0], parts[1], nil
    33  }
    34  
    35  // FIXME: what if we use http.FileSystem and a path, instead of using webutil.DataSource?
    36  // It would be one less funky custom type and has effectively the same semantics.
    37  
    38  func MustRegister(name string, deps []string, ds webutil.DataSource) {
    39  	global.MustRegister(name, deps, ds)
    40  }
    41  
    42  func Register(name string, deps []string, ds webutil.DataSource) error {
    43  	return global.Register(name, deps, ds)
    44  }
    45  
    46  // NewUIRegistry makes a new empty initialized UIRegistry.
    47  func NewUIRegistry() *UIRegistry {
    48  	return &UIRegistry{
    49  		reg: make(map[string]*entry),
    50  	}
    51  }
    52  
    53  // UIRegistry is a registry of JS and CSS libraries; the global instance of which is called Global in this package.
    54  type UIRegistry struct {
    55  	rwmu sync.RWMutex
    56  
    57  	reg map[string]*entry
    58  }
    59  
    60  func (r *UIRegistry) MustRegister(name string, deps []string, ds webutil.DataSource) {
    61  	err := r.Register(name, deps, ds)
    62  	if err != nil {
    63  		panic(err)
    64  	}
    65  }
    66  func (r *UIRegistry) Register(name string, deps []string, ds webutil.DataSource) error {
    67  	r.rwmu.Lock()
    68  	defer r.rwmu.Unlock()
    69  
    70  	_, _, err := ParseName(name)
    71  	if err != nil {
    72  		return err
    73  	}
    74  
    75  	r.reg[name] = &entry{
    76  		DataSource: ds,
    77  		Deps:       deps,
    78  	}
    79  
    80  	return nil
    81  }
    82  
    83  // Lookup takes a single name and returns the corresponding DataSource.
    84  func (r *UIRegistry) Lookup(name string) (webutil.DataSource, error) {
    85  
    86  	r.rwmu.RLock()
    87  	defer r.rwmu.RUnlock()
    88  
    89  	if ret, ok := r.reg[name]; ok {
    90  		return ret.DataSource, nil
    91  	}
    92  
    93  	return nil, ErrNotFound
    94  }
    95  
    96  // ResolveDeps takes one or more names and returns a list of the libraries that need to be
    97  // included in order to resolve all dependencies, in the correct sequence.
    98  func (r *UIRegistry) ResolveDeps(names ...string) ([]string, error) {
    99  
   100  	r.rwmu.RLock()
   101  	defer r.rwmu.RUnlock()
   102  
   103  	var out []string
   104  
   105  	// go through each requested library
   106  	for _, name := range names {
   107  
   108  		entry, ok := r.reg[name]
   109  		if !ok {
   110  			return nil, fmt.Errorf("unable to find registry entry for %q", name)
   111  		}
   112  
   113  		// for each dependency add it before this entry, unless it's already there
   114  		for _, dep := range entry.Deps {
   115  			if !stringSliceContains(out, dep) {
   116  				out = append(out, dep)
   117  			}
   118  		}
   119  
   120  		// add this entry
   121  		out = append(out, name)
   122  
   123  	}
   124  
   125  	// if after including dependencies the list was changed, recurse and resolve dependencies again
   126  	if !isSameStrings(out, names) {
   127  		return r.ResolveDeps(out...)
   128  	}
   129  
   130  	// dependencies didn't change the list, we're done
   131  	return out, nil
   132  
   133  }
   134  
   135  // Resolver describes a registry which is capable of resolving requests for libraries.
   136  // Components which need to resolve libraries but not register them (i.e. stuff dealing
   137  // with js and css files during the render path) should use this interface as the
   138  // appropriate abstraction.
   139  type Resolver interface {
   140  	Lookup(name string) (webutil.DataSource, error)
   141  	ResolveDeps(name ...string) ([]string, error)
   142  }
   143  
   144  // Entry describes a specific version of a library, it's dependencies and provides a way to get it's raw data (DataSource)
   145  type entry struct {
   146  	DataSource webutil.DataSource
   147  	Deps       []string
   148  }
   149  
   150  func isSameStrings(s1 []string, s2 []string) bool {
   151  
   152  	if len(s1) != len(s2) {
   153  		return false
   154  	}
   155  
   156  	for i := 0; i < len(s1); i++ {
   157  		if s1[i] != s2[i] {
   158  			return false
   159  		}
   160  	}
   161  
   162  	return true
   163  }
   164  
   165  func stringSliceContains(haystack []string, needle string) bool {
   166  	for _, e := range haystack {
   167  		if e == needle {
   168  			return true
   169  		}
   170  	}
   171  	return false
   172  }