Skip to main content

XKCD Image Generator

info

Please make sure that you've completed and are comfortable with the quickstart before moving on to any of these guides.

This guide will walk you through building and deploying a random XCKD comic selector and generator in your Cosmonic constellation.

About the Application

The XKCD image generator application is a simple HTTP application that randomly produces random XKCD comics on demand. It illustrates how to build a fully functioning application with a single actor and a few capability providers.

Architecture and Design

XKCD Image Generator Design

This application consists of a single actor, the XKCD image generator. It is bound to the HTTP server capability provider, the HTTP client provider, and it utilizes the wasmCloud builtin random number generation capability. It is designed to be opened in a browser, where the user will be treated to a randomly selected image.

Implementation Walkthrough

Prerequisites

  1. wash from the wasmCloud installation guide, at least version 0.12.0
  2. A Rust toolchain from the rust-lang website
  3. The wasm32-unknown-unknown target, by running rustup target add wasm32-unknown-unknown after installing Rust

Implementation

To start, we'll use wash to generate a new actor project from the hello world project template. This actor already has the HTTP Server handler setup for us, making it a quick start:

wash new actor xkcdgenerator --template-name hello

Using your favorite editor, go ahead and open up the xkcdgenerator project. To start, your src/lib.rs should look something like this

use wasmbus_rpc::actor::prelude::*;
use wasmcloud_interface_httpserver::{HttpRequest, HttpResponse};

#[derive(Debug, Default, Actor, HealthResponder)]
#[services(Actor, HttpServer)]
struct XkcdgeneratorActor {}

/// Implementation of HttpServer trait methods
#[async_trait]
impl HttpServer for XkcdgeneratorActor {
/// Returns a greeting, "Hello World", in the response body.
/// If the request contains a query parameter 'name=NAME', the
/// response is changed to "Hello NAME"
async fn handle_request(&self, _ctx: &Context, req: &HttpRequest) -> RpcResult<HttpResponse> {
let text = form_urlencoded::parse(req.query_string.as_bytes())
.find(|(n, _)| n == "name")
.map(|(_, v)| v.to_string())
.unwrap_or_else(|| "World".to_string());

Ok(HttpResponse {
body: format!("Hello {}", text).as_bytes().to_vec(),
..Default::default()
})
}
}

Of course, this is just a Hello World example for now, but this scaffolding already has our actor ready to receive HTTP requests and return HTTP responses.

The next step is to bring in the interface libraries for the capabilities we plan to make use of. Open up Cargo.toml, and in the dependencies section let's add the interfaces for httpclient and builtin-numbergen. Go ahead and import serde and serde_json, two serialization libraries that we'll make use of later:

[dependencies]
futures = "0.3"
form_urlencoded = "1.0"
serde = "1.0.145"
serde_json = "1.0.85"
wasmbus-rpc = "0.10"
wasmcloud-interface-httpserver = "0.7"
wasmcloud-interface-httpclient = "0.7"
wasmcloud-interface-numbergen = "0.7"

We'll use these interfaces to build our application in terms of contracts, not specific implementations. We'll get to choose the specific HTTP server and HTTP client later on.

Now, inside of src/lib, go ahead and update your imports section at the top to bring in the structures from your new interfaces. We'll import the HttpClient trait and HttpClientSender struct, as well as the random_in_range function for generating a random number in an inclusive range.

use wasmbus_rpc::actor::prelude::*;
use wasmcloud_interface_httpclient::{
HttpClient, HttpClientSender, HttpRequest as HttpClientRequest,
};
use wasmcloud_interface_httpserver::{HttpRequest, HttpResponse, HttpServer, HttpServerReceiver};
use wasmcloud_interface_numbergen::random_in_range;

Inside of the handle_request function, we can start writing our functional logic. Our goal is to receive HTTP requests and return HTTP responses that can display an XKCD comic in a web browser. Luckily, XCKD provides a handy API for fetching information about comics, and we can simply request an individual comic's information to get its title and image URL.

