Ah, advent. That time of year when the shops are filled with the sound of Christmas songs - largely, the very same ones they played when I was a kid. They're a bit corny, but the familiarity is somehow reassuring. And what better way to learn about this year's new Cro features than through the words of the Christmas hits?
The Cro HTTP client scores decently well on flexibility. The asynchronous API
fits neatly with Raku's concurrency features, and from the start it's been
possible to get the headers as soon as they arrive, then choose how to get
the body - including obtaining it as a Supply
of Blob
s, which is ideal for
dealing with large downloads.
Which is all well and good, but a lot of the time, we just want to get the request body, automatically deserialized according to the content type. This used to be a bit tedious, at least by Raku standards:
my $response = await Cro::HTTP::Client.get:
'https://www.youtube.com/watch?v=8ZUOYO9qljs';
my $body = await $response.body;
Now there's a get-body
method, shortening it to:
my $response = await Cro::HTTP::Client.get-body:
'https://www.youtube.com/watch?v=8ZUOYO9qljs';
There's post-body
, put-body
and friends to go with it too. (Is there a
head-body
? Of course not, silly. Ease off the eggnog.)
Cro offers improved latency out of the box for thanks to setting TCP_NODELAY
automatically on sockets. This disables Nagle's algorithm,
which reduces the network traffic of applications that do many small writes by
collecting multiple writes together to send at once. This makes sense in some
situations, but less so in the typical web application, where the resulting
increased latency of HTTP responses or WebSocket messages can make a web
application feel a little less responsive.
Cro has long made it easy to serve up static files on disk:
my $app = route {
get -> {
static 'content/index.html';
}
}
Which is fine in many situations, but not so convenient for those who would
like to distribute their applications in the module ecosystem, or to have them
installable with tools like zef
. In these situations, one should supply static
content as resources.
Serving those with Cro was, alas, less convenient than serving files on disk.
Thankfully, that's no longer the case. Within a route
block, we first need
to associate it with the distribution resources using resources-from
. Then
it is possible to use resource
in the very same way as static
.
my $app = route {
resources-from %?RESOURCES;
# It works with exact resources
get -> {
resource 'content/index.html';
}
# And also with path segments
get -> 'css', *@path {
resource 'content/css', @path;
}
}
We've also made it possible to serve templates from resources; simply call
templates-from-resources
, and then use template
as usual. See
the documentation
for details.
And the very next day, you made a template. And oh, was it more convenient
than before. In many applications pages have common elements: an indication of
what is in the shopping cart, or the username of the currently logged in user.
Previously, Cro left you to pass this into every template. Typically one would
write sub kind of sub
to envelope the main content and include the other
data:
sub shop($db) {
my $app = route {
sub env($session, $body) {
%( :user($session.user), :basket($session.basket), :$body )
}
get -> MySession $session, 'product', $id {
with $db.get-product($id) -> $product {
template 'product.crotmp', env($session, $product);
}
else {
not-found;
}
}
}
}
This works, but gets a bit tedious - not only here, but also inside of the template, where the envelope has to be unpacked, values passed into the layout, and so forth.
<:use 'layout.crotmp'>
<|layout(.body, .basket)>
<h1><.name></h1>
</|>
Template parts improve the situation. Inside of the route
block, we use
template-part
to provide the data for a particular "part". This can, like a
route handler, optionally receive the session or user object.
sub shop($db) {
my $app = route {
template-part 'basket', -> MySession $session {
$session.basket
}
template-part 'user', -> MySession $session {
$session.user
}
get -> MySession $session, 'product', $id {
with $db.get-product($id) -> $product {
template 'product.crotmp', $product;
}
else {
not-found;
}
}
}
}
Page templates are now simpler, since they don't have to pass along the content for common page elements:
<:use 'layout.crotmp'>
<|layout>
<h1><.name></h1>
</|>
Meanwhile, in the layout, we can obtain the part data and use it:
<:macro layout>
<html>
<body>
<header>
...
<div class="basket">
<:part basket($basket)>
<?$basket.items>
<$basket.items> items worth <$basket.value> EUR
</?>
</:>
</div>
</header>
...
</body>
</html>
</:>
The special MAIN
part can be used to obtain the top-level object passed to
the template, which provides an alternative to the topic approach. One can
provide multiple arguments for the MAIN
part (or any other part) by using
a Capture
literal:
sub shop($db) {
my $app = route {
get -> MySession $session {
my $categories = $db.get-categories;
my $offers = $db.get-offers;
template 'shop-start.crotmp', \($categories, $offers);
}
}
}
The template would then look like this:
<:part MAIN($categories, $offers)>
...
</:>
The Comma IDE already knows about this new feature,
and allows navigation between the part usage in a template and the part data
provider in the route
block.
A further annoyance when working with Cro templates was the caching of their
compilation. Which this is a fantastic optimization when the application is
running in product - the template does not have to be parsed each time - it
meant that one had to restart the application to test out template changes.
While the cro
development tool would automate the restarts, it was still a
slower development experience than would have been ideal.
Now, setting CRO_DEV=1
in the environment will invalidate the compilation
of templates that change, meaning that changes to templates will be
available without a restart.
In many situations we might wish for your application to handle a HTTP request by forwarding it to another HTTP server, possibly tweaking the headers and/or body in one or both directions. For example, we might have several services that are internal to our application, and wish to expose them through a single facade that also does things like rate limiting.
Suppose we have a payments
service and a bookings
service, and wish to
expose them in a single facade service. We could do it like this:
my $app = route {
# /payments/foo proxied to https://payments-service/foo
delegate <payments *> => Cro::HTTP::ReverseProxy.new:
to => 'https://payments-service/';
# /bookings/foo proxied to https://bookings-service/foo
delegate <bookings *> => Cro::HTTP::ReverseProxy.new:
to => 'https://bookings-service/';
}
This is just scratching the surface of the many features
of Cro::HTTP::ReverseProxy
. Have fun!
Whether you're building REST services, HTTP APIs, server-side web applications, reactive web applications using WebSockets, or something else, we hope this year's Cro improvements will make your Raku development merry and bright.