github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/names.go (about) 1 // Copyright (c) 2017 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package dosa 22 23 import ( 24 "strings" 25 26 "github.com/pkg/errors" 27 ) 28 29 const ( 30 fqnSeparator = "." 31 rootFQN = FQN("") 32 maxNameLen = 32 33 ) 34 35 // FQN is the fully qualified name for an entity 36 type FQN string 37 38 // String satisfies fmt.Stringer interface 39 func (f FQN) String() string { 40 return string(f) 41 } 42 43 // Child returns a new child FQN with the given comp at the end 44 func (f FQN) Child(s string) (FQN, error) { 45 comp, err := NormalizeName(s) 46 if err != nil { 47 return "", errors.Wrap(err, "cannot create child FQN") 48 } 49 return FQN(strings.Join([]string{string(f), comp}, fqnSeparator)), nil 50 } 51 52 // ToFQN converts the input string to FQN 53 func ToFQN(s string) (FQN, error) { 54 if s == "" { 55 return rootFQN, nil 56 } 57 comps := strings.Split(s, fqnSeparator) 58 normalizedComps := make([]string, len(comps)) 59 for i, c := range comps { 60 comp, err := NormalizeName(c) 61 if err != nil { 62 return "", errors.Wrap(err, "cannot create FQN with invalid name component") 63 } 64 normalizedComps[i] = comp 65 } 66 return FQN(strings.Join(normalizedComps, fqnSeparator)), nil 67 } 68 69 func isInvalidFirstRune(r rune) bool { 70 return !((r >= 'a' && r <= 'z') || r == '_') 71 } 72 73 func isInvalidOtherRune(r rune) bool { 74 return !(r >= '0' && r <= '9') && isInvalidFirstRune(r) 75 } 76 77 // IsValidName checks if a name conforms the following rules: 78 // 1. name starts with [a-z_] 79 // 2. the rest of name can contain only [a-z0-9_] 80 // 3. the length of name must be greater than 0 and less than or equal to maxNameLen 81 func IsValidName(name string) error { 82 if len(name) == 0 { 83 return errors.Errorf("cannot be empty") 84 } 85 if len(name) > maxNameLen { 86 return errors.Errorf("too long: %v has length %d, max allowed is %d", name, maxNameLen, len(name)) 87 } 88 if strings.IndexFunc(name[:1], isInvalidFirstRune) != -1 { 89 return errors.Errorf("name must start with [a-z_]. Actual='%s'", name) 90 } 91 if strings.IndexFunc(name[1:], isInvalidOtherRune) != -1 { 92 return errors.Errorf("name must contain only [a-z0-9_], Actual='%s'", name) 93 } 94 return nil 95 } 96 97 // NormalizeName normalizes names to a canonical representation by lowercase everything. 98 // It returns error if the resultant canonical name is invalid. 99 func NormalizeName(name string) (string, error) { 100 lowercaseName := strings.ToLower(strings.TrimSpace(name)) 101 if err := IsValidName(lowercaseName); err != nil { 102 return "", errors.Wrapf(err, "failed to normalize to a valid name for %s", name) 103 } 104 return lowercaseName, nil 105 }