To start, generate a random number between 1 (the first XKCD comic) and 2680 (the most recent comic when this tutorial was written):

// Generate a comic number, between the first and most recent comic
let random_num = random_in_range(1, 2680).await?;

Then, we can use that number to form the XKCD URL where metadata is stored as JSON and make an HTTP request to fetch that data.

// Create request URL where XKCD stores JSON metadata about comics
let xkcd_url = format!("https://xkcd.com/{}/info.0.json", random_num);
let response = HttpClientSender::new()
.request(ctx, &HttpClientRequest::get(&xkcd_url))
.await?;

Now we need to parse this response body as JSON and pull out both the title and the img fields. The full metadata output contains more fields, but they aren't relevant to our application and we can ignore them. For example, fetching information about comic 2680 from the url https://xkcd.com/2680/info.0.json gives us:

{
"month": "10",
"num": 2680,
"link": "",
"year": "2022",
"news": "",
"safe_title": "Battery Life",
"transcript": "",
"alt": "It's okay, I'm at 10%, so I'm good for another month or two.",
"img": "https://imgs.xkcd.com/comics/battery_life.png",
"title": "Battery Life",
"day": "3"
}

In order to deserialize this JSON response, we'll use the aformentioned serde and serde_json libraries. At the end of your file, after the closing bracket from impl HttpServer for XkcdgeneratorActor, add the following struct definition:

#[derive(serde::Deserialize)]
// XKCD comic metadata fields
struct XkcdComic {
title: String,
img: String,
}

Then, we can go back and deserialize the response body after we make our HTTP request:

// Deserialize JSON to retrieve comic title and img URL
let comic: XkcdComic = serde_json::from_slice(&response.body).map_err(|e| {
RpcError::ActorHandler(format!("Failed to deserialize comic request: {}", e))
})?;

Lastly, we can remove any leftover logic from the Hello World template and replace it with an HTML response body, including the comic title and image URL embedded in the page:

// Format HTTP response body as an HTML string
let body = format!(
r#"
<!DOCTYPE html>
<html>
<head>
<title>Your XKCD random comic</title>
</head>
<body>
<h1>{}</h1>
<img src="{}"/>
</body>
</html>
"#,
comic.title, comic.img
);

Ok(HttpResponse {
body: body.as_bytes().to_vec(),
..Default::default()
})

The Final Result

With that, your full src/lib.rs file should look like the following:

use wasmbus_rpc::actor::prelude::*;
use wasmcloud_interface_httpclient::{
HttpClient, HttpClientSender, HttpRequest as HttpClientRequest,
};
use wasmcloud_interface_httpserver::{HttpRequest, HttpResponse, HttpServer, HttpServerReceiver};
use wasmcloud_interface_numbergen::random_in_range;

#[derive(Debug, Default, Actor, HealthResponder)]
#[services(Actor, HttpServer)]
struct XkcdgeneratorActor {}

/// Implementation of HttpServer trait methods
#[async_trait]
impl HttpServer for XkcdgeneratorActor {
async fn handle_request(&self, ctx: &Context, _req: &HttpRequest) -> RpcResult<HttpResponse> {
// Generate a comic number, between the first and most recent comic
let random_num = random_in_range(1, 2680).await?;
// Create request URL where XKCD stores JSON metadata about comics
let xkcd_url = format!("https://xkcd.com/{}/info.0.json", random_num);
let response = HttpClientSender::new()
.request(ctx, &HttpClientRequest::get(&xkcd_url))
.await?;

// Deserialize JSON to retrieve comic title and img URL
let comic: XkcdComic = serde_json::from_slice(&response.body).map_err(|e| {
RpcError::ActorHandler(format!("Failed to deserialize comic request: {}", e))
})?;

// Format HTTP response body as an HTML string
let body = format!(
r#"
<!DOCTYPE html>
<html>
<head>
<title>Your XKCD random comic</title>
</head>
<body>
<h1>{}</h1>
<img src="{}"/>
</body>
</html>
"#,
comic.title, comic.img
);

Ok(HttpResponse {
body: body.as_bytes().to_vec(),
..Default::default()
})
}
}

