David K. Bainbridge | 528b318 | 2017-01-23 08:51:59 -0800 | [diff] [blame] | 1 | // Copyright 2014 Canonical Ltd. |
| 2 | // Licensed under the LGPLv3, see LICENCE file for details. |
| 3 | |
| 4 | package names |
| 5 | |
| 6 | import ( |
| 7 | "fmt" |
| 8 | "regexp" |
| 9 | ) |
| 10 | |
| 11 | // CharmTagKind specifies charm tag kind |
| 12 | const CharmTagKind = "charm" |
| 13 | |
| 14 | // Valid charm url can be either in V1 or V3 format. (V2 is a |
| 15 | // charmstore web URL like https://jujucharms.com/postgresql/105, but |
| 16 | // that's not valid as a tag.) |
| 17 | // |
| 18 | // V1 is of the form: |
| 19 | // schema:~user/series/name-revision |
| 20 | // where |
| 21 | // schema is optional and can be either "local" or "cs". |
| 22 | // When not supplied, "cs" is implied. |
| 23 | // user is optional and is only applicable for "cs" schema |
| 24 | // series is optional and is a valid series name |
| 25 | // name is mandatory and is the name of the charm |
| 26 | // revision is optional and can be -1 if revision is unset |
| 27 | // |
| 28 | // V3 is of the form |
| 29 | // schema:user/name/series/revision |
| 30 | // with the same fields and constraints as the V1 format. |
| 31 | |
| 32 | var ( |
| 33 | // SeriesSnippet is a regular expression representing series |
| 34 | SeriesSnippet = "[a-z]+([a-z0-9]+)?" |
| 35 | |
| 36 | // CharmNameSnippet is a regular expression representing charm name |
| 37 | CharmNameSnippet = "[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*" |
| 38 | |
| 39 | localSchemaSnippet = "local:" |
| 40 | v1CharmStoreSchemaSnippet = "cs:(~" + validUserNameSnippet + "/)?" |
| 41 | revisionSnippet = "(-1|0|[1-9][0-9]*)" |
| 42 | |
| 43 | validV1CharmRegEx = regexp.MustCompile("^(" + |
| 44 | localSchemaSnippet + "|" + |
| 45 | v1CharmStoreSchemaSnippet + ")?(" + |
| 46 | SeriesSnippet + "/)?" + |
| 47 | CharmNameSnippet + "(-" + |
| 48 | revisionSnippet + ")?$") |
| 49 | |
| 50 | v3CharmStoreSchemaSnippet = "(cs:)?(" + validUserNameSnippet + "/)?" |
| 51 | |
| 52 | validV3CharmRegEx = regexp.MustCompile("^(" + |
| 53 | localSchemaSnippet + "|" + |
| 54 | v3CharmStoreSchemaSnippet + ")" + |
| 55 | CharmNameSnippet + "(/" + |
| 56 | SeriesSnippet + ")?(/" + |
| 57 | revisionSnippet + ")?$") |
| 58 | ) |
| 59 | |
| 60 | // CharmTag represents tag for charm |
| 61 | // using charm's URL |
| 62 | type CharmTag struct { |
| 63 | url string |
| 64 | } |
| 65 | |
| 66 | // String satisfies Tag interface. |
| 67 | // Produces string representation of charm tag. |
| 68 | func (t CharmTag) String() string { return t.Kind() + "-" + t.Id() } |
| 69 | |
| 70 | // Kind satisfies Tag interface. |
| 71 | // Returns Charm tag kind. |
| 72 | func (t CharmTag) Kind() string { return CharmTagKind } |
| 73 | |
| 74 | // Id satisfies Tag interface. |
| 75 | // Returns charm URL. |
| 76 | func (t CharmTag) Id() string { return t.url } |
| 77 | |
| 78 | // NewCharmTag returns the tag for the charm with the given url. |
| 79 | // It will panic if the given charm url is not valid. |
| 80 | func NewCharmTag(charmURL string) CharmTag { |
| 81 | if !IsValidCharm(charmURL) { |
| 82 | panic(fmt.Sprintf("%q is not a valid charm name", charmURL)) |
| 83 | } |
| 84 | return CharmTag{url: charmURL} |
| 85 | } |
| 86 | |
| 87 | var emptyTag = CharmTag{} |
| 88 | |
| 89 | // ParseCharmTag parses a charm tag string. |
| 90 | func ParseCharmTag(charmTag string) (CharmTag, error) { |
| 91 | tag, err := ParseTag(charmTag) |
| 92 | if err != nil { |
| 93 | return emptyTag, err |
| 94 | } |
| 95 | ct, ok := tag.(CharmTag) |
| 96 | if !ok { |
| 97 | return emptyTag, invalidTagError(charmTag, CharmTagKind) |
| 98 | } |
| 99 | return ct, nil |
| 100 | } |
| 101 | |
| 102 | // IsValidCharm returns whether name is a valid charm url. |
| 103 | func IsValidCharm(url string) bool { |
| 104 | return validV1CharmRegEx.MatchString(url) || validV3CharmRegEx.MatchString(url) |
| 105 | } |