github.com/hooklift/terraform@v0.11.0-beta1.0.20171117000744-6786c1361ffe/registry/regsrc/module.go (about)

     1  package regsrc
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"regexp"
     7  	"strings"
     8  )
     9  
    10  var (
    11  	ErrInvalidModuleSource = errors.New("not a valid registry module source")
    12  
    13  	// nameSubRe is the sub-expression that matches a valid module namespace or
    14  	// name. It's strictly a super-set of what GitHub allows for user/org and
    15  	// repo names respectively, but more restrictive than our original repo-name
    16  	// regex which allowed periods but could cause ambiguity with hostname
    17  	// prefixes. It does not anchor the start or end so it can be composed into
    18  	// more complex RegExps below. Alphanumeric with - and _ allowed in non
    19  	// leading or trailing positions. Max length 64 chars. (GitHub username is
    20  	// 38 max.)
    21  	nameSubRe = "[0-9A-Za-z](?:[0-9A-Za-z-_]{0,62}[0-9A-Za-z])?"
    22  
    23  	// providerSubRe is the sub-expression that matches a valid provider. It
    24  	// does not anchor the start or end so it can be composed into more complex
    25  	// RegExps below. Only lowercase chars and digits are supported in practice.
    26  	// Max length 64 chars.
    27  	providerSubRe = "[0-9a-z]{1,64}"
    28  
    29  	// moduleSourceRe is a regular expression that matches the basic
    30  	// namespace/name/provider[//...] format for registry sources. It assumes
    31  	// any FriendlyHost prefix has already been removed if present.
    32  	moduleSourceRe = regexp.MustCompile(
    33  		fmt.Sprintf("^(%s)\\/(%s)\\/(%s)(?:\\/\\/(.*))?$",
    34  			nameSubRe, nameSubRe, providerSubRe))
    35  
    36  	// disallowed is a set of hostnames that have special usage in modules and
    37  	// can't be registry hosts
    38  	disallowed = map[string]bool{
    39  		"github.com":    true,
    40  		"bitbucket.org": true,
    41  	}
    42  )
    43  
    44  // Module describes a Terraform Registry Module source.
    45  type Module struct {
    46  	// RawHost is the friendly host prefix if one was present. It might be nil
    47  	// if the original source had no host prefix which implies
    48  	// PublicRegistryHost but is distinct from having an actual pointer to
    49  	// PublicRegistryHost since it encodes the fact the original string didn't
    50  	// include a host prefix at all which is significant for recovering actual
    51  	// input not just normalized form. Most callers should access it with Host()
    52  	// which will return public registry host instance if it's nil.
    53  	RawHost      *FriendlyHost
    54  	RawNamespace string
    55  	RawName      string
    56  	RawProvider  string
    57  	RawSubmodule string
    58  }
    59  
    60  // NewModule construct a new module source from separate parts. Pass empty
    61  // string if host or submodule are not needed.
    62  func NewModule(host, namespace, name, provider, submodule string) *Module {
    63  	m := &Module{
    64  		RawNamespace: namespace,
    65  		RawName:      name,
    66  		RawProvider:  provider,
    67  		RawSubmodule: submodule,
    68  	}
    69  	if host != "" {
    70  		m.RawHost = NewFriendlyHost(host)
    71  	}
    72  	return m
    73  }
    74  
    75  // ParseModuleSource attempts to parse source as a Terraform registry module
    76  // source. If the string is not found to be in a valid format,
    77  // ErrInvalidModuleSource is returned. Note that this can only be used on
    78  // "input" strings, e.g. either ones supplied by the user or potentially
    79  // normalised but in Display form (unicode). It will fail to parse a source with
    80  // a punycoded domain since this is not permitted input from a user. If you have
    81  // an already normalized string internally, you can compare it without parsing
    82  // by comparing with the normalized version of the subject with the normal
    83  // string equality operator.
    84  func ParseModuleSource(source string) (*Module, error) {
    85  	// See if there is a friendly host prefix.
    86  	host, rest := ParseFriendlyHost(source)
    87  	if host != nil {
    88  		if !host.Valid() || disallowed[host.Display()] {
    89  			return nil, ErrInvalidModuleSource
    90  		}
    91  	}
    92  
    93  	matches := moduleSourceRe.FindStringSubmatch(rest)
    94  	if len(matches) < 4 {
    95  		return nil, ErrInvalidModuleSource
    96  	}
    97  
    98  	m := &Module{
    99  		RawHost:      host,
   100  		RawNamespace: matches[1],
   101  		RawName:      matches[2],
   102  		RawProvider:  matches[3],
   103  	}
   104  
   105  	if len(matches) == 5 {
   106  		m.RawSubmodule = matches[4]
   107  	}
   108  
   109  	return m, nil
   110  }
   111  
   112  // Display returns the source formatted for display to the user in CLI or web
   113  // output.
   114  func (m *Module) Display() string {
   115  	return m.formatWithPrefix(m.normalizedHostPrefix(m.Host().Display()), false)
   116  }
   117  
   118  // Normalized returns the source formatted for internal reference or comparison.
   119  func (m *Module) Normalized() string {
   120  	return m.formatWithPrefix(m.normalizedHostPrefix(m.Host().Normalized()), false)
   121  }
   122  
   123  // String returns the source formatted as the user originally typed it assuming
   124  // it was parsed from user input.
   125  func (m *Module) String() string {
   126  	// Don't normalize public registry hostname - leave it exactly like the user
   127  	// input it.
   128  	hostPrefix := ""
   129  	if m.RawHost != nil {
   130  		hostPrefix = m.RawHost.String() + "/"
   131  	}
   132  	return m.formatWithPrefix(hostPrefix, true)
   133  }
   134  
   135  // Module returns just the registry ID of the module, without a hostname or
   136  // suffix.
   137  func (m *Module) Module() string {
   138  	return fmt.Sprintf("%s/%s/%s", m.RawNamespace, m.RawName, m.RawProvider)
   139  }
   140  
   141  // Equal compares the module source against another instance taking
   142  // normalization into account.
   143  func (m *Module) Equal(other *Module) bool {
   144  	return m.Normalized() == other.Normalized()
   145  }
   146  
   147  // Host returns the FriendlyHost object describing which registry this module is
   148  // in. If the original source string had not host component this will return the
   149  // PublicRegistryHost.
   150  func (m *Module) Host() *FriendlyHost {
   151  	if m.RawHost == nil {
   152  		return PublicRegistryHost
   153  	}
   154  	return m.RawHost
   155  }
   156  
   157  func (m *Module) normalizedHostPrefix(host string) string {
   158  	if m.Host().Equal(PublicRegistryHost) {
   159  		return ""
   160  	}
   161  	return host + "/"
   162  }
   163  
   164  func (m *Module) formatWithPrefix(hostPrefix string, preserveCase bool) string {
   165  	suffix := ""
   166  	if m.RawSubmodule != "" {
   167  		suffix = "//" + m.RawSubmodule
   168  	}
   169  	str := fmt.Sprintf("%s%s/%s/%s%s", hostPrefix, m.RawNamespace, m.RawName,
   170  		m.RawProvider, suffix)
   171  
   172  	// lower case by default
   173  	if !preserveCase {
   174  		return strings.ToLower(str)
   175  	}
   176  	return str
   177  }