annotation Athena::Routing::QueryParam

Overview

Used to define (and configure) a query parameter tied to a given argument.

The type of the query param is derived from the type restriction of the associated controller action argument.

Usage

The most basic usage is adding an annotation to a controller action whose name matches a controller action argument. A description may also be included to describe what the query param is used for. In the future this may be used for generating OpenAPI documentation for the related parameter.

A non-nilable type denotes it as required. If the parameter is not supplied, and no default value is assigned, an ART::Exceptions::BadRequest exception is raised.

class ExampleController < ART::Controller
  @[ART::Get("/")]
  @[ART::QueryParam("page", description: "What page of results to return.")] # The name can also be supplied as a named argument like `@[ART::QueryParam(name: "page")]`.
  def index(page : Int32) : Int32
    page
  end
end

ART.run

# GET /?page=2 # => 2
# GET /        # => {"code":422,"message":"Parameter 'page' of value '' violated a constraint: 'This value should not be null.'\n"}

Key

In the case of wanting the controller action argument to have a different name than the actual query parameter, the key option can be used.

class ExampleController < ART::Controller
  @[ART::Get("/")]
  @[ART::QueryParam("foo", key: "bar")]
  def index(foo : String) : String
    foo
  end
end

ART.run

# GET /?bar=value # => "value"

Optional

A nilable type denotes it as optional. If the parameter is not supplied, and no default value is assigned, it is nil.

class ExampleController < ART::Controller
  @[ART::Get("/")]
  @[ART::QueryParam("page")] # The name can also be supplied as a named argument like `@[ART::QueryParam(name: "page")]`.
  def index(page : Int32?) : Int32?
    page
  end
end

ART.run

# GET /          # => null
# GET /?page=2   # => 2
# GET /?page=bar # => {"code":400,"message":"Required parameter 'page' with value 'bar' could not be converted into a valid '(Int32 | Nil)'."}

Strict

By default, parameters are validated strictly; this means an ART::Exceptions::BadRequest exception is raised when the value is considered invalid. Such as if the value does not satisfy the parameter's requirements, it's a required parameter and was not provided, or could not be converted into the desired type.

An example of this is in the first usage example. A 400 bad request was returned when the required parameter was not provided.

When strict mode is disabled, the default value (or nil) will be used instead of raising an exception if the actual value is invalid.

NOTE When setting strict: false, the related controller action argument must be nilable or have a default value.

class ExampleController < ART::Controller
  @[ART::Get("/")]
  @[ART::QueryParam("page", strict: false)]
  def index(page : Int32?) : Int32?
    page
  end
end

ART.run

# GET /          # => null
# GET /?page=2   # => 2
# GET /?page=bar # => null

If strict mode is enabled AND the argument is nilable, the value will only be checked strictly if it is provided and does not meet the parameter's requirements, or could not be converted. If it was not provided at all, nil, or the default value will be used.

Requirements

It's a common practice to validate incoming values before they reach the controller action. ART::QueryParam supports doing just that. It supports validating the value against a Regex pattern, an AVD::Constraint, or an array of AVD::Constraints.

The value is only considered valid if it satisfies the defined requirements. If the value does not match, and strict mode is enabled, a 422 response is returned; otherwise nil, or the default value is used instead.

Regex

The most basic form of validation is a Regex pattern that asserts a value matches the provided pattern.

class ExampleController < ART::Controller
  @[ART::Get("/")]
  @[ART::QueryParam("page", requirements: /\d{2}/)]
  def index(page : Int32) : Int32
    page
  end
end

ART.run

# GET /          # => {"code":422,"message":"Parameter 'page' of value '' violated a constraint: 'This value should not be null.'\n"}
# GET /?page=10  # => 10
# GET /?page=bar # => {"code":400,"message":"Required parameter 'page' with value 'bar' could not be converted into a valid 'Int32'."}
# GET /?page=5   # => {"code":422,"message":"Parameter 'page' of value '5' violated a constraint: 'Parameter 'page' value does not match requirements: (?-imsx:^(?-imsx:\\d{2})$)'\n"}

