The main di:es:el language reference, explained through examples rather than grammars etc.
We use messages and rules, here's a quick dictionary:
Here is a sample flow, where you can see the original message (input) and the rules which generated other messages (generated) and tests (expect):
All these are organized in Stories and specs:
Essentially, the logic is configured in specs and the stories are tests and use cases.
$when
) or a mock (defined with $mock
) can decompose a message into other messages, send requests and process responses from other systems or databases, set values in the context etc. Why we chose rules as opposed to classic structured programming is flexibility and minimal coding. You'll see that pattern matching is a very powerful tool, compared to classic if/else/switch constructs.
The number of language concepts is small:
$msg
, $when
and $mock
$send
and $expect
$val
, $def
$class
, $object
Stories are used to send a message and then test some expectations about it:
$send snakk.text(url="https://www.google.ca")
$expect (payload contains "html")
The DSL above will look like this, when rendered in a page (if it doesn't look like this, then it is not valid syntax):
send::
msg snakk.text (url ="https://www.google.ca"
...)
expect:: (payload contains "html"
)
In a story, this is the test trigger. When "running the story" this will send the message.
The expressions available in parameter/argument lists are fairly intuitive, see Expressions and pattern matching or the full set of tests we use for the engine itself: expr story++.
Tests are expected to follow a $send
, here's another example.
$send subDb.create(name="Jane", address="12 Greenbriar, Aurora")
$expect (subId ~= "sub[0-9]*")
$expect (name=="Jane")
There is a variety of expressions available for $expect
, see Expressions and pattern matching or the full set of tests we use for the engine itself: expr story++.
We can also expect messages, not just values.
$expect entity.method (parms)
Specifications are used to mock services or implement actual rules.
Mocking will simulate a message and populate some values as if they were returned by the real execution of the message. This would be a typical lookup in a database or another service; a logical service to calculate something etc.
$mock chimes.welcome => (greetings="Greetings, "+name)
0 $mock::
chimes.welcome
. (greetings
=("Greetings, " + name)
)
or just make up a value, unrelated to a message (settings whatnot):
$val name:String="gigi"
val name="gigi"
="gigi"
The =>
means decomposition, i.e. an initial message is decomposed into more messages. When decomposing into more than one message, put each message on its own line, startig with the =>
symbol:
$mock cust.addToCart
=> inventory.check
=> $if (result=="ok") cart.addItem
=> $if (result!="ok") (error="Not enough items")
=> $else (message="this branch will weirdly trigger when adding items - read the preceeding IFs again...")
$when
constructs a rule, i.e. when a message is matched, generate another message. This is how most systems work: they receive messages and send messages in response (decompose messages).
$when home.guest_arrived(name=="Jane") => chimes.welcome(name="Jane")
$when::
home.guest_arrived (name == "Jane"
)
chimes.welcome (name="Jane"
="Jane")
This could also be written with an $if
- this is suitable if a condition uses context variables not just message input:
$when home.guest_arrived $if (name=="Jane") => chimes.welcome(name="Jane")
A system can use both mocks and rules of the same messages. If a rule is found, it will run instead of the mock, unless the system is running in mock mode, see Flags and modes.
Each decomposition inside the IF will accept a "guard" condition, as either an $if
or an $else
:
$mock cust.addToCart
=> inventory.check
=> $if (result=="ok") cart.addItem
=> $if (result!="ok") (error="Not enough items")
=> $else (message="this branch will weirdly trigger when adding items - read the preceeding IFs again...")
The $else
applies to the last preceeding IF - in the example above the $else
will be activated when the result is ok...
You can add various flags to the rules, liek so:
$when <fallback> home.guest_arrived $if (name=="Jane") => chimes.welcome(name="Jane")
The supported flags now are:
fallback
- see Falback rule belowexclusive
- see Exclusive rule belowbefore
- this rule will be invoked before other matches. Useful forafter
- this rule will be invoked after other matchestrace
- the result of this rule will be shown only in trace modedebug
- the result of this rule will be shown only in debug modeA note on before
/after
: you cannot combine these with exclusive
.
Special archetypes can also control message visibility, i.e.:
$msg <public> home.guest_arrived
This declares a message which is public, i.e. it can be invoked directly by anyone even if the default auth level is say "member or trusted". The second one below only applies only for a specific client (to which the authentication token was created for).
$when <role.admin> diesel.rest(path ~path "/admin/etc)
=> ...
$when <role.admin,client.myUI> diesel.rest(path ~path "/adminspecial/etc)
=> ...
This rule is only available to users with the role specified. Roles are defined in several ways:
With $val
you can define values - note that these are actually variables, their value can be changed.
$val name = "gigi"
Also note that JSON is naturally supported, so you could define constants and enumerations:
$val TYPES = {
INT: "int",
STR: "str"
}
A fallback rule will be applied if no other rule with the same entity.method combination is applicable to the message. This is useful to have a "default" version of a rule apply.
For isntance, if you call the rule below with 3
the fallback rule will not apply, since there already was a rule for that message applied already.
$when testdiesel.fback1(r156==3)
=> (r156=r156+2)
$when <fallback> testdiesel.fback1(r156)
=> (r156=r156+1)
If an exclusive rule is applicable to the current message / event, it will apply exclusively and no other rule of the same entity.method will apply.
before
and after
rulesThese are useful to protect for instance REST resources generically, i.e.
$when diesel.rest (path ~= "/prefix/resource1/:id)
$when diesel.rest (path ~= "/prefix/resource1/query)
$when diesel.rest (path ~= "/prefix/resource1/:id/somethign)
This can protect all access to the resource, so you don't have to do it every call:
$when <before> diesel.rest (path ~= "/prefix/resource1/.*)
=> if (diesel.user is undefined) diesel.flow.return (
diesel.http.response.status=401,
payload = "Not auth".
)
This is a special transformation that can be applied to a json document, to flatten it into a list of parameters that can be used to call a message:
$when something.hapenned ()
=> (j = {a:1, b:2})
=> something.else (j.asAttrs)
j
would be flattened and a
and b
would be parameters to the new message something.else
.
You can use indentation to simulate regular blocks:
$when testdiesel.else1(branch)
=> $if(branch) do.this
|=> testdiesel.else.if
|=> (res131="if")
=> $else do.this
|=> testdiesel.else.else
|=> (res131="else")
Note that do.this
does nothing, it's not a recognized message. You can use diesel.branch
to avoid an "unknown" tag.
Instead of spaces, you use |
to ident - as many as levels deep you want to go. Note the use of do.this
this is a message that does nothing, but it is needed to become the parent of the indented nodes underneath. You can use any message you want - but to avoid confusion, use a message that has no rules handling it.
Although not very rule-friendly, this may help avoid many rules that apply in only one case.
This is a sequence, with guards based on the results of the previous activities:
$when cust.addToCart
=> inventory.check
=> $if (result=="ok") cart.addItem
=> $if (result!="ok") (error="Not enough items")
$when cust.addToCart => (haha="nok")
$mock inventory.* => (result="ok")
The second when rule here is not part of the sequence and would run in parallel with the sequence.
There are a number of built-in functions, like sizeOf
and now
- see Expressions and pattern matching for details.
You can also embed Javascript logic as "functions":
$def my.func(p1,p2) {{
return p1+p2;
}}
You can call this by just raising a message with the same name:
$send my.func (p1,p2)
or by using a when to generate one:
$msg sample.msg2 (p1="a", p2="b")
$when sample.msg2 (p1,p2) => my.func (p1,p2)
This allows you do quickly embed logic, without super programming:
You can access only Java classes in a package with the "api." prefix.
You may or may not use the return
statement - for simple expressions it is not needed.
diesel
objectYou can use the predefined diesel
object:
ctx
func
You can easily interact with REST services via JSON, XML or simple text, see Snakking REST.
Currently all parms are strings.
JSON will be supported (map, array, string, number) and structure.
$ctx.sync
.
While storytelling is naturally synchronous, the execution is by default asynchronous, i.e. somewhat out of order.
So, for instance, this:
$when a.a => sub.load (name="John")
$when a.a => ctx.log (subId)
$mock sub.load => (subId="1234")
Will be resolved in the following sequence:
If you're looking for a sequence, use this version - it will do what you expected:
$when a.a
=> sub.load (name="John")
=> ctx.log (subId)
In this case, the executions are still asynchronous, but a.2 is started only when a.1 will finish, so in essence they run in sequence .
Even further, you can do stuff like:
$when a.a
=> sub.load (name="John")
=> $if (error != "") ctx.log (subId)
=> $if (error == "") ctx.log (error)
AHA, so it's parallel or concurrent execution! Not really! It is asynchronous, i.e. out of order.
Activities are actually executed in the same actor/thread, so they are ran one after another. Therefor, you have no concurrency issues. However, if their implementations are asynchronous (like snakking or I/O) then those run in parallel with all others.
...it can get tricky. You should remember that messages are assumed to be asynchronous microservices. So, sub.load
would take some time and a network call. If you do want to wait for it, you have the option... but if you don't, then you don't have to.
So, what if we wanted to simulate a callback?
$when a.a
=> sub.load (name="John")
=> call.back (subId, error)
This will launch sub.load
asynchronously and, when this particular call is completed, the call.back
is activated.
The $mock
and $when
are rules which are triggered only when matching the current message. Here is one example that matches any cust.addToCart
message:
$when cust.addToCart
=> $if (type == "residential") bill.now
=> $if (type == "commercial") bill.later
And
$when cust.addToCart (type == "residential") => bill.now
$when cust.addToCart (type == "commercial") => bill.later
Wherever a value is expected, you can enter a variety of expressions, see Expressions and pattern matching.
Stories are interpreted top to bottom. All $msg
are generated and each subsequent $expect
is ran AFTER the message finished.
Each message however is run asynchornously, so all messages by default are started in sequence.
You can control this with $ctx.storySync
which is the default - this will set the story telling mode to synchronous, so all messages are ran in sequence.
The opposite is $ctx.storyAsync
which will start all messages asynchronously. Note that unintuitively, after setting the story mode to async you can switch it back to sync, as the story itself is interpreted in sequence.
TBD - there is a watcher mode, when the stories are continuously checked against a live stream of messages.
$send subDb.create(name="Jane", address="12 Greenbriar, Aurora")
$expect (subId ~= "sub[0-9]*")
$expect (name=="Jane")
The two tests above will be applied whenever a message subDb.create
matching those parameters will be seen. This way, integration or unit tests can be tested all the time, even in production.
You need to log in to post a comment!