
RagConnect
-- an example
Using The full example is available at https://git-st.inf.tu-dresden.de/jastadd/ragconnect-minimal.
Preparation
The following examples are inspired by the real test case read1write2 The idea is to have two nonterminals, where input information is received on one of them, and - after transformation - is sent out by both.
Let the following grammar be used:
A ::= <Input:String> /<OutputOnA:String>/ B* ;
B ::= /<OutputOnB:String>/ ;
To declare receiving and sending tokens, a dedicated DSL is used:
// endpoint definitions
receive A.Input ;
send A.OutputOnA ;
send B.OutputOnB using Transformation ;
// mapping definitions
Transformation maps String s to String {:
return s + "postfix";
:}
This defines A.Input
to receive updates, and the other two tokens to send their value, whenever it changes.
Additionally, a transformation will be applied on B.OutputOnB
before sending out its value.
Such mapping definitions can be defined for receiving tokens as well. In this case, they are applied before the value is set. If no mapping definition is given, or if the required type (depending on the communication protocol, see later) does not match, a "default mapping definition" is used to avoid boilerplate code converting from or to primitive types.
Furthermore, let the following attribute definitions be given:
syn String A.getOutputOnA() = "a" + getInput();
syn String B.getOutputOnB() = "b" + input();
inh String B.input();
eq A.getB().input() = getInput();
In other words, OutputOnA
depends on Input
of the same node, and OutputOnB
depends on Input
of its parent node.
Currently, those dependencies have to be explicitely written down. It is expected, that in future version, this won't be necessary anymore.
This happens also in the DSL (dependencies have to be named to uniquely identify them):
// dependency definitions
A.OutputOnA canDependOn A.Input as dependencyA ;
B.OutputOnB canDependOn A.Input as dependencyB ;
Using generated code
After specifying everything, code will be generated if setup properly. Let's create an AST in some driver code:
A a = new A();
// set some default value for input
a.setInput("");
B b1 = new B();
B b2 = new B();
a.addB(b1);
a.addB(b2);
Now, we have to set the dependencies as described earlier.
// a.OutputOnA -> a.Input
a.addDependencyA(a);
// b1.OutputOnB -> a.Input
b1.addDependencyB(a);
// b2.OutputOnB -> a.Input
b2.addDependencyB(a);
Finally, we can actually connect the tokens. Depending on the enabled protocols, different URI schemes are allowed. In this example, we use the default protocol: MQTT.
a.connectInput("mqtt://localhost/topic/for/input");
a.connectOutputOnA("mqtt://localhost/a/out", true);
b1.connectOutputOnB("mqtt://localhost/b1/out", true);
b2.connectOutputOnB("mqtt://localhost/b2/out", false);
The first parameter of those connect-methods is always an URI-like String, to identify the protocol to use, the server operating the protocol, and a path to identify the concrete token.
In case of MQTT, the server is the host running an MQTT broker, and the path is equal to the topic to publish or subscribe to.
Please note, that the first leading slash (/
) is removed for MQTT topics, e.g., for A.Input
the topic is actually topic/for/input
.
For sending endpoints, there is a second boolean parameter to specify whether the current value shall be send immediately after connecting.
Communication protocol characteristics
Protocol | URI scheme | Default port | Type for mapping definitions | Remarks |
---|---|---|---|---|
mqtt |
mqtt://<broker-host>[:port]/<topic> |
1883 | byte[] |
First leading slash not included in topic. |
rest |
rest://localhost[:port]/<path> |
4567 | String |
Host is always localhost . |
Compiler options
The compiler is JastAdd-compliant, i.e., it accepts all flags available for JastAdd, though there is no process how to chain pre-processors yet. Additional options are as follows.
Name | Required (Default) | Description |
---|---|---|
--rootNode |
Yes | Root node in the base grammar. |
--protocols |
No (mqtt ) |
Protocols to enable, currently available: mqtt, rest . |
--printYaml |
No (false) | Print out YAML instead of generating files. |
--verbose |
No (false) | Print more messages while compiling. |
--logReads |
No (false) | Enable logging for every received message. |
--logWrites |
No (false) | Enable logging for every sent message. |
--version |
No (false) | Print version info and exit (reused JastAdd option) |
--o |
No (. ) |
Output directory (reused JastAdd option) |
All files to be process have to be passed as arguments. Their type is decided by the file extension (ast
and relast
for input grammars, connect
and ragconnect
for RagConnect definitions file).
Remarks
When constructing the AST and connecting it, one should always set dependencies before connecting, especially if updates already arriving for receiving endpoints. Otherwise, updates might not be propagated after setting dependencies, if values are equal after applying transformations of mapping definitions.
As an example, when using the following grammar and definitions for RagConnect ...
A ::= <Input:int> /<Output:String>/ ;
receive A.Input using Round ;
send A.Output ;
A.Output canDependOn A.Input as dependency1 ;
Round maps float f to int {:
return Math.round(f);
:}
... connecting first could mean to store the first rounded value and not propagating this update, since no dependencies are set, and not propagating further updates leading to the same rounded value even after setting the dependencies.