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