github.com/franciscocpg/up@v0.1.10/internal/redirect/redirect.go (about) 1 // Package redirect provides compiling and matching 2 // redirect and rewrite rules. 3 package redirect 4 5 import ( 6 "fmt" 7 "regexp" 8 "strings" 9 10 "github.com/fanyang01/radix" 11 "github.com/pkg/errors" 12 ) 13 14 // placeholders regexp. 15 var placeholders = regexp.MustCompile(`:(\w+)`) 16 17 // Rule is a single redirect rule. 18 type Rule struct { 19 Path string `json:"path"` 20 Location string `json:"location"` 21 Status int `json:"status"` 22 Force bool `json:"force"` 23 names map[string]bool 24 dynamic bool 25 sub string 26 path *regexp.Regexp 27 } 28 29 // URL returns the final destination after substitutions from path. 30 func (r Rule) URL(path string) string { 31 return r.path.ReplaceAllString(path, r.sub) 32 } 33 34 // IsDynamic returns true if a splat or placeholder is used. 35 func (r *Rule) IsDynamic() bool { 36 return r.dynamic 37 } 38 39 // IsRewrite returns true if the rule represents a rewrite. 40 func (r *Rule) IsRewrite() bool { 41 return r.Status == 200 || r.Status == 0 42 } 43 44 // Compile the rule. 45 func (r *Rule) Compile() { 46 r.path, r.names = compilePath(r.Path) 47 r.sub = compileSub(r.Path, r.Location, r.names) 48 r.dynamic = isDynamic(r.Path) 49 } 50 51 // Rules map of paths to redirects. 52 type Rules map[string]Rule 53 54 // Matcher for header lookup. 55 type Matcher struct { 56 t *radix.PatternTrie 57 } 58 59 // Lookup returns fields for the given path. 60 func (m *Matcher) Lookup(path string) *Rule { 61 v, ok := m.t.Lookup(path) 62 if !ok { 63 return nil 64 } 65 66 r := v.(Rule) 67 return &r 68 } 69 70 // Compile the given rules to a trie. 71 func Compile(rules Rules) (*Matcher, error) { 72 t := radix.NewPatternTrie() 73 m := &Matcher{t} 74 75 for path, rule := range rules { 76 rule.Path = path 77 rule.Compile() 78 t.Add(compilePattern(path), rule) 79 t.Add(compilePattern(path)+"/", rule) 80 } 81 82 return m, nil 83 } 84 85 // compileSub returns a substitution string. 86 func compileSub(path, s string, names map[string]bool) string { 87 // splat 88 s = strings.Replace(s, `:splat`, `${splat}`, -1) 89 90 // placeholders 91 s = placeholders.ReplaceAllStringFunc(s, func(v string) string { 92 name := v[1:] 93 94 // TODO: refactor to not panic 95 if !names[name] { 96 panic(errors.Errorf("placeholder %q is not present in the path pattern %q", v, path)) 97 } 98 99 return fmt.Sprintf("${%s}", name) 100 }) 101 102 return s 103 } 104 105 // compilePath returns a regexp for substitutions and return 106 // a map of placeholder names for validation. 107 func compilePath(s string) (*regexp.Regexp, map[string]bool) { 108 names := make(map[string]bool) 109 110 // escape 111 s = regexp.QuoteMeta(s) 112 113 // splat 114 s = strings.Replace(s, `\*`, `(?P<splat>.*?)`, -1) 115 116 // placeholders 117 s = placeholders.ReplaceAllStringFunc(s, func(v string) string { 118 name := v[1:] 119 names[name] = true 120 return fmt.Sprintf(`(?P<%s>[^/]+)`, name) 121 }) 122 123 // trailing slash 124 s += `\/?` 125 126 s = fmt.Sprintf(`^%s$`, s) 127 return regexp.MustCompile(s), names 128 } 129 130 // compilePattern to a syntax usable by the trie. 131 func compilePattern(s string) string { 132 return placeholders.ReplaceAllString(s, "*") 133 } 134 135 // isDynamic returns true for splats or placeholders. 136 func isDynamic(s string) bool { 137 return hasPlaceholder(s) || hasSplat(s) 138 } 139 140 // hasPlaceholder returns true for placeholders 141 func hasPlaceholder(s string) bool { 142 return strings.ContainsRune(s, ':') 143 } 144 145 // hasSplat returns true for splats. 146 func hasSplat(s string) bool { 147 return strings.ContainsRune(s, '*') 148 }