Athena Framework 0.15.0

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 proper Routing component

The actual Routing component is currently in a prototype stage, but the preliminary results are looking pretty good :slight_smile::

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::Questions
    • 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!

6 Likes