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
Also on Dev.to