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