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  }