Athena 0.9.0

Athena 0.9.0

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.

DI Overhaul

Athena 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.

In version 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 struct service behaves differently than a class service. 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.

The 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.

Also on Dev.to

6 Likes