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::Constraint
s.
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.