Updates in the Athenaverse
The major highlight of this release is the release of the Mercure component and the related MercureBundle! The latter of which shows off initial support for third-party bundles. This is a well needed feature that brings real-time updates into the Athenaverse.
Highlights:
- Initial release of
mercureandmercure-bundle! Also shows off initial support for third-party bundle support httpandhttp-kernelbroken out as standalone components. The latter can be used as the base of your own event based framework- Macro code coverage reporting in the
speccomponent AVD::Constraints::Sizesplits into dedicatedCountandLengthconstraints
Mercure Component
A fairly common use case is wanting a client, either a web browser or mobile phone, to receive updates about something in real-time. The Crystal stdlib comes with WebSocket support which is usually how this is handled. However, they do not fit well into the architecture of Athena. To solve this, the new Mercure component allows using Mercure; a modern replacement for the WebSocket API that provides a way to send uni-directional real-time updates via SSE. It does require running a dedicated service to manage the connections, but works out quite well as it provides a lot of flexibility. Mainly since the service managing the connections is separate from the API/web service itself. E.g. it would be possible to send updates directly from a console command, or async worker, etc.
Here’s a basic example of how this would look, assuming there is a running Mercure Hub:
require "athena-mercure"
token_factory = AMC::TokenFactory::JWT.new ENV["MERCURE_JWT_SECRET"]
# Use `*` to give the created JWT access to all topics.
token_provider = AMC::TokenProvider::Factory.new token_factory, publish: ["*"]
hub = AMC::Hub.new ENV["MERCURE_URL"], token_provider, token_factory
update = AMC::Update.new(
"https://example.com/my-topic",
{message: "Hello world @ #{Time.local}!"}.to_json
)
hub.publish update # => urn:uuid:e1ee88e2-532a-4d6f-ba70-f0f8bd584022
See the API Docs for more information.
Mercure Bundle
In the Athena ecosystem, a Bundle provides a mechanism by which external code can be integrated into the framework. In short it handles wiring up types as services, and provides a way to configure how that code works. The framework itself is implemented as a bundle, even if this one is kept somewhat internal.
Up until now, all Athena components have been third-party dependency free. Meaning they either have no dependencies, or only depend on other components. The Mercure component is a bit different however in that it depends upon the de-facto standard JWT shard for handling JWT auth within the Mercure protocol; this felt like the proper way forward versus rolling my own. Additionally, in the past all new components were directly integrated within the framework out of the box. This battery included approach had a nice DX, but also some drawbacks.
For both of these reasons, I thought it would be a perfect use-case for an Athena maintained bundle to handle the integration of the Mercure component into the framework, versus having it installed by default. This not only keeps the extra dependencies optional unless you want the feature, but also reduces the amount of code that needs to be compiled.
The bundle works like any other shard, add it to your shard.yml, shards install, then require "athena-mercure_bundle". From here you can set it up via the same ATH.configure command you may already be familiar with:
ATH.configure({
# May also have framework specific customizations
framework: {
# ...
},
# Mercure bundle specific configuration
mercure: {
hubs: {
default: {
url: ENV["MERCURE_URL"],
jwt: {
secret: ENV["MERCURE_JWT_SECRET"],
publish: ["*"],
subscribe: ["*"],
},
},
},
},
})
Behind the scenes this handles wiring everything up such that all you have to do in inject a Hub instance and can get right to publishing updates:
@[ADI::Register]
class BroadcastController < ATH::Controller
def initialize(@hub : AMC::Hub::Interface); end
@[ARTA::Post("/broadcast")]
def broadcast : Nil
@hub.publish AMC::Update.new(
"https://example.com/books/1",
{status: "OutOfStock"}.to_json
)
end
end
Overall I’m quite pleased with how it turned out. In theory it may also be possible for third-party frameworks/shards to leverage the bundle concept in their own code as it provides a very flexible, and powerful, compile time validated/type safe way to configure things. This would require more exploration of use cases tho. Feel free to reach out if you’re interested in exploring this more! In the meantime, I’m planning on seeing how the current state of things works out, and assuming all goes well, write up some better documentation on how third-party bundles can be created. This would be a major win as now the community itself could get involved with integrating their favorite shards into the ecosystem.
Component Extractions
Getting to this point however, took quite a lot of refactoring internally. The main gotcha was if someone wants to create their own bundle and define an event listener for example, they shouldn’t have to require the whole framework just to have access to that one type. They should be able to require only the components they need. To handle this, some of the Athena internals were extracted into their own shards.
http- A dedicated home for HTTP-specific types (request, response, proxy headers, etc.)http-kernel- Exposes the core request/response handling logic
Other shards could use these as the basis of their own framework for example. The evolution of this pattern is going to be exploring the idea of making almost every component optional. E.g. if you want routing, you install/require that component. The same goes for console commands, validation, serialization, etc. This would make the framework much slimmer, and thus faster to compile since there is less code involved. How exactly all of this works out is TBD as it’s still in the experimental phase, but feel free to reach out if you have thoughts/ideas on how this should work.
This is of course a breaking change, but with a simple find/replace migration path. See UPGRADE.md for more detailed instructions.
Macro Code Coverage
Crystal 1.17 introduced the macro_code_coverage tool that generates a code coverage report based on a program’s macro usage. To accompany this, the Spec component is now able to generate macro code coverage reports based on the code executed via ASPEC::Methods.assert_compiles and ASPEC::Methods.assert_compile_time_error. Simply set the ATHENA_SPEC_COVERAGE_OUTPUT_DIR ENV var, and it’ll write the reports to that directory in the format of macro_coverage.${spec_file_name}#L${line_in_the_spec_file}.codecov.json, which could then be uploaded to Codecov for use in coverage reporting.
A few new helpers landed alongside this:
ASPEC.compile_time_assert— richer assertions from inside anassert_compilesblockassert_compiles/assert_compile_time_errorcan now execute code before and after the code under test, giving more flexibility in how compile-time scenarios are set up
Size Constraint → Count + Length
The old AVD::Constraints::Size tried to do two different jobs: validate how many elements a collection has and validate how long a string is. In practice this caused confusion, especially when the error messages and semantics really needed to differ between the two.
As of this release, Size is split into two dedicated constraints:
AVD::Constraints::Count— validates the number of elements in a collectionAVD::Constraints::Length— validates the length of a string, with an optional unit (e.g. characters vs. bytes)
# Validate the number of items in a collection
@[Assert::Count(1..10)]
property tags : Array(String)
# Validate string length (characters by default)
@[Assert::Length(8..128)]
property password : String
This is of course is also a breaking change, anywhere you were previously using Size, you’ll need to move to whichever of the two fits.
Smaller Items
- Clock:
ACLK::Monotonichas been removed, due to it being deprecated upstream - Console: Opt-in support for deriving a command’s name from
PROGRAM_NAME. Useful when a CLI binary is invoked via a symlink and you want the command name to match the symlink’s name rather than the underlying binary’s. - Compile Time Error Messages: A pass was completed on compile time error messages raised from various components to improve the clarity and location of the messages
That’s it for this update! As usual, feel free to join me in the Athena Discord server if you have any suggestions, questions, ideas, or just want to chat!