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 nodetypes 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 graphs 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 nodetypes 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 using nodetypes.