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  }