With Crystal 0.35.0 finally released, I'm happy to also announce the release of Athena 0.9.0!
This release focused on further refining the overall foundation of the framework. This includes:
- A new Getting Started section
- An overhaul of the DI framework
- A refactor of how controller action arguments are resolved
- A refactor of how param converters work
- Compile time route collision detection
- Some additional minor QoL improvements
See the linked types for more details/examples.
0.9.0 comes bundled with Athena Dependency Injection 0.2.0, which overhauls the implementation of the service container; making the usage of it simpler, more feature rich, easier to maintain, and more robust.
0.1.x, services were required to include the
ADI::Service module, as well as specify any service dependencies within the ADI::Register annotation as position arguments. This is no longer required as dependencies are now resolved automatically based on type restrictions. The module is also no longer required.
NOTE: I'm using records for brevity, a
structservice behaves differently than a
classservice. Be sure to pick the right one for your use case. See the docs for more details.
# Before @[ADI::Register] record ServiceOne do include ADI::Service end @[ADI::Register("@my_service", true)] record ServiceTwo, service : ServiceOne, debug : Bool do include ADI::Service end # After @[ADI::Register] record ServiceOne @[ADI::Register(_debug: true)] record ServiceTwo, service : ServiceOne, debug : Bool
By default, only services can be auto resolved, however ADI.bind can be use to allow auto resolving Scalar Arguments and Tagged Services. Service Aliases can be used to define a "default" service for a given interface. Service dependencies can also be declared as optional or even based on Generic Services.
Action Handling Refactor
The logic that resolves a specific action argument has been decoupled from the logic that resolves the arguments for an action. This is mainly a behind the scenes change, but does come with some user facing changes. The main one being the request's attributes. Prior versions of Athena exposed a
Hash on the
HTTP::Request object that could be used to store arbitrary data specific to the request's life-cycle. This has been replaced with ART::ParameterBag; the concept is the same, but the API is better and is more robust.
ART::ParameterBag is also where the path/query parameters are stored. In fact, any value stored within it is able to be automatically provided to the controller action. This is accomplished via ART::Arguments::Resolvers::RequestAttribute which looks for a value in the bag with the same name as the controller argument. Custom resolves can also be defined by creating an implementation of ART::Arguments::Resolvers::ArgumentValueResolverInterface.
Param Converter Refactor
ART::ParamConverterInterfaces have undergone a rewrite. The API has changed to no longer return the converted value. It should instead, most commonly, be stored in the request's attributes. Param converters are now also services themselves; this allows using DI to supply any require dependencies needed for the conversion process. The concept of ART::ParamConverterInterface::ConfigurationInterface has also been introduced to allow defining extra configuration data that should be read from the ART::ParamConverter annotation.
require "athena" @[ADI::Register] struct MultiplyConverter < ART::ParamConverterInterface configuration by : Int32 # :inherit: def apply(request : HTTP::Request, configuration : Configuration) : Nil arg_name = configuration.name return unless request.attributes.has? arg_name value = request.attributes.get arg_name, Int32 request.attributes.set arg_name, value * configuration.by, Int32 end end class ParamConverterController < ART::Controller @[ART::Get(path: "/multiply/:num")] @[ART::ParamConverter("num", converter: MultiplyConverter, by: 4)] def multiply(num : Int32) : Int32 num end end ART.run # GET /multiply/3 # => 12
A built in ART::TimeConverter that converts a date(time) string into a
Time instance has also been added.
Route Collision Detection
Athena will now raise a compile time error if two routes share the same path; either statically, or with path arguments in the same locations.
class TestController < ART::Controller @[ART::Get(path: "some/path/:id")] def action1(id : Int64) : Int64 id end end class OtherController < ART::Controller @[ART::Get(path: "some/path/:id")] def action2(id : Int64) : Int64 id end end ART.run # #=> Route action OtherController#action2's path "/some/path/:id" conflicts with TestController#action1's path "/some/path/:id".
Other Minor Improvements
- Introduced the concept of ART::Response::Writer to control how the response content is written to the response IO
- Allow ART::Response to write directly to the response IO
- Add some additional ART::Exceptions types, and make defining custom types easier
As usual feel free to join me in the Athena Gitter channel if you have any suggestions, questions, or ideas. I'm also available on Discord (
Blacksmoke16#0016) or via Email.
Top comments (0)