Skip to content

class Athena::Routing::Route
inherits Reference #

Provides an object-oriented way to represent an HTTP route, including the path, methods, schemes, host, and/or conditions required for it to match.

Ultimately, ART::Routes are compiled into ART::CompiledRoute that represents an immutable snapshot of a route, along with ART::CompiledRoute::Tokens representing each route parameter.

By default, a route is very liberal in regards to what allows when matching. E.g. Matching anything that matches the path, but with any HTTP method and any scheme. The methods and schemes properties can be used to restrict which methods/schemes the route allows.

# This route will only handle `https` `POST` requests to `/path`.
route1 = ART::Route.new "/path", schemes: "https", methods: "POST"

# This route will handle `http` or `ftp` `GET`/`PATCH` requests to `/path`.
route2 = ART::Route.new "/path", schemes: {"https", "ftp"}, methods: {"GET", "PATCH"}

Expressions#

In some cases you may want to match a route using arbitrary dynamic runtime logic. An example use case for this could be checking a request header, or anything else on the underlying ART::RequestContext and/or ART::Request instance. The condition property can be used for just this purpose:

route = ART::Route.new "/contact"
route.condition do |context, request|
  request.headers["user-agent"].includes? "Firefox"
end

This route would only match requests whose user-agent header includes Firefox. Be sure to also handle cases where headers may not be set.

Warning

Route conditions are NOT taken into consideration when generating routes via an ART::Generator::Interface.

Parameters#

Route parameters represent variable portions within a route's path. Parameters are uniquely named placeholders wrapped within curly braces. For example, /blog/{slug} includes a slug parameter. Routes can have more than one parameter, but each one may only map to a single value. Parameter placeholders may also be included with static portions for a string, such as /blog/posts-about-{category}. This can be useful for supporting format based URLs, such as /users.json or /users.csv via a /users.{_format} path.

Parameter Validation#

By default, a placeholder is happy to accept any value. However in most cases you will want to restrict which values it allows, such as ensuring only numeric digits are allowed for a page parameter. Parameter validation also allows multiple routes to have variable portions within the same location. I.e. allowing /blog/{slug} and /blog/{page} to co-exist, which is a limitation for some other Crystal routers.

The requirements property accepts a Hash(String, String | Regex) where the keys are the name of the parameter and the value is a pattern in which the value must match for the route to match. The value can either be a string for exact matches, or a Regex for more complex patterns.

Route parameters may also be inlined within the path by putting the pattern within <>, instead of providing it as a dedicated argument. For example, /blog/{page<\\d+>} (note we need to escape the \ within a string literal).

routes = ART::RouteCollection.new
routes.add "blog_list", ART::Route.new "/blog/{page}", requirements: {"page" => /\d+/}
routes.add "blog_show", ART::Route.new "/blog/{slug}"

matcher.match "/blog/foo" # => {"_route" => "blog_show", "slug" => "foo"}
matcher.match "/blog/10"  # => {"_route" => "blog_list", "page" => "10"}

Tip

Checkout ART::Requirement for a set of common, helpful requirement regexes.

Optional Parameters#

By default, all parameters are required, meaning given the path /blog/{page}, /blog/10 would match but /blog would NOT match. Parameters can be made optional by providing a default value for the parameter, for example:

ART::Route.new "/blog/{page}", {"page" => 1}, {"page" => /\d+/}

# ...

matcher.match "/blog" # => {"_route" => "blog_list", "page" => "1"}

Caution

More than one parameter may have a default value, but everything after an optional parameter must also be optional. For example within /{page}/blog, page will always be required and /blog will NOT match.

defaults may also be inlined within the path by putting the value after a ?. This is also compatible with requirements, allowing both to be defined within a path. For example /blog/{page<\\d+>?1}.

Tip

The default value for a parameter may also be nil, with the inline syntax being adding a ? with no following value, e.g. {page?}. Be sure to update any type restrictions to be nilable as well.

Priority Parameter#

When determining which route should match, the first matching route will win. For example, if two routes were added with variable parameters in the same location, the first one that was added would match regardless of what their requirements are. In most cases this will not be a problem, but in some cases you may need to ensure a particular route is checked first.

Special Parameters#

The routing component comes with a few standardized parameters that have special meanings. These parameters could be leveraged within the underlying implementation, but are not directly used within the routing component other than for matching.

  • _format - Could be used to set the underlying format of the request, as well as determining the content-type of the response.
  • _fragment - Represents the fragment identifier when generating a URL. E.g. /article/10#summary with the fragment being summary.
  • _locale - Could be used to set the underlying locale of the ART::Request based on which route is matched.
ART::Route.new(
  "/articles/{_locale}/search.{_format}",
  {
    "_locale" => "en",
    "_format" => "html",
  },
  {
    "_locale" => /en|fr/,
    "_format" => /html|xml/,
  }
)

This route supports en and fr locales in either html or xml formats with a default of en and html.

Tip

The trailing . is optional if the parameter to the right has a default. E.g. /articles/en/search would match with a format of html but /articles/en/search.xml would be required for matching non-default formats.

Extra Parameters#

The defaults defined within a route do not all need to be present as route parameters. This could be useful to provide extra context to the controller that should handle the request.