#[derive(serde::Deserialize)]
// XKCD comic metadata fields
struct XkcdComic {
title: String,
img: String,
}

Remember that we use a deny-by-default security model, so you need to explicitly sign your module with the capabilities it intends to use (httpserver, httpclient, numbergen). Update the Makefile's CLAIMS section to include these capabilities:

CLAIMS   = wasmcloud:httpserver wasmcloud:httpclient wasmcloud:builtin:numbergen

Go ahead and run make to build and sign this actor, the resulting signed module will be located under build/xkcdgenerator_s.wasm.

Running this Application

You can either run this application locally with wasmCloud or on the Cosmonic platform. The components are the same either way, and you can follow the wasmCloud steps first to test locally before moving onto running this on Cosmonic.

Cosmonic takes care of all of the platform requirements for running this application, the only necessary step to get this running is to upload your XKCD generator actor to a publicly accessible OCI registry. Follow the steps in the Deploying your Application documentation, and as soon as you have the OCI reference for your actor you can come back here.

Once you have the OCI reference for your actor, and it can be accessed publicly, you're ready to deploy your application on Cosmonic.

info

Hint: You can always check if an OCI reference is publicly accessible by inspecting its claims. You can also see what capabilities the actor declares, so you know exactly what the actor is allowed to do.

wash claims inspect <oci_reference>

Navigate to Cosmonic and click Launch if you haven't already signed in. Ensure that you have at least one host on your Infrastructure View; if you don't already have a host you can use the Launch Host button:

One host on infrastructure view and launch button

Navigate to the Logic View and use the toolbar on the left to Launch an Actor. Enter your actor reference in the OCI reference field (example shown) and then select Launch Actor.

actor launch modal

You'll see your actor on the canvas in just a few seconds.

XKCD actor on canvas

Next you'll launch the HTTP Server (Wormhole) and HTTP Client capability providers. Use the Launch Provider button on the left toolbar to bring up the modal, then launch both the HTTP Server and HTTP Client capability providers (you'll need to do them one at a time.) Both capability providers should keep the link name as default, and you can choose any host for them.

XKCD actor on canvas

Once you're done, your canvas should look something like this:

canvas with XKCD and two providers

Next we'll link your XKCD actor to the two capability providers, then expose an HTTPS endpoint with a wormhole so you can generate comics with a public URL. Linking to these two providers is simple since you don't need any runtime configuration, simply drag from the wasmcloud:httpserver capability bubble on the XKCD actor to the HTTP Server (Wormhole) provider and then click Create Link. Repeat these same steps dragging from the wasmcloud:httpclient bubble to the HTTP Client provider.

XKCD and httpserver and httpclient linked

To access our app in the browser, use the Create Wormhole button on the left toolbar and select your XKCD actor. If this is your only actor on the canvas, Cosmonic automatically selects this for you. For now, this wormhole should be created without authentication so you can load it in a browser tab, rather than providing an Authorization bearer token.

create wormhole modal

Once the wormhole is created, you can click on the wormhole on the canvas to select it and click Access your Wormhole to open another tab. Take a quick moment to marvel at your XKCD comic, and refresh to generate as many comics as you'd like!

full application linked on canvas

info

As soon as you've accessed your wormhole, you'll see live, directed traffic lines on your canvas showing traffic that's flowing in your constellation.

full application on canvas with traffic lines

Source Code

This example can be found in the Things to Build repository under the Cosmonic GitHub organization.

Watch us build it

Cosmonic developers did a livestream building this actor and explaining every step of the way. Check it out!