Resource Fun
If you aren’t comfortable with the environment in which SimPEL processes execute and want to run the process shown in this tutorial, it’s recommended to have a prior look at the first tutorial. But if you’re just looking around, feel free to proceed. All the files mentioned here are located in the distribution under the samples/vote directory or online.
A Voting Process and Its Toolkit
This tutorial is going to show you how to implement a RESTful web service using SimPEL, demonstrating mostly connectedness and addressability (the uniform interface comes with the limited number of SimPEL constructs that closely match HTTP methods). Our goal is to build a voting service and let everybody express their opinion. The process itself is fairly straightforward:
- Someone submits a new vote subject, with a list of participants’ emails.
- Participants get notified that a new vote started and are invited to vote at a specific URL.
- Participants can vote by submitting their choice.
- At anytime the current tally can be queried.
- When the vote is over, it can be closed. The tally stays available but voting isn’t allowed anymore.
- A vote can be completely canceled.
This process is probably slightly atypical in the sense that it’s completely self contained (most business processes leverage existing services). However it will demonstrate how we can have a process use an external library and integrate with plain Javascript code fairly easily. For more on external calls, the Calling Out tutorial should teach you what you need to know.
During the execution, we’re going to maintain an XML data structure representing all the current ballots. From this structure we’ll extract the current tally. Here is what an example list of ballots will look like:
<ballots>
<ballot email="fred">simple</ballot>
<ballot email="tonio">open</ballot>
<ballot email="nico">open</ballot>
</ballots>
We’re going to need a library to add ballots and build a tally based on this data structure. We don’t want to do all that data manipulation in the process, it would make it harder to read and maintain and wouldn’t promote a proper separation of concern. By putting that code in a separate library, we’ll be able to change the ballots list without even touching the process.
Library is actually a big word for not much in our case, we’ll only need 3 functions included in the vote-lib.js file. The implementation is fairly straightforward, even if you’re not very familiar with Javascript you should be able to grasp the gist of it. It’s not necessary to understand it all anyway, what we’re really interested in is how the process will use these.
A Closer Look
Before diving into specific sections of the vote process, it’s useful to have a look at the overall structure for a minute:
processConfig.inMem = false;
processConfig.address = "/api/vote";
rootUrl = "http://localhost:3434"
emailUrl = "http://localhost:3434/api/mock/email";
load("vote-lib.js");
// Provides links to additional resources that can be used during the vote.
function addLinks(vote, addr) {
vote.appendChild(<action rel="cancel" href={rootUrl + addr + "/cancel"} method="POST"/>);
vote.appendChild(<link rel="tally" href={rootUrl + addr + "/tally"} />);
return vote;
}
// Links to a ballot resource.
function createBallotInfo(vote, voteUrl, name) {
info = vote.copy();
info.appendChild(<link rel="ballot" href={rootUrl + voteUrl + "/ballot/" + name} />);
info.appendChild(<action rel="ballot" href={rootUrl + voteUrl + "/ballot/" + name} method="POST" />);
return info;
}
// Utility function to build the target URL of a ballot.
function buildBallotUrl(processUrl, name) {
return rootUrl processUrl.slice(4) "/" + name;
}
process Vote {
receive(self) { |newvote|
vote = addLinks(newvote, self);
status = <status>Vote created</status>;
reply(status);
}
// Sending an e-mail to all participants to let them know a vote started
m = 0;
while(m < vote.participants.name.length()) {
inviteEmail = <email><to>{ vote.participants.name[m] "@intalio.com" }</to></email>;
inviteEmail.body = "A vote has been started on '" vote.text
"'. Please cast your vote at " buildBallotUrl(self, vote.participants.name[m]) ".";
request(emailUrl, "post", inviteEmail);
m = m 1;
}
// Declaring all necessary resources
tally = resource("/tally");
ballot = resource("/ballot/{name}");
close = resource("/close");
cancel = resource("/cancel");
// This element will hold all the ballots
ballots = <ballots></ballots>;
voteOpen = true;
scope {
receive(cancel) { |r|
cancelResp = <vote>Vote canceled.</vote>;
reply(cancelResp);
}
} onQuery(self) {
reply(vote);
} onQuery(tally) {
currentTally = getCurrentTally(ballots);
reply(currentTally);
} onReceive(ballot) { |b, name|
if (voteOpen == true) {
ballots = updateBallots(ballots, b.ballot, name);
userBallot = getUserBallot(ballots, name);
reply(userBallot);
} else {
resp = <vote>Vote is closed.</vote>;
reply(resp);
}
} onQuery(ballot) { |name|
userBallot = getUserBallot(ballots, name);
if (userBallot.length != 0) {
reply(userBallot);
} else {
info = createBallotInfo(vote, self, name);
reply(info);
}
} onReceive(close) {
voteOpen = false;
finalTally = getCurrentTally(ballots);
reply(finalTally);
}
}
The header contains mostly global constants definition and a few utility functions. Those functions are plain Javascript, making the manipulation of XML documents very easy thanks to the E4X extensions. Then comes the process body itself which is roughly in two parts: the initialization of the process and the vote interactions. The part not to miss in the header is the load call, just after the emailUrl definition. That’s how we load the small vote API functions in the current context, just by loading the file that contains them (the path is relative to the process file).
Initialization
The process starts by receiving a request on the self resource, by now you should be familiar with this pattern:
process Vote {
receive(self) { |newvote|
vote = addLinks(newvote, self);
status = <status>Vote created</status>;
reply(status);
}
// Sending an e-mail to all participants to let them know a vote started
m = 0;
while(m < vote.participants.name.length()) {
inviteEmail = <email><to>{ vote.participants.name[m] "@intalio.com" }</to></email>;
inviteEmail.body = "A vote has been started on '" vote.text
"'. Please cast your vote at " buildBallotUrl(self, vote.participants.name[m]) ".";
request(emailUrl, "post", inviteEmail);
m = m 1;
}
…
Here we’re using a block notation for the first receive: the newvote variable is local to the block and contains the received request body. In the block we initialize the vote by inserting links in the received vote structure, in SimPEL any assignment can call an external function. As a result, our vote resource will be decently well connected, requesting it will tell you what can be done next. We end the block with a basic reply.
The following loop is used to call out to a very simple email service. The loop itself is a basic while. In this case a “for each” type of loop iterating on the selected XML nodes would probably much more elegant but at the time of this writing, it hasn’t been implemented in SimPEL yet. It should be coming really soon however.
The composition of the email request is done using E4X embedded expressions. Then it’s sent in a POST request to the emailUrl defined in the process header. Each POST will result in an email being sent to the provided address. If you’re wondering about the implementaion of the email web service, for the purpose of this tutorial we’ve mocked it with another process. Its implementation is in the vote-email-mock.simpel file also contained in the samples directory of the distribution.
Vote Interactions
Once an e-mail has been sent to all participants, the vote itself can start. All participants are expected to cast their vote by calling back the process. In the real world you would probably have a web UI that would server as a frontend to the vote web service but that’s outside of the scope of this tutorial. What’s really interesting to us is what simple HTTP requests the process will handle.
The set of resources that the vote exposes, in addition to the vote itself represented by the process and its self resource, are declared like this:
…
// Declaring all necessary resources
tally = resource("/tally");
ballot = resource("/ballot/{name}");
close = resource("/close");
cancel = resource("/cancel");
…
All resource are declared relatively to self and their declarations are fairly straightforward. The only slight difference is in the ballot resource declaration that uses a URL pattern. This resource will respond to all requests under /ballot/ and we’re going to see shortly how the name is bound.
All those resources are used as follow:
…
// This element will hold all the ballots
ballots = <ballots></ballots>;
voteOpen = true;
scope {
receive(cancel) { |r|
cancelResp = <vote>Vote canceled.</vote>;
reply(cancelResp);
}
} onQuery(self) {
reply(vote);
} onQuery(tally) {
currentTally = getCurrentTally(ballots);
reply(currentTally);
} onReceive(ballot) { |b, name|
if (voteOpen == true) {
ballots = updateBallots(ballots, b.ballot, name);
userBallot = getUserBallot(ballots, name);
reply(userBallot);
} else {
resp = <vote>Vote is closed.</vote>;
reply(resp);
}
} onQuery(ballot) { |name|
userBallot = getUserBallot(ballots, name);
if (userBallot.length != 0) {
reply(userBallot);
} else {
info = createBallotInfo(vote, self, name);
reply(info);
}
} onReceive(close) {
voteOpen = false;
finalTally = getCurrentTally(ballots);
reply(finalTally);
}
}
As we’ve seen in the first tutorial, the scope enables parallel events to happen as long as it’s active. Here we have 5 separate possible events and the scope will stay active until the receive it contains get triggered for cancellation. So cancellation is achieved just by allowing the scope to exit, stopping the event listeners and completing the process.
But before cancellation, a lot can happen. The first onQuery, listening on self just gets you the current vote. Thanks to the initialization work we’ve done it returns an XML structure that’s well connected, linking to the tally and closing resources. The second onQuery produces the current tally by calling out to the getCurrentTally function, part of our small Javascript library.
The third event and the first onReceive is probably the most important part of our process as it registers individual ballots. Its implementation shouldn’t need too much explanation. The interesting part is in the block declaration, more specifically the variables passed to the block, here b and name. In an onReceive, the first block variable contains the request. Subsequent variables get associated with the URL patterns in the corresponding resource. If you remember the ballot resource declaration, we had a name pattern in its URL. That’s where our name variable value will come from: the URL. So if someone POSTs to ballot/joe, the name variable will contain “joe”.
The same mechanism gets into play in the onQuery on the ballot resource. But an onQuery maps to an HTTP GET request, there’s no body, hence the URL parameters start right from the first block variable. Also notice how we reply in both branches of the if. This is important, requests always need a response. In a block without a reply, a default empty reply gets generated, which isn’t what we want here.
The final onReceive has nothing really special worth mentioning, it just doesn’t care what the request exactly is so it even doesn’t declare a block variable.
Let’s Execute!
After all these explanations, it’s time for some playing. Start the SimPEL runtime and deploy the vote process by copying the whole samples/vote directory under scripts. Our mock email service will be deployed as well. Your processes can be deployed under any directory structure under scripts so feel free to arrange them you way you want.
Once everything deployed, to start the process we’ll use our old curl friend:
$ curl -v --data "<vote><participants><name>mriou</name><name>assaf</name></participants><text>Pepperoni or salsiccia?</text></vote>" --request POST --header "Content-Type: application/xml" http://localhost:3434/api/vote
The returned location should be something like http://localhost:3434/api/vote/1054. Make sure to use the proper URL id in all the following requests. Now let’s see what we can get there:
$ curl --request GET http://localhost:3434/api/vote/1054 <?xml version="1.0" encoding="UTF-8"?> <vote xmlns=""><participants><name>mriou</name><name>assaf</name></participants><text>Pepperoni or salsiccia?</text><action href="http://localhost:3434/api/vote/1053/cancel" method="POST" rel="cancel"/><link href="http://localhost:3434/api/vote/1053/tally" rel="tally"/></vote>
As expected, we get back the vote structure, including interesting links. Now let’s submit a couple of votes:
$ curl --data "<vote><ballot>Pepperoni</ballot></vote>" --request POST --header "Content-Type: application/xml" http://localhost:3434/api/vote/1054/ballot/mriou Pepperoni $ curl --data "<vote><ballot>Salsiccia</ballot></vote>" --request POST --header "Content-Type: application/xml" http://localhost:3434/api/vote/1054/ballot/assaf Salsiccia
You might wonder how we got those links, after all a truly RESTful service should give all URLs by traversing the resources URL space but in this case the ballot URLs aren’t public with the vote. In this process, all the ballot URLs have been sent by email to the participants so the 2 above curl calls simulate those interactions.
This should give us a tally with a couple of entries:
$ curl --request GET http://localhost:3434/api/vote/1054/tally <?xml version="1.0" encoding="UTF-8"?> <tally><vote text="Pepperoni">1</vote><vote text="Salsiccia">1</vote></tally>
And it seems we have a draw!. From here, you should be able to close the vote by yourself and see what happens if you try to submit a ballot after. Also try to cancel the process and again, see what happens if you get the tally.
One last thing: there’s a sample web page that does the same as our curl session but a bit more graphically. It relies on some Javascript code using JQuery. Give it a try, it’s fun: http://localhost:3434/vote/.
Final Notes
You should now be much more familiar with building your own resources with SimPEL in just a few lines. It requires a bit of thinking but the result is remarkably clean. Of course the process we’ve seen here still stays very simple but it’s very easy to extend, adding more resources, relying on other services (as our email mock demonstrated) or on libraries.