abstract class Athena::Framework::Controller
inherits Reference
#
The core of any framework is routing; how a route is tied to an action.
Athena takes an annotation based approach; an annotation, such as ARTA::Get
is applied to an instance method of a controller class, which will be executed when that endpoint receives a request.
Additional annotation also exist for defining a query parameter. See ATHA::QueryParam
for more information.
Child controllers must inherit from ATH::Controller
(or an abstract child of it). Each request gets its own instance of the controller to better allow for DI via Athena::DependencyInjection
.
A route action can either return an ATH::Response
, or some other type. If an ATH::Response
is returned, then it is used directly. Otherwise an ATH::Events::View
is emitted to convert
the action result into an ATH::Response
. By default, ATH::Listeners::View
will JSON encode the value if it is not handled earlier by another listener.
Example#
The following controller shows examples of the various routing features of Athena. ATH::Controller
also defines various macro DSLs, such as ATH::Controller.get
to make defining routes
seem more Sinatra/Kemal like. See the documentation on the macros for more details.
require "athena"
require "mime"
# The `ARTA::Route` annotation can also be applied to a controller class.
# This can be useful for applying a common path prefix, defaults, requirements,
# etc. to all actions in the controller.
@[ARTA::Route(path: "/athena")]
class TestController < ATH::Controller
# A GET endpoint returning an `ATH::Response`.
# Can be used to return raw data, such as HTML or CSS etc, in a one-off manor.
@[ARTA::Get(path: "/index")]
def index : ATH::Response
ATH::Response.new "<h1>Welcome to my website!</h1>", headers: HTTP::Headers{"content-type" => MIME.from_extension(".html")}
end
# A GET endpoint returning an `ATH::StreamedResponse`.
# Can be used to stream the response content to the client;
# useful if the content is too large to fit into memory.
@[ARTA::Get(path: "/users")]
def users : ATH::Response
ATH::StreamedResponse.new headers: HTTP::Headers{"content-type" => "application/json; charset=utf-8"} do |io|
User.all.to_json io
end
end
# A GET endpoint with no params returning a `String`.
#
# Action return type restrictions are required.
@[ARTA::Get("/me")]
def get_me : String
"Jim"
end
# A GET endpoint with no params returning `Nil`.
# `Nil` return types are returned with a status
# of 204 no content
@[ARTA::Get("/no_content")]
def get_no_content : Nil
# Do stuff
end
# A GET endpoint with two `Int32` params returning an `Int32`.
#
# The parameters of a route _MUST_ match the parameters of the action.
# Type restrictions on action parameters are required.
@[ARTA::Get("/add/{val1}/{val2}")]
def add(val1 : Int32, val2 : Int32) : Int32
val1 + val2
end
# A GET endpoint with a required trailing slash, a `String` route param,
# and a required string query param that must match the given pattern; returning a `String`.
#
# Athena treats non `GET`/`HEAD` routes with a trailing slash as unique
# E.g. `POST /foo/bar/` versus `POST /foo/bar`.
# Be sure to keep you routes consistent!
#
# A non-nilable type denotes it as required. If the parameter is not supplied,
# and no default value is assigned, an `ATH::Exception::BadRequest` exception is raised.
@[ARTA::Get("/event/{event_name}/")]
@[ATHA::QueryParam("time", requirements: /\d:\d:\d/)]
def event_time(event_name : String, time : String) : String
"#{event_name} occurred at #{time}"
end
# A GET endpoint with an optional query parameter and optional path param
# with a default value; returning a `NamedTuple(user_id : Int32?, page : Int32)`.
#
# A nilable type denotes it as optional.
# If the parameter is not supplied (or could not be converted),
# and no default value is assigned, it is `nil`.
@[ATHA::QueryParam("user_id")]
@[ARTA::Get("/events/{page}")]
def events(user_id : Int32?, page : Int32 = 1) : NamedTuple(user_id: Int32?, page: Int32)
{user_id: user_id, page: page}
end
# A GET endpoint with route parameter requirements.
# The parameter must match the supplied Regex or this route will not be matched.
#
# This feature can allow multiple routes to exist with parameters in the same location,
# but with different requirements.
@[ARTA::Get("/time/{time}/", requirements: {"time" => /\d{2}:\d{2}:\d{2}/})]
def get_constraint(time : String) : String
time
end
# A POST endpoint with a route param and accessing the request body; returning a `Bool`.
#
# It is recommended to use param converters to pass an actual object representing the data (assuming the body is JSON)
# to the route's action; however the raw request body can be accessed by typing an action argument as `ATH::Request`.
@[ARTA::Post("/test/{expected}")]
def post_body(expected : String, request : ATH::Request) : Bool
expected == request.body.try &.gets_to_end
end
# An endpoint may also have more than one route annotation applied to it.
# This can be useful in allowing for a route to support multiple aliases.
@[ARTA::Get("/users/{id}")]
@[ARTA::Get("/people/{id}")]
def get_user(id : Int64) : User
# Fetch the user
user = ...
user
end
end
ATH.run
# GET /athena/index # => <h1>Welcome to my website!</h1>
# GET /athena/users # => [{"id":1,...},...]
# GET /athena/wakeup/17 # => Morning, Allison it is currently 2020-02-01 18:38:12 UTC.
# GET /athena/me # => "Jim"
# GET /athena/add/50/25 # => 75
# GET /athena/event/foobar?time=1:1:1 # => "foobar occurred at 1:1:1"
# GET /athena/event/foobar/?time=1:1:1 # => "foobar occurred at 1:1:1"
# GET /athena/events # => {"user_id":null,"page":1}
# GET /athena/events/17?user_id=19 # => {"user_id":19,"page":17}
# GET /athena/time/12:45:30 # => "12:45:30"
# GET /athena/time/12:aa:30 # => 404 not found
# GET /athena/no_content # => 204 no content
# GET /athena/users/19 # => {"user_id":19}
# GET /athena/people/19 # => {"user_id":19}
# POST /athena/test/foo, body: "foo" # => true
Methods#
#generate_url(route : String, params : Hash(String, _) = Hash(String, String | ::Nil).new, reference_type : ART::Generator::ReferenceType = :absolute_path) : String
#
Generates a URL to the provided route with the provided params.
See ART::Generator::Interface#generate
.
#generate_url(route : String, reference_type : ART::Generator::ReferenceType = :absolute_path, **params)
#
Generates a URL to the provided route with the provided params.
See ART::Generator::Interface#generate
.
#redirect(url : String | Path, status : HTTP::Status = HTTP::Status::FOUND) : ATH::RedirectResponse
#
Returns an ATH::RedirectResponse
to the provided url, optionally with the provided status.
class ExampleController < ATH::Controller
@[ARTA::Get("redirect/google")]
def redirect_to_google : ATH::RedirectResponse
self.redirect "https://google.com"
end
end
#redirect_to_route(route : String, params : Hash(String, _) = Hash(String, String | ::Nil).new, status : HTTP::Status = :found) : ATH::RedirectResponse
#
Returns an ATH::RedirectResponse
to the provided route with the provided params.
require "athena"
class ExampleController < ATH::Controller
# Define a route to redirect to, explicitly naming this route `add`.
# The default route name is controller + method down snake-cased; e.x. `example_controller_add`.
@[ARTA::Get("/add/{value1}/{value2}", name: "add")]
def add(value1 : Int32, value2 : Int32, negative : Bool = false) : Int32
sum = value1 + value2
negative ? -sum : sum
end
# Define a route that redirects to the `add` route with fixed parameters.
@[ARTA::Get("/")]
def redirect : ATH::RedirectResponse
self.redirect_to_route "add", {"value1" => 8, "value2" => 2}
end
end
ATH.run
# GET / # => 10
#redirect_to_route(route : String, status : HTTP::Status = :found, **params) : ATH::RedirectResponse
#
Returns an ATH::RedirectResponse
to the provided route with the provided params.
require "athena"
class ExampleController < ATH::Controller
# Define a route to redirect to, explicitly naming this route `add`.
# The default route name is controller + method down snake-cased; e.x. `example_controller_add`.
@[ARTA::Get("/add/{value1}/{value2}", name: "add")]
def add(value1 : Int32, value2 : Int32, negative : Bool = false) : Int32
sum = value1 + value2
negative ? -sum : sum
end
# Define a route that redirects to the `add` route with fixed parameters.
@[ARTA::Get("/")]
def redirect : ATH::RedirectResponse
self.redirect_to_route "add", value1: 8, value2: 2
end
end
ATH.run
# GET / # => 10
#redirect_view(url : Status, status : HTTP::Status = HTTP::Status::FOUND, headers : HTTP::Headers = HTTP::Headers.new) : ATH::View
#
Returns an ATH::View
that'll redirect to the provided url, optionally with the provided status and headers.
Is essentially the same as #redirect
, but invokes the view layer.
#route_redirect_view(route : Status, params : Hash(String, _) = Hash(String, String | ::Nil).new, status : HTTP::Status = HTTP::Status::CREATED, headers : HTTP::Headers = HTTP::Headers.new) : ATH::View
#
Returns an ATH::View
that'll redirect to the provided route, optionally with the provided params, status, and headers.
Is essentially the same as #redirect_to_route
, but invokes the view layer.
#view(data = nil, status : HTTP::Status | Nil = nil, headers : HTTP::Headers = HTTP::Headers.new) : ATH::View
#
Returns an ATH::View
with the provided data, and optionally status and headers.
@[ARTA::Get("/{name}")]
def say_hello(name : String) : ATH::View(NamedTuple(greeting: String))
self.view({greeting: "Hello #{name}"}, :im_a_teapot)
end
Macros#
delete
#
Helper DSL macro for creating DELETE
actions.
The first argument is the path that the action should handle; which maps to path on the HTTP method annotation.
The second argument is a variable amount of arguments with a syntax similar to Crystal's record
.
There are also a few optional named arguments that map to the corresponding field on the HTTP method annotation.
The macro simply defines a method based on the options passed to it. Additional annotations, such as for query params or a param converter can simply be added on top of the macro.
Optional Named Arguments#
return_type
- The return type to set for the action. Defaults toString
if not provided.constraints
- Any constraints that should be applied to the route.
Example#
class ExampleController < ATH::Controller
delete "values/{value1<\\d+>}/{value2<\\d+\\.\\d+>}", value1 : Int32, value2 : Float64 do
"Value1: #{value1} - Value2: #{value2}"
end
end
get
#
Helper DSL macro for creating GET
actions.
The first argument is the path that the action should handle; which maps to path on the HTTP method annotation.
The second argument is a variable amount of arguments with a syntax similar to Crystal's record
.
There are also a few optional named arguments that map to the corresponding field on the HTTP method annotation.
The macro simply defines a method based on the options passed to it. Additional annotations, such as for query params or a param converter can simply be added on top of the macro.
Optional Named Arguments#
return_type
- The return type to set for the action. Defaults toString
if not provided.constraints
- Any constraints that should be applied to the route.
Example#
class ExampleController < ATH::Controller
get "values/{value1<\\d+>}/{value2<\\d+\\.\\d+>}", value1 : Int32, value2 : Float64 do
"Value1: #{value1} - Value2: #{value2}"
end
end
head
#
Helper DSL macro for creating HEAD
actions.
The first argument is the path that the action should handle; which maps to path on the HTTP method annotation.
The second argument is a variable amount of arguments with a syntax similar to Crystal's record
.
There are also a few optional named arguments that map to the corresponding field on the HTTP method annotation.
The macro simply defines a method based on the options passed to it. Additional annotations, such as for query params or a param converter can simply be added on top of the macro.
Optional Named Arguments#
return_type
- The return type to set for the action. Defaults toString
if not provided.constraints
- Any constraints that should be applied to the route.
Example#
class ExampleController < ATH::Controller
head "values/{value1<\\d+>}/{value2<\\d+\\.\\d+>}", value1 : Int32, value2 : Float64 do
"Value1: #{value1} - Value2: #{value2}"
end
end
link
#
Helper DSL macro for creating LINK
actions.
The first argument is the path that the action should handle; which maps to path on the HTTP method annotation.
The second argument is a variable amount of arguments with a syntax similar to Crystal's record
.
There are also a few optional named arguments that map to the corresponding field on the HTTP method annotation.
The macro simply defines a method based on the options passed to it. Additional annotations, such as for query params or a param converter can simply be added on top of the macro.
Optional Named Arguments#
return_type
- The return type to set for the action. Defaults toString
if not provided.constraints
- Any constraints that should be applied to the route.
Example#
class ExampleController < ATH::Controller
link "values/{value1<\\d+>}/{value2<\\d+\\.\\d+>}", value1 : Int32, value2 : Float64 do
"Value1: #{value1} - Value2: #{value2}"
end
end
patch
#
Helper DSL macro for creating PATCH
actions.
The first argument is the path that the action should handle; which maps to path on the HTTP method annotation.
The second argument is a variable amount of arguments with a syntax similar to Crystal's record
.
There are also a few optional named arguments that map to the corresponding field on the HTTP method annotation.
The macro simply defines a method based on the options passed to it. Additional annotations, such as for query params or a param converter can simply be added on top of the macro.
Optional Named Arguments#
return_type
- The return type to set for the action. Defaults toString
if not provided.constraints
- Any constraints that should be applied to the route.
Example#
class ExampleController < ATH::Controller
patch "values/{value1<\\d+>}/{value2<\\d+\\.\\d+>}", value1 : Int32, value2 : Float64 do
"Value1: #{value1} - Value2: #{value2}"
end
end
post
#
Helper DSL macro for creating POST
actions.
The first argument is the path that the action should handle; which maps to path on the HTTP method annotation.
The second argument is a variable amount of arguments with a syntax similar to Crystal's record
.
There are also a few optional named arguments that map to the corresponding field on the HTTP method annotation.
The macro simply defines a method based on the options passed to it. Additional annotations, such as for query params or a param converter can simply be added on top of the macro.
Optional Named Arguments#
return_type
- The return type to set for the action. Defaults toString
if not provided.constraints
- Any constraints that should be applied to the route.
Example#
class ExampleController < ATH::Controller
post "values/{value1<\\d+>}/{value2<\\d+\\.\\d+>}", value1 : Int32, value2 : Float64 do
"Value1: #{value1} - Value2: #{value2}"
end
end
put
#
Helper DSL macro for creating PUT
actions.
The first argument is the path that the action should handle; which maps to path on the HTTP method annotation.
The second argument is a variable amount of arguments with a syntax similar to Crystal's record
.
There are also a few optional named arguments that map to the corresponding field on the HTTP method annotation.
The macro simply defines a method based on the options passed to it. Additional annotations, such as for query params or a param converter can simply be added on top of the macro.
Optional Named Arguments#
return_type
- The return type to set for the action. Defaults toString
if not provided.constraints
- Any constraints that should be applied to the route.
Example#
class ExampleController < ATH::Controller
put "values/{value1<\\d+>}/{value2<\\d+\\.\\d+>}", value1 : Int32, value2 : Float64 do
"Value1: #{value1} - Value2: #{value2}"
end
end
render(template)
#
Renders a template.
Uses ECR
to render the template, creating an ATH::Response
with its rendered content and adding a text/html
content-type
header.
The response can be modified further before returning it if needed.
Variables used within the template must be defined within the action's body manually if they are not provided within the action's arguments.
# greeting.ecr
Greetings, <%= name %>!
# example_controller.cr
class ExampleController < ATH::Controller
@[ARTA::Get("/{name}")]
def greet(name : String) : ATH::Response
render "greeting.ecr"
end
end
ATH.run
# GET /Fred # => Greetings, Fred!
render(template, layout)
#
Renders a template within a layout.
# layout.ecr
<h1>Content:</h1> <%= content -%>
# greeting.ecr
Greetings, <%= name %>!
# example_controller.cr
class ExampleController < ATH::Controller
@[ARTA::Get("/{name}")]
def greet(name : String) : ATH::Response
render "greeting.ecr", "layout.ecr"
end
end
ATH.run
# GET /Fred # => <h1>Content:</h1> Greetings, Fred!
unlink
#
Helper DSL macro for creating UNLINK
actions.
The first argument is the path that the action should handle; which maps to path on the HTTP method annotation.
The second argument is a variable amount of arguments with a syntax similar to Crystal's record
.
There are also a few optional named arguments that map to the corresponding field on the HTTP method annotation.
The macro simply defines a method based on the options passed to it. Additional annotations, such as for query params or a param converter can simply be added on top of the macro.
Optional Named Arguments#
return_type
- The return type to set for the action. Defaults toString
if not provided.constraints
- Any constraints that should be applied to the route.
Example#
class ExampleController < ATH::Controller
unlink "values/{value1<\\d+>}/{value2<\\d+\\.\\d+>}", value1 : Int32, value2 : Float64 do
"Value1: #{value1} - Value2: #{value2}"
end
end