github.com/System-Glitch/goyave/v3@v3.6.1-0.20210226143142-ac2fe42ee80e/parametrizeable.go (about) 1 package goyave 2 3 import ( 4 "fmt" 5 "regexp" 6 "strings" 7 ) 8 9 var regexCache map[string]*regexp.Regexp 10 11 // parametrizeable represents a route or router accepting 12 // parameters in its URI. 13 type parametrizeable struct { 14 regex *regexp.Regexp 15 parameters []string 16 } 17 18 // compileParameters parse the route parameters and compiles their regexes if needed. 19 // If "ends" is set to true, the generated regex ends with "$", thus set "ends" to true 20 // if you're compiling route parameters, set to false if you're compiling router parameters. 21 func (p *parametrizeable) compileParameters(uri string, ends bool) { 22 idxs, err := p.braceIndices(uri) 23 if err != nil { 24 panic(err) 25 } 26 27 var builder strings.Builder 28 29 // Final regex will never be larger than src uri + 2 (for ^ and $) 30 // Make initial alloc to avoid the need for realloc 31 builder.Grow(len(uri) + 2) 32 33 builder.WriteString("^") 34 length := len(idxs) 35 if length > 0 { 36 end := 0 37 for i := 0; i < length; i += 2 { 38 raw := uri[end:idxs[i]] 39 end = idxs[i+1] 40 sub := uri[idxs[i]+1 : end] 41 parts := strings.SplitN(sub, ":", 2) 42 if parts[0] == "" { 43 panic(fmt.Errorf("invalid route parameter, missing name in %q", sub)) 44 } 45 pattern := "[^/]+" // default pattern 46 if len(parts) == 2 { 47 pattern = parts[1] 48 if pattern == "" { 49 panic(fmt.Errorf("invalid route parameter, missing pattern in %q", sub)) 50 } 51 } 52 53 builder.WriteString(raw) 54 builder.WriteString("(") 55 builder.WriteString(pattern) 56 builder.WriteString(")") 57 end++ // Skip closing braces 58 p.parameters = append(p.parameters, parts[0]) 59 } 60 builder.WriteString(uri[end:]) 61 } else { 62 builder.WriteString(uri) 63 } 64 65 if ends { 66 builder.WriteString("$") 67 } 68 69 pattern := builder.String() 70 cachedRegex, ok := regexCache[pattern] 71 if !ok { 72 regex := regexp.MustCompile(pattern) 73 regexCache[pattern] = regex 74 p.regex = regex 75 } else { 76 p.regex = cachedRegex 77 } 78 79 if p.regex.NumSubexp() != length/2 { 80 panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", uri) + 81 "Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)") 82 } 83 } 84 85 // braceIndices returns the first level curly brace indices from a string. 86 // Returns an error in case of unbalanced braces. 87 func (p *parametrizeable) braceIndices(s string) ([]int, error) { 88 var level, idx int 89 indices := make([]int, 0, 2) 90 length := len(s) 91 for i := 0; i < length; i++ { 92 if s[i] == '{' { 93 level++ 94 if level == 1 { 95 idx = i 96 } 97 } else if s[i] == '}' { 98 level-- 99 if level == 0 { 100 if i == idx+1 { 101 return nil, fmt.Errorf("empty route parameter in %q", s) 102 } 103 indices = append(indices, idx, i) 104 } else if level < 0 { 105 return nil, fmt.Errorf("unbalanced braces in %q", s) 106 } 107 } 108 } 109 if level != 0 { 110 return nil, fmt.Errorf("unbalanced braces in %q", s) 111 } 112 return indices, nil 113 } 114 115 // makeParameters from a regex match and the given parameter names. 116 // The match parameter is expected to contain only the capturing groups. 117 // 118 // Given ["/product/33/param", "33", "param"] ["id", "name"] 119 // The returned map will be ["id": "33", "name": "param"] 120 func (p *parametrizeable) makeParameters(match []string, names []string) map[string]string { 121 length := len(match) 122 params := make(map[string]string, length-1) 123 for i := 1; i < length; i++ { 124 params[names[i-1]] = match[i] 125 } 126 return params 127 }