Constraint(s)

In some cases validating a value may require more logic than is possible via a regular expression. A parameter's requirements can also be set to a specific, or array of, Assert AVD::Constraint annotations.

class ExampleController < ART::Controller
  @[ART::Get("/")]
  @[ART::QueryParam("page", requirements: @[Assert::PositiveOrZero])]
  def index(page : Int32) : Int32
    page
  end
end

ART.run

# GET /?page=2  # => 2
# GET /?page=-5 # => {"code":422,"message":"Parameter 'page' of value '-9' violated a constraint: 'This value should be positive or zero.'\n"}

See the external documentation for more information.

Map

By default, the parameter's requirements are applied against the resulting value, which makes sense when working with scalar values. However, if the parameter is an Array of values, then it may make more sense to run the validations against each item in that array, as opposed to on the whole array itself.

This behavior can be enabled by using the map: true option, which essentially wraps all the requirements within an AVD::Constraints::All constraint.

class ExampleController < ART::Controller
  @[ART::Get("/")]
  @[ART::QueryParam("ids", map: true, requirements: [@[Assert::Positive], @[Assert::Range(-3..10)]])]
  def index(ids : Array(Int32)) : Array(Int32)
    ids
  end
end

ART.run

# GET /               # => {"code":422,"message":"Parameter 'ids' of value '' violated a constraint: 'This value should not be null.'\n"}
# GET /?ids=10&ids=2  # => [10,2]
# GET /?ids=10&ids=-2 # => {"code":422,"message":"Parameter 'ids[1]' of value '-2' violated a constraint: 'This value should be positive.'\n"}

Incompatibles

Incompatibles represent the parameters that can't be present at the same time.

class ExampleController < ART::Controller
  @[ART::Get("/")]
  @[ART::QueryParam("bar")]
  @[ART::QueryParam("foo", incompatibles: ["bar"])]
  def index(foo : String?, bar : String?) : String
    "#{foo}-#{bar}"
  end
end

ART.run

# GET /?bar=bar         # => "-bar"
# GET /?foo=foo         # => "foo-"
# GET /?foo=foo&bar=bar # => {"code":400,"message":"Parameter 'foo' is incompatible with parameter 'bar'."}

Param Converters

While Athena is able to auto convert query parameters from their String representation to Bool, or Number types, it is unable to do that for more complex types, such as Time. In such cases an ART::ParamConverterInterface is required.

For simple converters that do not require any additional configuration, you can just specify the ART::ParamConverterInterface.class you wish to use for this query parameter. Default and nilable values work as they do when not using a converter.

class ExampleController < ART::Controller
  @[ART::QueryParam("start_time", converter: ART::TimeConverter)]
  @[ART::Get("/time")]
  def time(start_time : Time = Time.utc) : String
    "Starting at: #{start_time}"
  end
end

ART.run

# GET /time                                 # => "Starting at: 2020-11-25 20:29:55 UTC"
# GET /time?start_time=2020-04-07T12:34:56Z # => "Starting at: 2020-04-07 12:34:56 UTC"

Extra Configuration

In some cases a param converter may require additional configuration. In this case a NamedTuple may be provided as the value of converter. The named tuple must contain a name key that represents the ART::ParamConverterInterface.class you wish to use for this query parameter. Any additional key/value pairs will be passed to the param converter.

class ExampleController < ART::Controller
  @[ART::QueryParam("start_time", converter: {name: ART::TimeConverter, format: "%Y--%m//%d  %T"})]
  @[ART::Get("/time")]
  def time(start_time : Time) : String
    "Starting at: #{start_time}"
  end
end

ART.run

# GET /time?start_time="2020--04//07  12:34:56" # => "Starting at: 2020-04-07 12:34:56 UTC"

NOTE The dedicated ART::ParamConverter annotation may be used as well, just be sure to give it and the query parameter the same name.

Defined in:

annotations.cr