Athena Framework 0.15.0
This release focused on polishing existing features, preparing the Athena ecosystem for the future. With highlights including:
- Improved param converters
- Namespace renaming
- Console component
Improved Param Converters
ATH::ParamConverters are a unique and powerful feature of Athena Framework that has been made even more powerful with this release. A common use case for param converters is deserializing a request body into an object that can be provided to your controller action. However, in the past, such a feature needed to be manually defined within each application’s code. This is no longer the case and a ATH::RequestBodyConverter now comes built into the framework!
The implementation has also been made much more robust. You no longer need to manually specify the type to deserialize into as it is inferred from the parameter’s type restriction. Additionally, the built in converter handles both ASR::Serializable and JSON::Serializable types as well as removing the need to include ASR::Model
in some contexts. A compile time error is raised if you try to use with a type that is not serializable.
Before
# Define a base type so that it can be stored in an ivar.
abstract struct DTO
include ASR::Model
end
struct UserCreate < DTO
include AVD::Validatable
include ASR::Serializable # CANNOT be JSON::Serializable
# Getters with validation constraints for the user's name, email, etc.
end
@[ADI::Register]
struct RequestBody < ART::ParamConverterInterface
# A bunch of code from the cookbook.
end
class UserController < ART::Controller
@[ARTA::Post("/user")]
@[ARTA::ParamConverter("user_create", converter: RequestBody, type: UserCreate)]
def new_user(user_create : UserCreate) : UserCreate
user_create
end
end
After
struct UserCreate
include AVD::Validatable
include ASR::Serializable # OR JSON::Serializable
# Getters with validation constraints for the user's name, email, etc.
end
class UserController < ATH::Controller
@[ATHA::Post("/user")]
@[ATHA::ParamConverter("user", converter: ATH::RequestBodyConverter)]
def new_user(user : User) : User
user
end
end
Behind the scenes, each param converter uses generics to know the type of the related action parameter. This can be used to provide compile time type safety. I.e. if the converter only can handle parameters of a specific type:
Type Safe Converters
@[ADI::Register]
class MultiplyConverter < ATH::ParamConverter
configuration multiplier : Int32
# :inherit:
def apply(request : ATH::Request, configuration : Configuration(Int32)) : Nil
# Multiply and re-set the attribute by some multiplier defined in the configuration
end
# :inherit:
def apply(request : ATH::Request, configuration : Configuration(T)) : Nil forall T
{% T.raise "MultiplyConverter does not support arguments of type '#{T}'." %}
end
end
In this example the second argument of the first #apply
method is set to an Int32
generic argument. This will restrict that method to only handle instances where the param converter was applied to an argument of type Int32
. The second overload handles all other types as it is a free variable. This overload will raise a compile time error if the converter is applied to an argument that is NOT an Int32
. Ultimately, this makes the converter compile time safe.
See the API documentation for more information/examples.
Namespace changes
Another major change of this release is renaming of the namespace the framework lives in. The main reason for this is that previously the framework code was living in the Routing
namespace. This would conflict with the future Routing
component. It also didn’t make sense to have framework level code living in that namespace anyway, given the majority of it does not have anything to do with routing persay.
There are no functional changes, so find & replace all should be sufficient to upgrade:
ARTA
=>ATHA
ART
=>ATH
Athena::Routing
=>Athena::Framework
NOTE: Routing/URL generation types still use
Athena::Routing
/ART
names as they will eventually be moved into the properRouting
component
The actual Routing
component is currently in a prototype stage, but the preliminary results are looking pretty good :
Prototype Router Benchmark
/get
retour 3.06M (326.45ns) (± 1.73%) 576B/op 7.25× slower
amber 3.48M (287.22ns) (± 0.72%) 386B/op 6.38× slower
radix 17.72M ( 56.42ns) (± 0.87%) 112B/op 1.25× slower
Athena 22.21M ( 45.02ns) (± 0.87%) 64.0B/op fastest
/get/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z
retour 1.38M (722.22ns) (± 0.68%) 576B/op 15.30× slower
amber 353.68k ( 2.83µs) (± 4.54%) 4.03kB/op 59.90× slower
radix 2.28M (438.36ns) (± 0.85%) 273B/op 9.29× slower
Athena 21.18M ( 47.20ns) (± 3.68%) 64.0B/op fastest
/get/books/23/chapters
retour 1.13M (886.38ns) (± 2.78%) 944B/op 6.20× slower
amber 1.06M (946.37ns) (± 3.46%) 1.23kB/op 6.62× slower
radix 1.70M (587.39ns) (± 1.51%) 432B/op 4.11× slower
Athena 6.99M (143.04ns) (± 2.15%) 97.0B/op fastest
Console Component
In other news from the Athena Ecosystem, I’m elated to announce initial release of the Athena::Console component! It allows the creation of CLI based commands, with plenty of abstractions, extension points, features, and helpers to make that process easier. Both to create and to maintain.
Example Command
class AddCommand < ACON::Command
@@default_name = "add"
@@default_description = "Sums two numbers, optionally making making the sum negative"
protected def configure : Nil
self
.argument("value1", :required, "The first value")
.argument("value2", :required, "The second value")
.option("negative", description: "If the sum should be made negative")
end
protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status
sum = input.argument("value1", Int32) + input.argument("value2", Int32)
sum = -sum if input.option "negative", Bool
output.puts "The sum of the values is: #{sum}"
ACON::Command::Status::SUCCESS
end
end
While there is too much to cover in this thread, a few highlights include:
- Robust input parsing, that includes:
- Type safe conversion logic of input values to their desired type
- Optional, required, and array arguments
- no-value, optional/required value, negatable, and array options
- Both short and long flags with stacked short flag option support
- Independently managed output sections
- Asking
ACON::Question
s- Such as confirmation, single answer, and (multiple)choice
- Output styling, with custom style support
output.puts "<error>foo</error>"
- High level reusable formatting styles
- Such as for printing blocks, titles, lists, etc.
- Testing abstractions
Example Test
require "spec"
require "athena-spec"
describe AddCommand do
describe "#execute" do
it "without negative option" do
tester = ACON::Spec::CommandTester.new AddCommand.new
tester.execute value1: 10, value2: 7
tester.display.should eq "The sum of the values is: 17\n"
end
it "with negative option" do
tester = ACON::Spec::CommandTester.new AddCommand.new
tester.execute value1: -10, value2: 5, "--negative": nil
tester.display.should eq "The sum of the values is: 5\n"
end
end
end
The component is already quite feature rich, but there is still more to do, so keep an eye out for new versions that could contain things like:
- Progress bars
- Tables
- Eventing
- Signal aware commands
- And much more!
Checkout the release notes for a complete list of changes. 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.
P.S. Athena now has a Discord server!