Athena Framework 0.17.0
This release focused on catching up on pending changes after the introduction of the How I migrated Athena to a Monorepo...and you can too, and various new/QoL features.
Validator Features
One of the more noteworthy changes in this release is bumping the version of the Athena::Validator component bundled with the framework. This bump does come with some breaking changes, but shouldn’t affect most users unless you were defining custom AVD::ConstraintValidator
s. In addition to this, some new constraints were introduced:
These allow validating various common aspects such as size and type, but also image specific things such as width/height, pixels/ratio, and orientation. The latter of which makes use of the new Athena::ImageSize component that allows resolving image information from various image formats.
Improved Compiler Performance
Another breaking change involved making AVD::ExecutionContext
no longer a generic type. Having it as a generic caused greatly increased compile times due to the large amount of union types it could be, e.g. in the specs.
Benchmarks with cleared cache:
Before
$ time crystal spec --stats --progress --order=random
Parse: 00:00:00.000050488 ( 0.93MB)
Semantic (top level): 00:00:00.131115705 ( 72.07MB)
Semantic (new): 00:00:00.001074542 ( 72.07MB)
Semantic (type declarations): 00:00:00.014930494 ( 72.07MB)
Semantic (abstract def check): 00:00:00.012288425 ( 72.07MB)
Semantic (ivars initializers): 00:00:00.009849691 ( 80.07MB)
Semantic (cvars initializers): 00:00:00.112866592 ( 104.13MB)
Semantic (main): 00:00:05.211501488 (1004.00MB)
Semantic (cleanup): 00:00:00.000441593 (1004.00MB)
Semantic (recursive struct check): 00:00:00.002850060 (1004.00MB)
Codegen (crystal): 00:00:03.372504871 (1140.00MB)
Codegen (bc+obj): 00:00:05.393375077 (1140.00MB)
Codegen (linking): 00:00:01.255013004 (1140.00MB)
Codegen (bc+obj):
- no previous .o files were reused
<spec output removed for brevity>
real 0m16.424s
user 0m30.805s
sys 0m1.998s
After
$ time crystal spec --stats --progress --order=random
Parse: 00:00:00.000086493 ( 0.93MB)
Semantic (top level): 00:00:00.141055135 ( 72.07MB)
Semantic (new): 00:00:00.001126495 ( 72.07MB)
Semantic (type declarations): 00:00:00.022422536 ( 72.07MB)
Semantic (abstract def check): 00:00:00.005490472 ( 80.07MB)
Semantic (ivars initializers): 00:00:00.008942025 ( 80.07MB)
Semantic (cvars initializers): 00:00:00.115754305 ( 112.13MB)
Semantic (main): 00:00:02.346613108 ( 580.00MB)
Semantic (cleanup): 00:00:00.000379141 ( 580.00MB)
Semantic (recursive struct check): 00:00:00.001237565 ( 580.00MB)
Codegen (crystal): 00:00:01.407115005 ( 724.00MB)
Codegen (bc+obj): 00:00:02.102648726 ( 724.00MB)
Codegen (linking): 00:00:00.757712058 ( 724.00MB)
Codegen (bc+obj):
- no previous .o files were reused
<spec output removed for brevity>
real 0m7.834s
user 0m15.646s
sys 0m1.422s
Definitely quite the improvement! Just goes to show how big of an impact large unions can have on compile times.
Argument Resolvers
One of the coolest features of Athena is its ability to provide arguments directly to the controller action methods, without needing an extra params
hash. Ultimately this makes things more natural to read, document, and test. Internally this feature is handled by argument resolvers. This released added a few more resolvers to enable automagically providing Enum
, UUID
, and default values to the controller action.
require "athena"
enum Color
Red
Blue
Green
end
class ExampleController < ATH::Controller
@[ARTA::Get("/uuid/{uuid}")]
def get_uuid(uuid : UUID) : String
"Version: #{uuid.version} - Variant: #{uuid.variant}"
end
@[ARTA::Get("/string/{color}")]
def get_color(color : Color) : Color
color
end
@[ARTA::Get("/default")]
def default(id : Int32 = 123) : Int32
id
end
@[ARTA::Get("/nilable")]
def nilable(id : Int32?) : Int32?
id
end
end
ATH.run
# GET /uuid/b115c7a5-0a13-47b4-b4ac-55b3e2686946 # => "Version: V4 - Variant: RFC4122"
# GET /string/red # => "red"
# GET /default # => 123
# GET /nilable # => null
Pretty cool right? Custom resolvers may also be defined, but more on this in the next section.
Future of Param Converters
Currently argument resolvers are like the black sheep of how to customize how values are provided to controller actions. Even so, they are quite a bit more powerful/flexible than their ATH::ParamConverter counterpart. I came to realize that there is not really a need to support both, and a future release will replace existing param converter logic with (a probably revamped/polished) argument resolver implementation. Ultimately this will remove the need to add the @[ATHA::ParamConverter]
annotation at all in most cases.
The one blocking feature to support this is the ability to provide extra configuration to the resolver, such as the format to use when converting a datetime string into a Time
. Hopefully Allow annotations on method/macro parameters · Issue #12039 · crystal-lang/crystal · GitHub will be approved/merged in time for Crystal 1.5.0
in order to unblock this migration. If so, then annotation(s) could be applied to specific parameters, which could then be exposed on argument within the resolver via Athena’s custom annotation feature. They then could be either used to opt an argument in to a specific resolver, or provide extra configuration data to the resolver.
Routing Features
This release also expanded on the integration of the Athena::Routing component by augmenting it with some new features that pair nicely with the new argument resolvers. Specifically the ART::Requirement namespace was created to expose various Regex constants for common route requirements, such as UUIDs, digits, etc. For example, you can now do things like:
require "athena"
class ExampleController < ATH::Controller
@[ARTA::Get("/uuid/{uuid}", requirements: {"uuid" => ART::Requirement::UUID_V4})]
def get_uuid(uuid : UUID) : String
"Version: #{uuid.version} - Variant: #{uuid.variant}"
end
end
ATH.run
# GET /uuid/b115c7a5-0a13-47b4-b4ac-55b3e2686946 # => "Version: V4 - Variant: RFC4122"
# GET /uuid/foo # => 404
# GET /uuid/fd8ed8b2-d99c-3f36-83cb-02f88c5e00e0 # => 404 (is a V3 UUID)
The namespace is also the future home of helper types related to route requirements, with the first being ART::Requirement::Enum which makes it easier to define a requirement based on all, or a subset of, the members of an enum that handles member removals/additions. For example:
require "athena"
enum Color
Red
Blue
Green
Black
end
class ExampleController < ATH::Controller
@[ARTA::Get(
"/color/{color}",
requirements: {"color" => ART::Requirement::Enum(Color).new}
)]
def get_color(color : Color) : Color
color
end
@[ARTA::Get(
"/rgb-color/{color}",
requirements: {"color" => ART::Requirement::Enum(Color).new(:red, :green, :blue)}
)]
def get_rgb_color(color : Color) : Color
color
end
end
ATH.run
# GET /color/red # => "red"
# GET /color/pink # => 404
#
# GET /rgb-color/red # => "red"
# GET /rgb-color/green # => "green"
# GET /rgb-color/blue # => "blue"
# GET /rgb-color/black # => 404
Also remember Athena components can be used independently. So if you like what you see, consider integrating it into your project/framework .
Notable Mentions
- All components now have a common-changelog compliant
CHANGELOG.md
file - The minimum Crystal version is now
1.4.0
- The ASPEC::Methods.assert_error method was revamped to allow testing compile time errors without needing a file per test case
- Various bug fixes and doc improvements
Checkout the release notes for a complete list of changes. As usual feel free to join me in the Athena Discord server if you have any suggestions, questions, or ideas. I’m also available via Email.