github.com/System-Glitch/goyave/v3@v3.6.1-0.20210226143142-ac2fe42ee80e/parametrizeable.go (about)

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