SimPEL Language Reference
Basic Language Constructs
Before diving into language elements, introducing a few common definitions is necessary:
ns_id : (ID '::')? ID;
block : '{' proc_stmt+ '}';
param_block : '{' ('|' ID (',' in+=ID)* '|')? proc_stmt+ '}';
body : block | proc_stmt;
A ns_id is an identifier preceded by an optional namespace prefix, with ‘::’ as separator. For example Foo or myns::Foo are valid ns_id instances. A block is pretty self explanatory, it’s at least one process statement between curly braces. Like { reply(); }. A param_block is like a block but includes some parameters, provided between pipe characters (‘|’) and separated by commas. A valid param_block could be { |a| reply(a); }. A body is either a block or a single process statement.
All statements in SimPEL are required to end with a semicolon.
Process
The process construct is the root of a process definition. It’s also the root scope. It declares a name as a ns_id and contains the whole process body as a sequence of statements:
process : 'process' ns_id body;
A process with no namespace prefix will be created under the default http://ode.apache.org/simpel/1.0/definition namespace. In most cases you don’t really need to worry about namespaces, name collision in processes isn’t that common.
Examples:
process Purchase {
…
}
process myns::ServiceMock {
}
Conditions
process Purchase {
…
}
process myns::ServiceMock {
}
Conditions are pretty much what you would expect:
if: 'if' '(' expr ')' body ('else' body)?
Any valid expression can be used in an if as long as it’s boolean (returns true or false). The else branch is optional. At the moment there’s no specific syntax for chaining else and if (like elseif) although that should come soon.
The else branches associate to the closer previous if where there’s ambiguity.
Example:
if (updatedTask.data.accept == "true") {
…
} else {
…
}
Loops
if (updatedTask.data.accept == "true") {
…
} else {
…
}
The base loop is a while defined this way:
while: 'while' '(' expr ')' body
It’s simply composed of a boolean expression and a body. The body will be executed as long as the condition evaluates to true. The condition is tested before each execution so if it returns false when first tested, the body will never be executed.
Example:
i = 0; j = 1; cur = 1;
while (cur <= counter) {
k = i; i = j; j = kj; cur = cur1;
}
The SimPEL grammar also supports until, foreach and forall (with parallel semantics) loops but they’re unimplemented at this time.
Wait
Wait allows delayed execution, waiting for a certain period of time before completing. The delay is specified using an ISO 8601 (pdf) duration or date/time formatted string. For example P1Y2MT2H or PT2M30S are valid durations. A valid date/time is 1999-05-31T13:20:00-05:00.
Example:
wait("PT1S");
Scope
wait("PT1S");
A scope is an enclosing context, grouping a range of statements. It defines variables accessibility, resource lifecycle and the availability of handlers (like event handlers). By itself, a scope doesn’t serve any purpose but it supports the definition of other constructs:
scope: ‘scope’ (‘(’ ID ‘)’)? body scope_stmt*;
scope_decl: onevent | onalarm | onquery | onreceive | onupdate | compensation;
A scope can be named using an identifier. It contains a body, eventually followed by several related declarations. For more information regarding the event handler (the on* elements), refer to the RESTful Communications section. Compensation and onAlarm are unimplemented at this time.
Example:
scope {
counter = 0;
while(counter > 0) {
wait("PT10S");
}
} onQuery(self) {
links = <counter></counter>;
links.decrement = dec;
links.value = value;
reply(links);
} onQuery(value) {
reply(counter);
} onReceive(dec) {
counter = counter - 1;
reply(counter);
}
Expressions
scope {
counter = 0;
while(counter > 0) {
wait("PT10S");
}
} onQuery(self) {
links = <counter></counter>;
links.decrement = dec;
links.value = value;
reply(links);
} onQuery(value) {
reply(counter);
} onReceive(dec) {
counter = counter - 1;
reply(counter);
}
Literals and Operators
Supported literals at this time are integers (14 or 1000), floats, strings with escape sequences (“abc” or “\tfoo\n”) and XML (
Supported operators from lower to higher precedence:
==, !=, <, >, <=, >=, &&, ||, +, -, *, /, ! and unary -.
Variables and Scoping
==, !=, <, >, <=, >=, &&, ||, +, -, *, /, ! and unary -.
Variable declaration in SimPEL is implicit and enclosed in the nearest scope where it’s first used. A variable starts to exist when it’s first assigned and stops to exist when the scope enclosing that first assignment exits. Variables can’t be shadowed (where a name hides another identical name in a higher level scope), they’re always redefined.
Variable assignment has a copy semantic, the content is duplicated.
Other Language Constructs
Other constructs like pick, flow (with signals and joins), try/catch, with, throw, compensate, exit will find their way in SimPEL but they’re all unimplemented at this time.
E4X Support
In addition to classic expressions (like foo = bar + 4), SimPEL supports most of the E4X specification. This translates into the following properties:
- XML literals
- Embed expressions in the literals using { … } escapes.
- Easy access to XML nodes (order.item.@id)
Detailing all the features of E4X is outside the scope of this specification, please check the E4X specification for more.
E4X support is still partial at the moment. Specifically escaping is only supported for element and not attributes and the set of accepted expressions in an escape is limited to expressions of the form foo.bar[baz].
Examples:
inviteEmail = <email><to>{ vote.participants.name[m] + "@intalio.com" }</to></email>;
inviteEmail.body = "Hi, how are you?";
RESTful Communications
inviteEmail = <email><to>{ vote.participants.name[m] + "@intalio.com" }</to></email>;
inviteEmail.body = "Hi, how are you?";
SimPEL has several constructs to enable HTTP-based communication. The goal is to make receiving and issuing HTTP requests easy, hiding the low level details in the common case without making complicated interactions impossible. In addition to the basic HTTP support, SimPEL adds constraints to enable characteristics like idempotence or cache control.
Resources
Resources are scoped, are associated with a name by assignment and declare the relative path that denotes the relationship with other resources. In the default case (not other resource is provided), it’s relative to the running process. Processes use resource references for the purpose of receiving requests on that resource or passing the absolute resource path. Once first assigned, resources are read-only.
A resource that specifies a static path will be instantiated by appending that path to the instance URL. A resource that specifies a path relative to another resource will be instantiated by appending that path to the URL of the other resource. We use the $ notation to reference another resource, so the path $foo/bar will append /bar to the URL denoted by the resource $foo. Note that at the time of this writing, the $ notation isn’t supported yet.
Resources can rely on URL patterns defined between curly braces (ex: “/ballot/{name}”). The way patterns are bound to variable will be defined in the Providing a Resource section.
Process definitions, once deployed, exist permanently with their own resource exposed under the deployment URL. The process resources react to POST requests by starting a new process execution (or process instance) that gets, from this point, its own resource. This resource is implicitly defined in all processes as self.
New resources can be instantiated by a POST request on an existing resource, to realize the classic resource collection pattern but this unimplemented for now.
Example:
scope {
cancelResource = resources("/cancel");
}
Providing a Resource
scope {
cancelResource = resources("/cancel");
}
The first way to provide a resource in a process is by using receive:
receive : receive_base param_block | receive_base SEMI;
receive_base : 'receive' '(' ID ')';
The receive takes a resource for parameter and waits for a POST request. Once the POST is received, it completes normally. All processes should start with a receive on self called the instantiating receive. A process will reply to a POST request on an instantiating receive using the 201 (Created) status code and will provide the URL of the new process instance in the Location HTTP header. Subsequently a process can have any number of receives, including on self.
The payload received by a receive can either be associated to a variable using assignment or by passing it into a block. The block scopes the variable and enables a couple of shortcuts for replies.
Each receive must be matched with a reply on the same resource:
reply: 'reply' '(' (ID (',' ID)?)? ')';
The first reply parameter is used to provide the payload to be sent back to the caller. It has to be a variable, expressions aren’t accepted at the moment. The second parameter is the resource replying from (more than one interaction can be going on at once in a process). The payload can be omitted if the response can be empty, in which case only headers will be sent back. In a block receive, the resource can be omitted as well, it will automatically use the same resource as the receive. In a block receive, the reply can be completely omitted, in which case an implicit empty reply will be inserted as the last statement in the block.
To implement a resource over recurring requests, a SimPEL process can rely on event handlers. Handlers are associated to a scope and live as long as their scope. Like a receive, all event handlers have to be matched with a reply, following the same rules as block receives.
The onQuery event handler responds to GET requests, as such it’s required to have no side effect. As a consequence, an onQuery can only modify variables local to its block and has read-only access to other variables (%(todo)not enforced for now).
onquery: 'onQuery' '(' ID ')' param_block;
If the resource provided to onQuery includes URL patterns, the block can declare parameters that will be associated to pattern element values extracted from the URL. As an example, if a resource is exposed under “/ballot/{name}” and the URL called is “/ballot/john”, the onQuery block can define a |name| parameter that will be initialized to “john”.
At the moment, no cache control is implemented but the process engine can use cache control to optimize the GET method by handling conditional GETs and setting the Last-Modified/ETag headers. These are set to detect any change in the state of the process instance (which excludes responding to a GET request).
The onReceive is an event handler equivalent to a classic receive:
onreceive: 'onReceive' '(' ID ')' param_block;
The first block parameter defined on an onReceive block will be initialized with the received payload. Subsequent block parameters are associated with eventual URL patterns.
The onUpdate event handler (%(todo)unsupported for now%) is used to modify state and responds to PUT requests:
onupdate : 'onUpdate' '(' r=ID ')' param_block;
Conditional PUT should be supported.
The onDelete event handler (%(todo)unsupported for now%) is used to modify state and responds to DELETE requests:
onDelete: 'onDelete' '(' r=ID ')' param_block;
Example (see the Resource Fun tutorial for the complete process):
tally = resource("/tally");
ballot = resource("/ballot/{name}");
close = resource("/close");
cancel = resource("/cancel");
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);
}
Calling Resources
tally = resource("/tally");
ballot = resource("/ballot/{name}");
close = resource("/close");
cancel = resource("/cancel");
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);
}
To build a request and call a URL, the request construct should be used:
request: request_base param_block | request_base SEMI;
request_base: 'request' '(' expr (',' STRING (',' ID)?)? ')';
The first parameter is an expression that must evaluate to an absolute URL string. The HTTPS URL scheme is supported. The second parameter is a case-insensitive string providing the HTTP method that should be called (“get”, “put”, “post”, “delete”; “head” is unsupported for now). The third parameter is a variable providing the entity-body and the headers that will be sent in the request.
In the case of a GET request, both the method and the variable (obviously GET requests have no body) can be omitted.
HTTP headers can be provided using the headers special element available on all request variables. Headers can be both set and read. Headers that normally include a dash (‘-’) character in the HTTP specification are replaced with an underscore (‘_’), for example Content-Type becomes Content_Type. For HTTP basic authentication, a special basicAuth sub-element of headers can be used to set the loging and password, using elements of the same name.
Request examples:
process AllMethods {
receive(self) { |query|
getRes = request(testRoot);
res = getRes.text();
postMsg = <foo>foo</foo>;
postRes = request(testRoot, "post", postMsg);
res = res + postRes.text();
putMsg = <bar>bar</bar>;
putRes = request(testRoot, "put", putMsg);
res = res + putRes.text();
request(testRoot, "delete");
reply(res);
}
}
Header example:
req = <order><id>{orderId}</id></order>
req.headers.basicAuth.login = "scott";
req.headers.basicAuth.password = "tiger";
req.headers.Accept = "application/xml";
request(orderingSystem, "POST", req);
Running Environment
req = <order><id>{orderId}</id></order>
req.headers.basicAuth.login = "scott";
req.headers.basicAuth.password = "tiger";
req.headers.Accept = "application/xml";
request(orderingSystem, "POST", req);
All SimPEL processes run tightly integrated with their own Javascript shell. The shell defines a few global utility functions and is used to execute the expressions embedded in a process. You can add your own functions and global variables in the shell by declaring them before the process definition.
Shell and Process Integration
The SimPEL header (comprising everything that comes before the process definition) is pre-loaded when a script gets deployed. It gets executed in a process-specific shell and can therefore alter it. The header can define global variables and functions necessary to the process. It can also be used to set values that the process engine uses to configure the process runtime, mainly by modifying the content of the processConfig hash (see configuration for more detail). Finally, it provides the load function which is very useful to avoid overloading the header definition, allowing to load those from a separate file.
The same shell is used when a process instance gets executed to evaluate all expressions. Those expressions can therefore rely on pre-defined external functions and values. However, during runtime, expressions can’t alter the global environment anymore, only process variables can be mutated. A modification of a global definition during a process execution will not persist.
For example, let’s have a look at the following process:
processConfig.address = "/crinresp";
var gppdAddress = "http://localhost:3434/gppd";
function splitting(csv) {
return <text>{csv.split("\n")[0]}</text>;
}
process RequestError {
receive(self) { |query|
resp = request(gppdAddress);
resp2 = splitting(resp);
print("— " + resp2.text());
reply(resp2);
}
}
The process header is used to set the address under which the process is going to be deployed, by altering a processConfig value, and to define the splitting global function. This function can then be used freely anywhere an expression can be included in a SimPEL process. Note also the use of the print pre-defined function. If the gppdAddress variable was to be altered by a process expression, as mentioned earlier, that change wouldn’t persist.
Utility Functions
Several functions are pre-loaded and always available in the shell. As such they can be called either from the process header of from expressions.
- load(string) – provided the path to a Javascript file (absolute or relative to the SimPEL script location), loads it in the current environment
- print(string) – prints the provided string to the standard output
- readFile(string)→string – provided the path to a text file (absolute or relative to the SimPEL script location), reads it and returns its content as a string
- readUrl(string)→string – provided a URL address, reads the content of the target and returns it as a string
- runCommand(string)→number – runs the provided native OS command, returning the resulting exit code