Updates in the Athenaverse - Windows Support and Déjà vu

Updates in the Athenaverse

This release cycle in the Athenaverse is small but mighty, with a fairly small set of changes but will have a lasting impression on the future of the framework and ecosystem as a whole.

Highlights include the following, but read on for more specific details/examples!

  • Revamped website again
  • Windows support
  • A better way to configure framework features

Revamped Website (again)

As you may remember, the Athena website went though a revamp as part of the last release. However, documentation is such an important part of a project that it warranted additional attention. I’m pleased to announce some improvements that have been made, both through user feedback and my continued thought that should hopefully make it even easier to get started.

  • Website is no longer its own repo
    • The Athena website and the monorepo previously were separate repos, which made keeping them in-sync and up to date a bit more annoying than it had to be.
    • As of this release, the Athena website is now integrated into the monorepo itself. This is accomplished via leveraging the fairly new MkDocs Projects plugin which allows having independent MkDocs projects that can be included into a parent project. The two primary benefits of this include: 1) it being easier to include contextual docs into the API docs of each component (e.g. extra files option for crystal doc · Issue #13184 · crystal-lang/crystal · GitHub), and 2) The GitHub integration at the top right now properly is synced to each component’s repo vs always being the monorepo.
      • The first benefit could make it easier to include how-to guides or other component specific information in the future while keeping everything well integrated.
  • Better site navigation/discovery
    • One of the first things you may notice is that the tabs at the top are gone. The old tabs represented the API documentation for each component, with the first Manual tab representing the contextual documentation. This lead to quite a bit of confusion around how to best navigate the site.
    • The layout of the site was re-done in order to improve on that aspect. The API docs are still accessible via clicking the link on the side pane. The landing page now includes the list of available components, its shortcut alias if applicable, and a short description of what each component does. The names of which link to that component’s getting started documentation.
  • Fully redone getting started guide
    • The old getting started guide basically only covered the HTTP/routing based features, without touching on what else the framework/ecosystem has to offer. This forced users to try and navigate API docs to find information on simply how to get started, which wasn’t a great experience.
    • The new guide is broken up into more logical pages to make discoverability easier and in turns covers all the high level features you may need. This also had the benefit of allowing the API docs to stay more focused/general.
      • P.S. See something that isn’t covered that you think should be? Let me know!
  • Ability to view docs for unreleased features
    • One of my personal gripes with the old documentation site was there was no way to view docs for upcoming features. This made it quite challenging to both communicate these features, and to gain feedback on them. At least without the user building the docs themselves, or viewing it on GitHub.
    • A dev version of the docs are now available at https://dev.athenaframework.org. The dev version represents the latest changes in master, while the non-dev version represents the latest stable release of each component. It still isn’t possible to view documentation for past versions of a component, but given only the latest has active support, I think that is reasonable.

Windows Support

I’m also very pleased to announce that all of the Athena components now support (and have CI setup) for Windows! This would not be possible without the Crystal Core team’s continued efforts. Maybe consider contributing in order to show your thanks (and to keep the progress going) :sunglasses:.

While CI is all green and everything is working as best I can tell. I do not personally use Windows, but if you do and notice something that isn’t working as expected, please be sure to report it. Either in the Athena monorepo if it’s Athena related, or in the Crystal repo for Crystal issues.

Re-imagined Configuration (again)

Three years ago as part of Framework version 0.13.0, the way you configure Athena Framework applications changed dramatically, moving away from YAML to in-code based approach. But this approach was not without its issues either:

  • Requires a lot of boilerplate
    • Need to define quite a lot of types in order to set things, most of which are just there to define the naming of the values
  • Requires monkey patching as a way to make Athena aware of the custom configuration/parameters
  • The service container isn’t aware of the values themselves
    • Unable to use the values to configure the container itself

As with any software project, iteration is the way to greatness. And of course we couldn’t just leave configuration alone. I’m excited to talk about the new-new way to handle configuration.

For those of you that may be interested in the nitty-gritty details, checkout the related GitHub issue. Otherwise, for the rest of us here is the gist of it. All configuration now focuses on the ATH.configure macro. This macro accepts a strictly typed/structured named tuple literal in order to provide the configuration (and parameters) that should be used. For example, to enable/configure the CORS listener:

ATH.configure({
  framework: {
    cors: {
      enabled:  true,
      defaults: {
        allow_credentials: true,
        allow_origin: ["https://app.example.com"],
        expose_headers: ["X-Transaction-ID X-Some-Custom-Header"],
      },
    },
  },
})

One of the major benefits of this approach is that all the configuration is processed/available at compile time. This allows doing some really neat things. Say for example you do not want/need CORS, if you explicitly disable it (the default), then that listener class is not included in the resulting binary at all. So you not only get a bit extra performance from it not running, but also makes the binary size a bit smaller. Longer term, this could be expanded to more of the framework’s integration with the various components. Similarly, the configuration values themselves are provided directly, so there is no runtime overhead of reading/validating configuration.

Schemas

The properties/features able to be configured are defined by a schema. At the moment the only bundle schema is framework, which is denoted via the top level framework key in the named tuple literal provided to ATH.configure. The remaining keys then map to the downcase, snake cased name of the modules under the ATH::Bundle::Schema in the Framework API documentation. For example, the docs for our CORS listener can be found at Cors - Framework, and the child Default module.

Checkout Next Level Documentation for how I managed to implement the unique docs for the schema types!

But wait! If we’re just configuring things via a macro, what about type safety/value validation? Through the magic of macros, I’m very proud to say you do not lose any of that by using this approach, as compared to the previous implementation.

A compile time error will be raised if the values are invalid according to its schema. For example:

 10 | allow_credentials: 10,
                          ^
Error: Expected configuration value 'framework.cors.defaults.allow_credentials' to be a 'Bool', but got 'Int32'.

This also works for nested values:

 10 | allow_origin:      [10, "https://app.example.com"] of String,
                           ^
Error: Expected configuration value 'framework.cors.defaults.allow_origin[0]' to be a 'String', but got 'Int32'.

Or if the schema defines a value that is not nilable nor has a default:

 10 | property some_property : String
               ^------------
Error: Required configuration property 'framework.some_property : String' must be provided.

It can also call out unexpected keys:

 10 | foo:      "bar",
                 ^
Error: Encountered unexpected property 'framework.cors.foo' with value '"bar"'.

It is also is able to handle strictly typed/validated complex configuration types, such as those used for the Format Listener’s rules:

ATH.configure({
  framework: {
    format_listener: {
      enabled: true,
      rules:   [
        {priorities: ["json", "xml"], host: "api.example.com", fallback_format: "json"},
        {path: /^\/image/, priorities: ["jpeg", "gif"], fallback_format: false},
        {path: /^\/admin/, priorities: ["xml", "html"]},
        {priorities: ["text/html", "*/*"], fallback_format: "html"},
      ],
    },
  },
})

Parameters

Sometimes the same configuration value is used in several places within ATH.configure. Instead of repeating it, you can define it as a “parameter”, which represents reusable configuration values. Parameters are intended for values that do not change between machines, and control the application’s behavior, e.g. the sender of notification emails, what features are enabled, or other high level application level values.

Parameters should NOT be used for values that rarely change, such as the max amount of items to return per page. These types of values are better suited to being a constant within the related type. Similarly, infrastructure related values that change from one machine to another, e.g. development machine to production server, should be defined using environmental variables.

Parameters can be defined using the special top level parameters key within ATH.configure.

ATH.configure({
  parameters: {
    "app.admin_email": "admin@example.com",
    "app.enable_v2_protocol": true,
    "app.supported_locales": ["en", "es", "de"],
  },
})

The parameter value may be any primitive type, including strings, bools, hashes, arrays, etc. From here they can be used when configuring a bundle via enclosing the name of the parameter within %. For example:

ATH.configure({
  some_bundle: {
    email: "%app.admin_email%",
  },
})

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.

9 Likes

This is great, keep up the great work! Thanks a lot for all the reusable stuff :heart:

2 Likes