goyave.dev/goyave/v4@v4.4.11/parametrizeable.go (about)

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