Skip to content

Instantly share code, notes, and snippets.

@jnthn

jnthn/article.md Secret

Created December 18, 2021 14:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnthn/2fe224cd4998c6c4baf6e55f92dd1fc6 to your computer and use it in GitHub Desktop.
Save jnthn/2fe224cd4998c6c4baf6e55f92dd1fc6 to your computer and use it in GitHub Desktop.

Let it Cro

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?

Dashing through the Cro

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 Blobs, 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.)

Oh I wish it could TCP_NODELAY

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.

I saw mummy serving a resource

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.

Last Christmas I gave you Cro parts

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.

While shepherds watched their docs by night

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.

The proxy and the IP

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!

God REST ye merry gentlemen

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment