Chuck kicks

Introduction (and TL;DR)

You want to build a website based on microservices.

Leonardo makes it easy to build the services and handle separation of concerns by routing messages to (sub)services exposed to the web browser. Jo exposes these services as JavaScript objects to the webpage (an idea I’ve been toying with recently). I call the result a Microservice-aware Web Application (MAWA), because the webpage accesses explicitly (is aware of) the different (micro)services made available by the server.

The purpose of this document is to show you how. The only software you need installed in your computer to try out what I show here is Docker.

I’m going to use some Jolie language [Montesi et al., 2014], in particular its web features [Montesi, 2016], because it makes things very simple for the purposes of this tutorial. You do not need to know Jolie to read what follows: the code is simple enough to be explained on the fly.

References

You can run the finished application with the following Docker commands.

docker pull fmontesi/jo-demo-chuck
docker run -it --rm -p 8080:8080 fmontesi/jo-demo-chuck

Just browse to http://localhost:8080/ once the container is running.

You can look at the finished source code here: https://github.com/fmontesi/jo-demo-chuck.

Get started

Create the following directory structure.

app/
	internal/
web/

We will use app for our services, and web for our web frontend files.

Install the web server

Go to app/internal and download the Leonardo web server in the subdirectory leonardo:

curl -L https://github.com/jolie/leonardo/archive/0.2.tar.gz | tar xvz --transform 's/leonardo-0.2/leonardo/'

We will use Leonardo to serve static content and to route service invocations from webpages to our services.

Write the main application

Now we write our main Jolie application, in file app/main.ol.

// This includes the LeonardoAdmin output port, which we'll use to configure Leonardo
include "internal/leonardo/ports/LeonardoAdmin.iol"

// Embedding is Jolie for running a service inside of the same VM
embedded {
Jolie:
	/*
		Run Leonardo and link it to the output port LeonardoAdmin
		Communications over embedding use local memory (they're fast)
		The Standalone=false parameter means that we'll have to configure Leonardo
	*/
	"-C Standalone=false internal/leonardo/cmd/leonardo/main.ol" in LeonardoAdmin
}

main
{
	with( config ) {
		.wwwDir = "../web" // The web content directory
	};
	config@LeonardoAdmin( config )(); // Send the configuration to Leonardo
	linkIn( Shutdown ) // Wait until somebody terminates us
}

Make a run script

First, pull Jolie.

docker pull jolielang/jolie

Now we make a convenience script to run our website. Write this in run.sh in the root directory of your project.

#!/bin/sh

docker run -it --rm \
	-v "$(pwd)"/web:/web \
	-v "$(pwd)"/app:/app \
	-w /app \
	-p 8080:8080 \
	jolielang/jolie \
	jolie main.ol

The script mounts the app and web directories as volumes, goes in app, and runs our program in main.ol. We expose the TCP port 8080.

Make the script executable:

chmod +x run.sh

Whenever I say “run the application” in the remainder, I mean: go to the root directory of the project and run ./run.sh (sh run.sh also works, if you did not make run.sh executable).

Test if your setup works

Create a file web/index.html with this content:

<html><body>Hello, World!</body></html>

Run the application (reminder: this means executing run.sh). You should see the output:

Leonardo started
	Location: socket://localhost:8080/
	Web directory: /web/

Go to http://localhost:8080/ with your web browser. You should see the “Hello, World!” message we wrote in web/index.html.

Hello, World!

Adding services

Let’s add some dynamic fun. We can use Leonardo to proxy requests to both external and internal services. External services are services run outside of our application. Internal services are provided by our application. Other than that, their configuration in Leonardo is the same.

External services

By “dynamic fun”, I meant Chuck Norris jokes. There’s an API for that: https://api.chucknorris.io/. (If you didn’t know about this, reading this tutorial might be worth it just for getting that link now…) For example, to search for a joke containing the word “computer”, we just invoke https://api.chucknorris.io/jokes/search?query=computer.

To get Chuck Norris in our application, we need to update app/main.ol as follows. I comment the new lines (the rest is as before).

include "internal/leonardo/ports/LeonardoAdmin.iol"

// Output port to contact api.chucknorris.io
outputPort Chuck {
Location: "socket://api.chucknorris.io:443/" // Use TCP/IP, port 443
Protocol: https {
	.osc.search.method = "get"; // HTTP method for operation search
	.osc.search.alias = "jokes/search?query=%{query}" // URI template for operation search
}
RequestResponse: search // Request-response operation declaration (search)
}

embedded {
Jolie:
	"-C Standalone=false internal/leonardo/cmd/leonardo/main.ol" in LeonardoAdmin
}

main
{
	with( config ) {
		.wwwDir = "../web";

		// Expose a service to the web application
		with( .redirection[0] ) {
			.name = "ChuckNorris"; // Name of the exposed service
			.binding -> Chuck // Binding information (from output port Chuck)
		}
	};
	config@LeonardoAdmin( config )();
	linkIn( Shutdown )
}

Our web server now exposes a ChuckNorris service that supports operation search. Next, we update the web/index.html page to invoke this service. We use Jo (a library to interact with Jolie web servers) to call the web server, and jQuery to interact with the DOM (but you can use any other framework you like).

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<script
		src="https://raw.githubusercontent.com/fmontesi/jo/master/lib/jo.js"></script>
	<script
		src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
		integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E="
		crossorigin="anonymous"></script>
</head>
<body>
	<p>
		<input type="text" id="jokeQuery" value="Computer" />
		<button id="getJokeBtn" type="button">Get me a Chuck joke!</button>
	</p>
	<p id="display"></p>

	<script>
		$(document).ready( () => {
			$("#getJokeBtn").click( () => {
				$("#display").html( "Work in progress..." );

				// Call operation search on the exposed service ChuckNorris
				Jo("ChuckNorris").search( { query: $("#jokeQuery").val() } )
					.then( response => {
						// Pick a random joke
						// api.chucknorris.io returns jokes in a "result" array subelement
						$("#display").html(
							( Array.isArray( response.result ) )
							? response.result[ Math.floor(Math.random()*response.result.length) ].value
							: "This is not a joke"
						);
					} )
					.catch( JoHelp.parseError ).catch( alert );
			} );
		} );
	</script>
</body>
</html>

Clicking the button “Get me a Chuck joke!” will fetch a Chuck Norris joke from https://api.chucknorris.io/ for us. I got this joke:

Chuck Norris is no joke

Internal services

What good is a joke if you can’t share it? We write a Jolie service to post our jokes to https://telegra.ph/. I’m going to put all the code in our app/main.ol. (In a real project, you’d probably want to start creating separate files at this point and embed them, just like we did with Leonardo.)

Here’s the new app/main.ol.

include "internal/leonardo/ports/LeonardoAdmin.iol"

outputPort Chuck {
Location: "socket://api.chucknorris.io:443/"
Protocol: https {
	.osc.search.method = "get";
	.osc.search.alias = "jokes/search?query=%{query}"
}
RequestResponse: search
}

embedded {
Jolie:
	"-C Standalone=false internal/leonardo/cmd/leonardo/main.ol" in LeonardoAdmin
}

include "string_utils.iol"

// Output port to Telegraph. We're going to need it to implement our internal service.
outputPort Telegraph {
Location: "socket://api.telegra.ph:443/"
Protocol: https {
	.format = "json";
	// Yeah, I'm using querystrings... that's the official example in the Telegraph API documentation
	.osc.createAccount.alias =
		"createAccount?short_name=%{short_name}";
	.osc.createPage.alias =
		"createPage?access_token=%{access_token}&title=%{title}&content=[\"%{content}\"]"
}
RequestResponse: createAccount, createPage
}

type CreatePageRequest:void {
	.title:string
	.content:string
}

interface TelegraphPosterIface {
RequestResponse: createPage(CreatePageRequest)(string) throws TelegraphError(string)
}

