Attribute Macro conflagrate::nodetype
source · [−]#[nodetype]
Expand description
Defines a block of code to be associated with nodes of a certain type in a graph.
Each executable node in a graph is given a type
, which is a non-standard GraphViz attribute
that conflagrate associates with a function of the same name passed to the
nodetype
macro.
The input of a node is the output of the previous node in the graph (or the graph’s input), and the output of the node is the input of the next node in the graph (or the return value of the graph).
#[nodetype]
async fn SplitCommand(input: String) -> VecDeque<String> {
input.split_whitespace().map(String::from).collect::<VecDeque<String>>()
}
Branching Behavior
In a graph, when one node has more than one arrow following it pointing to different nodes, then the graph has “branched”. Branches in control flow graphs can have multiple, contradictory meanings. In some cases, we may use branches to show conditional execution: if some condition is satisfied, follow one branch, otherwise follow another. In other cases branching may represent parallel execution: at this point in the application, spawn N tasks and copy the data to each task.
To cover these possibilities, conflagrate supports tagging the nodes in a graph with a
branch
attribute to specify what the node’s branching behavior should be (see graph
:
Node Attributes). Some choices of branching behavior require a
nodetype
’s output to be structured a certain way.
Parallel
The default branching behavior is simply to spawn a separate task for each following node in parallel. The output from the branching node is cloned to each trailing node.
Parallel branching puts no constraints on the return type of the node, other than the usual requirement that each following node must accept exactly the same number and types as their input.
Matcher
If a node is given the branch=matcher
attribute in the graph definition, it is specified to
have the matcher
branching behavior, where conflagrate executes only one trailing node
determined by the output of the matcher node. This puts a constraint on the form of the
output of the node.
The return type of the node is required to be a 2-tuple of the form (String, T)
, where the
String first element is used for matching and the T
second element is passed to the next node
as its input. Edges tagged with the value
attribute (see
graph
: Edge Attributes) are matched using the attribute’s value,
and a matching edge determines the following node. If no matches are found, an edge without a
value
attribute is used as the default. If no default edge is provided, the graph will
terminate, using the matcher node’s output as its output.
#[nodetype]
async fn GetCommand(input: VecDeque<String>) -> (String, VecDeque<String>) {
let cmd = input.pop_front().unwrap_or(String::from(""));
(cmd, input)
}
Result Matcher
Like the matcher behavior, if a node is described with the branch=resultmatcher
attribute,
the choice of trailing nodes depends on the output of the node. In this case, the return
type must be a single Result<T, E>
. Edges marked with the value=ok
attribute will match against the Ok(T)
variant and receive T
as their input type, and edges
with value=err
will match Err(E)
and receive E
as their input type. Unlike
the regular matcher type, multiple trailing nodes can be labeled with either value=ok
or
value=err
on their edges, allowing for parallel execution as in the default parallel
branching behavior.
#[nodetype]
async fn GetCommand(input: VecDeque<String>) -> Result<(String, VecDeque<String>), String> {
match input.pop_front() {
Ok(cmd) => Ok((cmd, input)),
Err(e) => Err(format!("unable to get command from input: {}", e.to_string())),
}
}
Blocking Versus Non-Blocking
Conflagrate applications are built using tokio
, so nodetype
s are converted to async
functions. If a regular function is passed into the nodetype
macro, conflagrate assumes
it is blocking and spawns its codeblock in a separate thread. To avoid spawning extra
threads, use async fn
wherever possible.
Visibility (Public Versus Private)
To facilitate larger projects split into multiple modules, the run
and run_graph
methods
of graph
s are pub
. The arguments to the graph are the input arguments to
the first node, and the return value of the graph is the return value of the last possible node,
so consequently the nodetype
functions of those nodes must also be pub
.
Testing
Under the hood, the nodetype
function is converted to a struct implementing a trait that
provides the function with a uniform call signature so that when it’s used with the
graph
macro, the graph builder doesn’t need to know anything about the shape of
your function. This makes testing more difficult, so your original function is also provided as
a test
static method. The test
method has exactly the same call signature as the original
definition.
#[nodetype]
async fn BusinessLogic(value: u32) -> Result<String, String> {
match value {
0..=10 => Ok(String::from("good")),
_ => Err(String::from("too high!"))
}
}
#[cfg(test)]
mod tests {
#[test]
fn handles_good_values() {
assert_eq!(BusinessLogic::test(1), Ok(String::from("good")));
assert_eq!(BusinessLogic::test(10), Ok(String::from("good")));
}
#[test]
fn bails_on_bad_values() {
assert_eq!(BusinessLogic::test(100), Err(String::from("too high!")));
}
}
Examples
Hello World
A simple “hello world” graph with two nodes, get_name
and print_name
. These nodes have
the nodetype
s GetName
and PrintGreeting
, respectively. The graph starts at get_name
and then follows to print_name
. The GetName
nodetype
returns a String
, so the
nodetype
of the node that follows, PrintGreeting
for print_name
, must take as its input
just a String
(plus an dependencies, but in this case there are none).
#[nodetype]
pub fn GetName() -> String {
let mut name = String::new();
println!("Hello, what is your name?");
std::io::stdin().read_line(&mut name).unwrap();
name.truncate(name.len() - 1);
name
}
#[nodetype]
pub fn PrintGreeting(name: String) {
println!("Hello, {}!", name)
}
graph!{
digraph GreetingGraph {
get_name[type=GetName, start=true];
print_name[type=PrintGreeting];
get_name -> print_name;
}
}
fn main() {
GreetingGraph::run(());
}
See Also
dependency
– Macro for defining an external resource or data to be used as input by a node in addition to the output of the previous node.graph
– Macro for building an executable control flow graph usingnodetype
s.