Athena 0.17.0

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::ConstraintValidators. 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 :wink:.

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.

11 Likes

Thanks for sharing! This inspired me to do some refactoring of Spider-Gazelle to support methods in the same way.

I also implemented parameter customizations which might help with your Time format issue.

2 Likes

Nice! It definitely works out well I’d say.

Current plan is to by default use RFC 3339 by default, but allow overriding it via annotation (assuming when my PR is merged/released). Something like:

class ExampleController < ATH::Controller
  @[ARTA::Get(path: "/event/{start_time}")]
  def event(@[ATHR::Time::Format("%F")] start_time : Time) : Time
    start_time
  end
end

or however the user wants to format the annotation. I.e. one parameter per line, annotations on top of the parameters, inline etc.

This way I don’t need to try and map configuration from annotations on the method to parameters, since you can just annotate the related parameter directly. Makes for a bit simpler implementation, and is more flexible. E.g. can use an annotation to enable a specific resolver or something.