ART::Route.new "/blog/{page}", {"page" => 1, "title" => "Hello world!"}

Slash Characters in Route Parameters#

By default, route parameters may include any value except a /, since that's the character used to separate the different portions of the URL. Route parameter matching logic may be made more permissive by using a more liberal regex, such as .+, for example:

ART::Route.new "/share/{token}", requirements: {"token" => /.+/}

Special parameters should NOT be made more permissive. For example, if the pattern is /share/{token}.{_format} and {token} allows any character, the /share/foo/bar.json URL will consider foo/bar.json as the token and the format will be empty. This can be solved by replacing the .+ requirement with [^.]+ to allow any character except dots.

Related to this, allowing multiple parameters to accept / may also lead to unexpected results.

Sub-Domain Routing#

The host property can be used to require the HTTP host header to match this value in order for the route to match.

mobile_homepage = ART::Route.new "/", host: "m.example.com"
homepage = ART::Route.new "/"

In this example, both routes match the same path, but one requires a specific hostname. The host parameter can also be used as route parameters, including defaults and requirements support:

mobile_homepage = ART::Route.new(
  "/",
  {"subdomain" => "m"},
  {"subdomain" => /m|mobile/},
  "{subdomain}.example.com"
)
homepage = ART::Route.new "/"

Tip

Inline defaults and requirements also works for host values, "{subdomain<m|mobile>?m}.example.com".

Constructors#

.new(path : String, defaults : Hash(String, _) = Hash(String, String | ::Nil).new, requirements : Hash(String, Regex | String) = Hash(String, Regex | String).new, host : String | Regex | Nil = nil, methods : String | Enumerable(String) | Nil = nil, schemes : String | Enumerable(String) | Nil = nil, condition : ART::Route::Condition | Nil = nil)#

Methods#

#add_defaults(defaults : Hash(String, _)) : self#

Adds the provided defaults, overriding previously set values.

#add_requirements(requirements : Hash(String, Regex | String)) : self#

Adds the provided requirements, overriding previously set values.

#clone#

Returns a copy of self with all instance variables cloned.

#compile : CompiledRoute#

Compiles and returns an ART::CompiledRoute representing this route. The route is only compiled once and future calls to this method will return the same compiled route, assuming no changes were made to this route in between.

#condition : Condition | ::Nil#

Returns the optional ART::Route::Condition callback used to determine if this route should match. See Routing Expressions for more information.

#condition : self#

Sets the optional ART::Route::Condition callback used to determine if this route should match.

route = ART::Route.new "/foo"
route.condition do |context, request|
  request.headers["user-agent"].includes? "Firefox"
end

See Routing Expressions for more information.

#condition=(condition : Condition | Nil)#

Returns the optional ART::Route::Condition callback used to determine if this route should match. See Routing Expressions for more information.

#default(key : String) : String | Nil#

Returns the default with the provided key, if any.

#defaults : Hash(String, String | ::Nil)#

Returns a hash representing the default values of a route's parameters if they were not provided in the request. See Optional Parameters for more information.

#defaults=(defaults : Hash(String, _)) : self#

Sets the hash representing the default values of a route's parameters if they were not provided in the request to the provided defaults. See Optional Parameters for more information.

#has_default?(key : String) : Bool#

Returns true if this route has a default with the provided key, otherwise false.

#has_requirement?(key : String) : Bool#

Returns true if this route has a requirement with the provided key, otherwise false.

#has_scheme?(scheme : String) : Bool#

Returns true if this route allows the provided scheme, otherwise false.

#host : String | ::Nil#

Returns the hostname that the HTTP host header must match in order for this route to match. See Sub-Domain Routing for more information.

#host=(pattern : String | Regex) : self#

Sets the hostname that the HTTP host header must match in order for this route to match to the provided pattern. See Sub-Domain Routing for more information.

#methods : Set(String) | ::Nil#

Returns the set of valid HTTP methods that this route supports. See ART::Route for more information.

#methods=(methods : String | Enumerable(String)) : self#

Sets the set of valid HTTP method(s) that this route supports. See ART::Route for more information.

#path : String#

Returns the URL that this route will handle. See Routing Parameters for more information.

#path=(pattern : String) : self#

Sets the path required for this route to match to the provided pattern.

#requirement(key : String) : Regex | Nil#

Returns the requirement with the provided key, if any.

#requirements : Hash(String, Regex)#

Returns a hash representing the requirements the route's parameters must match in order for this route to match. See Parameter Validation for more information.

#requirements=(requirements : Hash(String, Regex | String)) : self#

Sets the hash representing the requirements the route's parameters must match in order for this route to match to the provided requirements. See Parameter Validation for more information.

#schemes : Set(String) | ::Nil#

Returns the set of valid URI schemes that this route supports. See ART::Route for more information.

#schemes=(schemes : String | Enumerable(String)) : self#

Sets the set of valid URI scheme(s) that this route supports. See ART::Route for more information.

#set_default(key : String, value : String | Nil) : self#

Sets the default with the provided key to the provided value.

#set_requirement(key : String, requirement : Regex | String) : self#

Sets the requirement with the provided key to the provided value.