// Here's the internal service that we're going to expose to the webpage
service TelegraphPoster {
Interfaces: TelegraphPosterIface
main {
	createPage( postRequest )( postUrl ) {
		// Create a fresh account on Telegraph
		short_name = new;
		replaceAll@StringUtils( short_name { .regex = "-", .replacement = "" } )( short_name );
		createAccount@Telegraph( { .short_name = short_name } )( response );
		if ( !response.ok ) throw( TelegraphError, response.error );

		// Post the joke with the account we've just created
		access_token = response.result.access_token;
		createPage@Telegraph( {
			.access_token = access_token,
			.title = postRequest.title,
			.content = postRequest.content
		} )( response );
		if ( !response.ok ) throw( TelegraphError, response.error );
		postUrl = response.result.url // Return the post's url to the webpage
	}
}
}

main
{
	with( config ) {
		.wwwDir = "../web";
		with( .redirection[0] ) {
			.name = "ChuckNorris";
			.binding -> Chuck
		};
		with( .redirection[1] ) { // We add the redirection for TelegraphPoster
			.name = "TelegraphPoster";
			.binding.location = TelegraphPoster.location
		}
	};
	config@LeonardoAdmin( config )();
	linkIn( Shutdown )
}

So now we have a new service TelegraphPoster that the webpage can invoke to post content on Telegraph. Let’s use it to share our jokes.

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<script
		src="https://raw.githubusercontent.com/fmontesi/jo/master/lib/jo.js"></script>
	<script
		src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
		integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E="
		crossorigin="anonymous"></script>
</head>
<body>
	<p>
		<input type="text" id="jokeQuery" value="Computer" />
		<button id="getJokeBtn" type="button">Get me a Chuck joke!</button>
	</p>
	<p id="display"></p>
	<p>
		<button id="postJokeBtn" type="button">Share this wisdom</button>
	</p>
	<p id="telegraphDisplay"></p>

	<script>
		$(document).ready( () => {
			$("#getJokeBtn").click( () => {
				$("#display").html( "Work in progress..." );
				Jo("ChuckNorris").search( { query: $("#jokeQuery").val() } )
					.then( response => {
						// Pick a random joke
						$("#display").html(
							( Array.isArray( response.result ) )
							? response.result[ Math.floor(Math.random()*response.result.length) ].value
							: "This is not a joke"
						);
					} ).catch( JoHelp.parseError ).catch( alert );
			} );

			// Should be self-explanatory by now
			// When the button is clicked, invoke createPage at TelegraphPoster
			$("#postJokeBtn").click( () => {
				$("#telegraphDisplay").html( "Work in progress..." );
				Jo("TelegraphPoster").createPage( {
					title: "Chuck Norris Wisdom",
					content: $("#display").html()
				} ).then( response => {
					$("#telegraphDisplay").html(
						`Wisdom shared at <a href=\"${response.$}\" target=\"_new\">${response.$}</a>`
					);
				} ).catch( JoHelp.parseError ).catch( alert );
			} );
		} );
	</script>
</body>
</html>

Here’s a screenshot of what the final page looks like.

Now everybody knows

Done!

It’s done!

If you’d like to containerise your application, you just need a simple Dockerfile, like the following.

FROM jolielang/jolie

COPY app /app
COPY web /web

WORKDIR /app
EXPOSE 8080

CMD ["jolie","main.ol"]

Some cloud platforms like Heroku do not enforce the EXPOSE directive. They require a fixed HTTP endpoint port number, so change the last line into the following:

CMD jolie -C Location_Leonardo=\"socket://localhost:$PORT/\" main.ol

References

  • Fabrizio Montesi. (2016). Process-aware web programming with Jolie. Sci. Comput. Program., 130, 69–96. doi arxiv
  • Fabrizio Montesi, Claudio Guidi, & Gianluigi Zavattaro. (2014). Service-Oriented Programming with Jolie. In A. Bouguettaya, Q. Z. Sheng, & F. Daniel (Eds.), Web Services Foundations (pp. 81–107). Springer. doi pdf