Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Weft The book

Your first node

Here is Text, one of the smallest nodes in the catalog, with the lines that hook up its tests left out. Text takes a string you typed and sends it on.

catalog/basic/text/metadata.json:

{
  "type": "Text",
  "label": "Text",
  "description": "Emit a literal string (a prompt, a label, a config value).",
  "tags": ["literal", "string"],
  "icon": "Type",
  "color": "#64748b",
  "inputs": [
    { "name": "value", "type": "String", "widget": { "kind": "textarea" },
      "required": true, "label": "Value" }
  ],
  "outputs": [ { "name": "value", "type": "String" } ],
  "features": { "pure": true }
}

catalog/basic/text/mod.rs:

//! Text: emit a literal string configured at design time.

use async_trait::async_trait;

use weft::{RunContext, Node, NodeManifest, WeftResult};
use weft::node::NodeOutput;

#[derive(NodeManifest)]
pub struct TextNode;

#[async_trait]
impl Node for TextNode {
    async fn run(&self, ctx: RunContext) -> WeftResult<()> {
        let value: String = ctx.inputs.get("value")?;
        ctx.pulse_downstream(NodeOutput::new().set("value", value)).await
    }
}

Read an input, send an output. That is the shape of every node.

Write one

Make a folder anywhere under your project’s nodes/ (except nodes/base_catalog/), or anywhere under src/, for example next to the .weft file that uses it. nodes/base_catalog/ holds the standard library, and it is a folder of nodes like any other: weft loads and builds it the same way, and you can delete it if you do not use it. The one thing particular to it is that weft catalog update replaces it with the installed weft’s copy, so anything of yours in there would be lost.

nodes/word_count/
  metadata.json
  mod.rs

metadata.json:

{
  "type": "WordCount",
  "label": "Count words",
  "description": "Count the words in a piece of text.",
  "tags": ["text"],
  "inputs": [
    { "name": "text", "type": "String", "required": true, "label": "Text" }
  ],
  "outputs": [
    { "name": "count", "type": "Number" },
    { "name": "longest", "type": "String" }
  ]
}

mod.rs:

//! Count the words in a piece of text, and find the longest one.

use async_trait::async_trait;

use weft::{RunContext, Node, NodeManifest, WeftResult};
use weft::node::NodeOutput;

#[derive(NodeManifest)]
pub struct WordCountNode;

#[async_trait]
impl Node for WordCountNode {
    async fn run(&self, ctx: RunContext) -> WeftResult<()> {
        let text: String = ctx.inputs.get("text")?;

        let words: Vec<&str> = text.split_whitespace().collect();
        let longest = words.iter().max_by_key(|w| w.len()).copied().unwrap_or("");

        ctx.pulse_downstream(
            NodeOutput::new()
                .set("count", words.len() as f64)
                .set("longest", longest)
        ).await
    }
}

Now use it. The type name from your metadata is the name you write:

words = WordCount { text: draft.answer }
show = Debug { data: words.count }

weft run compiles your node into the project’s worker program, on your machine with your own Rust, and runs it. If you have not installed Rust yet, go and read the install page.

Five things that would have bitten you

Do not check your inputs. No asserting that text is really a string, no unwrapping something you were already promised. Every wire was checked before anything ran. A wrong value fails before it reaches your code.

Do not use .get(...).unwrap_or(...). That turns a real type error into your default and you never find out. ctx.inputs.get_or("limit", 10) is the same shape and fails honestly.

Emit each port at most once. Several calls are fine if they touch different ports, which is how you release a value early and a done flag at the end. Touching one port twice is an error naming it. Anything you never mention is closed for you when the body returns, and that closure is what tells everything downstream to stop waiting.

Keep values small. A value over 100 KB fails the emit, naming the port. Bytes go in storage, and what travels the wire is the marker saying where they are. If a big answer is not a file, go and read emitting for how to send less of it.

If your code does heavy work, hand it to spawn_blocking. Your body shares a few threads, one per CPU, with the calls arriving at the worker and with its other runs, and a thread only changes hands at an .await. Parsing a big document, resizing an image or hashing a large file in one go keeps the thread until it is done, and everything waiting for that thread waits with it. On a worker with one CPU, that is everything. The worker keeps its hold on your run from a separate thread, so a slow body never loses the run; it only slows the rest down.

let rows = tokio::task::spawn_blocking(move || parse_csv(&text))
    .await
    .node_err("parsing the export")?;

Failing well

let body = response.text().await.node_err("reading the reply")?;

.node_err("...") wraps somebody else’s error with what you were doing. On an Option, the message is what the reader sees when it was None, so say what was missing.

weft::node_bail!("pick a destination: a channel or a user");

node_bail! is for something your own code worked out. Write the message as an instruction, because it is going in front of whoever is building the program.

Those two, plus ? on anything the ctx hands back, cover almost every failure. The exception is a mistake in the program that your code spots, like SQL that does not parse: return Err(WeftError::Input(..)) for that. For why, and for which failures a program can catch on _error (an output weft gives every node), go and read which failures a program can handle.

Then a test

#[cfg(feature = "node-tests")]
mod tests;

and in your impl Node:

#[cfg(feature = "node-tests")]
fn tests(&self) -> Vec<weft::NodeTest> {
    tests::tests()
}

tests.rs:

pub fn tests() -> Vec<NodeTest> {
    vec![NodeTest::fake("counts_words", counts_words)]
}

async fn counts_words(rig: FakeRig) -> WeftResult<()> {
    let outcome = rig.run(&WordCountNode, json!({ "text": "one two three" })).await.ok()?;
    assert_eq!(outcome.outputs["count"], json!(3.0));
    assert_eq!(outcome.outputs["longest"], json!("three"));
    Ok(())
}
weft test-node WordCount

For the tiers, the rig and what a live test costs, go and read testing a node.