Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UI and next step to XKCD tutorial #23

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions xkcdgenerator/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# examples/actor/xkcd

.PHONY: build
PROJECT = xkcd
VERSION = $(shell cargo metadata --no-deps --format-version 1 | jq -r '.packages[] .version' | head -1)
REVISION = 2
# list of all contract claims for actor signing (space-separated)
CLAIMS = wasmcloud:httpclient wasmcloud:httpserver wasmcloud:builtin:numbergen
# registry url for our actor
REG_URL = localhost:5000/v2/$(PROJECT):$(VERSION)
# command to upload to registry (without last wasm parameter)
PUSH_REG_CMD = wash reg push --insecure $(REG_URL)
ACTOR_NAME = "XKCD Generator"

include ../../build/makefiles/actor.mk

#
# if you're running local builds you get these numbers from
# `make inspect` for providers, and `make actor_id` for actors
ACTOR_ID = $(shell make actor_id)
HTTPSERVER_PROVIDER_ID = VAG3QITQQ2ODAOWB5TTQSDJ53XK3SHBEIFNK4AYJ5RKAX2UNSCAPHA5M
HTTPCLIENT_PROVIDER_ID = VCCVLH4XWGI3SGARFNYKYT2A32SUYA2KVAIV2U2Q34DQA7WWJPFRKIKM

build:
cd ui && npm run build
wash build


link:
# link to httpserver and httpclient
# because numbergen is a builtin, it doesn't require a link command
wash ctl link put --timeout-ms 4000 $(ACTOR_ID) \
$(HTTPSERVER_PROVIDER_ID) wasmcloud:httpserver \
'config_json={"address":"127.0.0.1:8080"}'
wash ctl link put --timeout-ms 4000 $(ACTOR_ID) \
$(HTTPCLIENT_PROVIDER_ID) wasmcloud:httpclient

149 changes: 104 additions & 45 deletions xkcdgenerator/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,56 +1,115 @@
//!
//! This actor creates a simple web page with a random xkcd comic.
//!
//! The actor first selects a random comic number using the builtin number generator ,
//! then requests metadata for that comic from the xkcd site,
//! and generates and html page with the title and image url from the metadata.
//!
use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use serde_json::json;

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

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

// the highest numbered comic available. (around 2705 as of Sep 4, 2023)
// xkcd comics are numbered continuously starting at 1
const MAX_COMIC_ID: u32 = 2822;

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

/// Implementation of HttpServer trait methods
/// implement HttpServer handle_request method
#[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::ok(body))
impl HttpServer for XkcdActor {
async fn handle_request(&self, ctx: &Context, req: &HttpRequest) -> RpcResult<HttpResponse> {
match req.path.trim_start_matches('/') {
// Handle requests to retrieve comic data
"comic" =>
// all the work happens inside handle_inner.
// The purpose of this wrapper is to catch any errors generated
// by the inner function and turn them into a valid HttpResponse.
{
Ok(self
.handle_inner(ctx, req)
.await
.unwrap_or_else(|e| HttpResponse {
body: json!({ "error": e.to_string() }).to_string().into_bytes(),
status_code: 500,
..Default::default()
}))
}
ui_asset_path => {
let path = if ui_asset_path.is_empty() {
"index.html"
} else {
ui_asset_path
};
// Request for UI asset
Ok(Asset::get(path)
.map(|asset| {
let mut header = HashMap::new();
if let Some(content_type) = mime_guess::from_path(path).first() {
header
.insert("Content-Type".to_string(), vec![content_type.to_string()]);
}
HttpResponse {
status_code: 200,
header,
body: Vec::from(asset.data),
}
})
.unwrap_or_else(|| HttpResponse::not_found()))
}
}
}
}

impl XkcdActor {
async fn handle_inner(&self, ctx: &Context, _req: &HttpRequest) -> RpcResult<HttpResponse> {
let comic_num = random_in_range(1, MAX_COMIC_ID).await?;

// make a request to get the json metadata
let url = format!("https://xkcd.com/{}/info.0.json", comic_num);
let client = HttpClientSender::new();
let resp = client
.request(ctx, &CHttpRequest::get(&url))
.await
.map_err(|e| tag_err("sending req", e))?;
if !(200..300).contains(&resp.status_code) {
return Err(tag_err(
"unexpected http status",
resp.status_code.to_string(),
));
}
// Extract the 'title' and 'img' fields from the json response,
// and build html page
let info = serde_json::from_slice::<XkcdMetadata>(&resp.body)
.map_err(|e| tag_err("decoding metadata", e))?;
let resp = HttpResponse {
body: serde_json::to_vec(&info).unwrap_or_default(),
..Default::default()
};
Ok(resp)
}
}

/// Metadata returned as json
/// (this is a subset of the full metadata, but we only need two fields)
#[derive(Deserialize, Serialize)]
struct XkcdMetadata {
title: String,
img: String,
}

/// helper function to give a little more information about where the error came from
fn tag_err<T: std::string::ToString>(msg: &str, e: T) -> RpcError {
RpcError::ActorHandler(format!("{}: {}", msg, e.to_string()))
}
5 changes: 5 additions & 0 deletions xkcdgenerator/src/ui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use rust_embed::RustEmbed;

#[derive(RustEmbed)]
#[folder = "./ui/build"]
pub struct Asset;
20 changes: 20 additions & 0 deletions xkcdgenerator/ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
6 changes: 6 additions & 0 deletions xkcdgenerator/ui/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"arrowParens": "avoid",
"trailingComma": "es5",
"singleQuote": true,
"semi": true
}
68 changes: 68 additions & 0 deletions xkcdgenerator/ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).

## Available Scripts

In the project directory, you can run:

### `npm start`

Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.

The page will reload if you make edits.<br />
You will also see any lint errors in the console.

### `npm test`

Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.

### `npm run build`

Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.

The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!

See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.

### `npm run eject`

**Note: this is a one-way operation. Once you `eject`, you can’t go back!**

If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.

Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.

You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.

## Learn More

You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).

To learn React, check out the [React documentation](https://reactjs.org/).

### Code Splitting

This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting

### Analyzing the Bundle Size

This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size

### Making a Progressive Web App

This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app

### Advanced Configuration

This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration

### Deployment

This section has moved here: https://facebook.github.io/create-react-app/docs/deployment

### `npm run build` fails to minify

This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
1 change: 1 addition & 0 deletions xkcdgenerator/ui/build/Cosmonic.Logo-Hrztl_Color.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added xkcdgenerator/ui/build/android-chrome-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added xkcdgenerator/ui/build/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions xkcdgenerator/ui/build/asset-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"files": {
"main.js": "/static/js/main.32fcdd5a.js",
"static/js/27.2a45638c.chunk.js": "/static/js/27.2a45638c.chunk.js",
"index.html": "/index.html"
},
"entrypoints": [
"static/js/main.32fcdd5a.js"
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added xkcdgenerator/ui/build/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added xkcdgenerator/ui/build/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added xkcdgenerator/ui/build/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions xkcdgenerator/ui/build/github-mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions xkcdgenerator/ui/build/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/apple-touch-icon.png"/><link rel="manifest" href="/manifest.json"/><title>wasmCloud XKCD</title><script defer="defer" src="/static/js/main.32fcdd5a.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
25 changes: 25 additions & 0 deletions xkcdgenerator/ui/build/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
3 changes: 3 additions & 0 deletions xkcdgenerator/ui/build/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
Loading
Loading