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 }