goyave.dev/goyave/v5@v5.0.0-rc9.0.20240517145003-d3f977d0b9f3/route.go (about)

     1  package goyave
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"strings"
     7  
     8  	"github.com/samber/lo"
     9  	"goyave.dev/goyave/v5/cors"
    10  	"goyave.dev/goyave/v5/util/errors"
    11  	"goyave.dev/goyave/v5/validation"
    12  )
    13  
    14  // Route stores information for route matching and serving and can be
    15  // used to generate dynamic URLs/URIs. Routes can, just like routers,
    16  // hold Meta information that can be used by generic middleware to
    17  // alter their behavior depending on the route being served.
    18  type Route struct {
    19  	name    string
    20  	uri     string
    21  	methods []string
    22  	parent  *Router
    23  	Meta    map[string]any
    24  	handler Handler
    25  	middlewareHolder
    26  	parameterizable
    27  }
    28  
    29  var _ routeMatcher = (*Route)(nil) // implements routeMatcher
    30  
    31  // RuleSetFunc function generating a new validation rule set.
    32  // This function is called for every validated request.
    33  // The returned value is expected to be fresh, not re-used across
    34  // multiple requests nor concurrently.
    35  type RuleSetFunc func(*Request) validation.RuleSet
    36  
    37  // newRoute create a new route without any settings except its handler.
    38  // This is used to generate a fake route for the Method Not Allowed and Not Found handlers.
    39  // This route has the core middleware enabled and can be used without a parent router.
    40  // Thus, custom status handlers can use language and body.
    41  func newRoute(handler Handler, name string) *Route {
    42  	return &Route{
    43  		name:    name,
    44  		handler: handler,
    45  		Meta:    make(map[string]any),
    46  		middlewareHolder: middlewareHolder{
    47  			middleware: nil,
    48  		},
    49  	}
    50  }
    51  
    52  func (r *Route) match(method string, match *routeMatch) bool {
    53  	if params := r.parameterizable.regex.FindStringSubmatch(match.currentPath); params != nil {
    54  		if r.checkMethod(method) {
    55  			if len(params) > 1 {
    56  				match.mergeParams(r.makeParameters(params))
    57  			}
    58  			match.route = r
    59  			return true
    60  		}
    61  		match.err = errMatchMethodNotAllowed
    62  		return false
    63  	}
    64  
    65  	if match.err == nil {
    66  		// Don't override error if already set.
    67  		// Not nil error means it's either already errMatchNotFound
    68  		// or it's errMatchMethodNotAllowed, implying that a route has
    69  		// already been matched but with wrong method.
    70  		match.err = errMatchNotFound
    71  	}
    72  	return false
    73  }
    74  
    75  func (r *Route) checkMethod(method string) bool {
    76  	for _, m := range r.methods {
    77  		if m == method {
    78  			return true
    79  		}
    80  	}
    81  	return false
    82  }
    83  
    84  func (r *Route) makeParameters(match []string) map[string]string {
    85  	return r.parameterizable.makeParameters(match, r.parameters)
    86  }
    87  
    88  // Name set the name of the route.
    89  // Panics if a route with the same name already exists.
    90  // Returns itself.
    91  func (r *Route) Name(name string) *Route {
    92  	if r.name != "" {
    93  		panic(errors.NewSkip("route name is already set", 3))
    94  	}
    95  
    96  	if _, ok := r.parent.namedRoutes[name]; ok {
    97  		panic(errors.NewSkip(fmt.Errorf("route %q already exists", name), 3))
    98  	}
    99  
   100  	r.name = name
   101  	r.parent.namedRoutes[name] = r
   102  	return r
   103  }
   104  
   105  // SetMeta attach a value to this route identified by the given key.
   106  //
   107  // This value can override a value inherited by the parent routers for this route only.
   108  func (r *Route) SetMeta(key string, value any) *Route {
   109  	r.Meta[key] = value
   110  	return r
   111  }
   112  
   113  // RemoveMeta detach the meta value identified by the given key from this route.
   114  // This doesn't remove meta using the same key from the parent routers.
   115  func (r *Route) RemoveMeta(key string) *Route {
   116  	delete(r.Meta, key)
   117  	return r
   118  }
   119  
   120  // LookupMeta value identified by the given key. If not found in this route,
   121  // the value is recursively fetched in the parent routers.
   122  //
   123  // Returns the value and `true` if found in the current route or one of the
   124  // parent routers, `nil` and `false` otherwise.
   125  func (r *Route) LookupMeta(key string) (any, bool) {
   126  	val, ok := r.Meta[key]
   127  	if ok {
   128  		return val, ok
   129  	}
   130  	if r.parent == nil {
   131  		return nil, false
   132  	}
   133  	return r.parent.LookupMeta(key)
   134  }
   135  
   136  // ValidateBody adds (or replace) validation rules for the request body.
   137  func (r *Route) ValidateBody(validationRules RuleSetFunc) *Route {
   138  	validationMiddleware := findMiddleware[*validateRequestMiddleware](r.middleware)
   139  	if validationMiddleware == nil {
   140  		r.Middleware(&validateRequestMiddleware{BodyRules: validationRules})
   141  	} else {
   142  		validationMiddleware.BodyRules = validationRules
   143  	}
   144  	return r
   145  }
   146  
   147  // ValidateQuery adds (or replace) validation rules for the request query.
   148  func (r *Route) ValidateQuery(validationRules RuleSetFunc) *Route {
   149  	validationMiddleware := findMiddleware[*validateRequestMiddleware](r.middleware)
   150  	if validationMiddleware == nil {
   151  		r.Middleware(&validateRequestMiddleware{QueryRules: validationRules})
   152  	} else {
   153  		validationMiddleware.QueryRules = validationRules
   154  	}
   155  	return r
   156  }
   157  
   158  // CORS set the CORS options for this route only.
   159  // The "OPTIONS" method is added if this route doesn't already support it.
   160  //
   161  // If the options are not `nil`, the CORS middleware is automatically added globally.
   162  // To disable CORS, give `nil` options. The "OPTIONS" method will be removed
   163  // if it isn't the only method for this route.
   164  func (r *Route) CORS(options *cors.Options) *Route {
   165  	i := lo.IndexOf(r.methods, http.MethodOptions)
   166  	if options == nil {
   167  		r.Meta[MetaCORS] = nil
   168  		if len(r.methods) > 1 && i != -1 {
   169  			r.methods = append(r.methods[:i], r.methods[i+1:]...)
   170  		}
   171  		return r
   172  	}
   173  	r.Meta[MetaCORS] = options
   174  	if !hasMiddleware[*corsMiddleware](r.parent.globalMiddleware.middleware) {
   175  		r.parent.GlobalMiddleware(&corsMiddleware{})
   176  	}
   177  	if i == -1 {
   178  		r.methods = append(r.methods, http.MethodOptions)
   179  	}
   180  	return r
   181  }
   182  
   183  // Middleware register middleware for this route only.
   184  //
   185  // Returns itself.
   186  func (r *Route) Middleware(middleware ...Middleware) *Route {
   187  	r.middleware = append(r.middleware, middleware...)
   188  	for _, m := range middleware {
   189  		m.Init(r.parent.server)
   190  	}
   191  	return r
   192  }
   193  
   194  // BuildURL build a full URL pointing to this route.
   195  // Panics if the amount of parameters doesn't match the amount of
   196  // actual parameters for this route.
   197  func (r *Route) BuildURL(parameters ...string) string {
   198  	return r.parent.server.BaseURL() + r.BuildURI(parameters...)
   199  }
   200  
   201  // BuildProxyURL build a full URL pointing to this route using the proxy base URL.
   202  // Panics if the amount of parameters doesn't match the amount of
   203  // actual parameters for this route.
   204  func (r *Route) BuildProxyURL(parameters ...string) string {
   205  	return r.parent.server.ProxyBaseURL() + r.BuildURI(parameters...)
   206  }
   207  
   208  // BuildURI build a full URI pointing to this route. The returned
   209  // string doesn't include the protocol and domain. (e.g. "/user/login")
   210  // Panics if the amount of parameters doesn't match the amount of
   211  // actual parameters for this route.
   212  func (r *Route) BuildURI(parameters ...string) string {
   213  	fullURI, fullParameters := r.GetFullURIAndParameters()
   214  
   215  	if len(parameters) != len(fullParameters) {
   216  		panic(errors.Errorf("BuildURI: route has %d parameters, %d given", len(fullParameters), len(parameters)))
   217  	}
   218  
   219  	var builder strings.Builder
   220  	builder.Grow(len(fullURI))
   221  
   222  	idxs, _ := r.braceIndices(fullURI)
   223  	length := len(idxs)
   224  	end := 0
   225  	currentParam := 0
   226  	for i := 0; i < length; i += 2 {
   227  		raw := fullURI[end:idxs[i]]
   228  		end = idxs[i+1]
   229  		builder.WriteString(raw)
   230  		builder.WriteString(parameters[currentParam])
   231  		currentParam++
   232  		end++ // Skip closing braces
   233  	}
   234  	builder.WriteString(fullURI[end:])
   235  
   236  	return builder.String()
   237  }
   238  
   239  // GetName get the name of this route.
   240  func (r *Route) GetName() string {
   241  	return r.name
   242  }
   243  
   244  // GetURI get the URI of this route.
   245  // The returned URI is relative to the parent router of this route, it is NOT
   246  // the full path to this route.
   247  //
   248  // Note that this URI may contain route parameters in their définition format.
   249  // Use the request's URI if you want to see the URI as it was requested by the client.
   250  func (r *Route) GetURI() string {
   251  	return r.uri
   252  }
   253  
   254  // GetFullURI get the full URI of this route.
   255  //
   256  // Note that this URI may contain route parameters in their définition format.
   257  // Use the request's URI if you want to see the URI as it was requested by the client.
   258  func (r *Route) GetFullURI() string {
   259  	router := r.parent
   260  	segments := make([]string, 0, 3)
   261  	segments = append(segments, r.uri)
   262  
   263  	for router != nil {
   264  		segments = append(segments, router.prefix)
   265  		router = router.parent
   266  	}
   267  
   268  	// Revert segements
   269  	for i := len(segments)/2 - 1; i >= 0; i-- {
   270  		opp := len(segments) - 1 - i
   271  		segments[i], segments[opp] = segments[opp], segments[i]
   272  	}
   273  
   274  	return strings.Join(segments, "")
   275  }
   276  
   277  // GetMethods returns the methods the route matches against.
   278  func (r *Route) GetMethods() []string {
   279  	cpy := make([]string, len(r.methods))
   280  	copy(cpy, r.methods)
   281  	return cpy
   282  }
   283  
   284  // GetHandler returns the Handler associated with this route.
   285  func (r *Route) GetHandler() Handler {
   286  	return r.handler
   287  }
   288  
   289  // GetParent returns the parent Router of this route.
   290  func (r *Route) GetParent() *Router {
   291  	return r.parent
   292  }
   293  
   294  // GetFullURIAndParameters get the full uri and parameters for this route and all its parent routers.
   295  func (r *Route) GetFullURIAndParameters() (string, []string) {
   296  	router := r.parent
   297  	segments := make([]string, 0, 3)
   298  	segments = append(segments, r.uri)
   299  
   300  	parameters := make([]string, 0, len(r.parameters))
   301  	for i := len(r.parameters) - 1; i >= 0; i-- {
   302  		parameters = append(parameters, r.parameters[i])
   303  	}
   304  
   305  	for router != nil {
   306  		segments = append(segments, router.prefix)
   307  		for i := len(router.parameters) - 1; i >= 0; i-- {
   308  			parameters = append(parameters, router.parameters[i])
   309  		}
   310  		router = router.parent
   311  	}
   312  
   313  	// Revert segements
   314  	for i := len(segments)/2 - 1; i >= 0; i-- {
   315  		opp := len(segments) - 1 - i
   316  		segments[i], segments[opp] = segments[opp], segments[i]
   317  	}
   318  
   319  	// Revert parameters
   320  	for i := len(parameters)/2 - 1; i >= 0; i-- {
   321  		opp := len(parameters) - 1 - i
   322  		parameters[i], parameters[opp] = parameters[opp], parameters[i]
   323  	}
   324  
   325  	return strings.Join(segments, ""), parameters
   326  }