Skip to main content

call

Function call 

Source
pub fn call<F: Function>(f: F, input: Value) -> Call<F>
Expand description

One call of f, with its config compiled from input as a workflow would compile it; .await runs it.

The builder’s setters are optional: an empty message, no layers, ExprEngine::default, the name "function", changes captured, and no channel, deadline or cancellation token.

§Panics

When awaited, wherever Registry::register panics: on a name it reserves, or on a spec the function’s config cannot honour.

use dataflow_rs::datavalue::OwnedDataValue;
use dataflow_rs::expr::Output;
use dataflow_rs::function::testing::{self, Write};
use dataflow_rs::function::{BoxFuture, CallLayer, FunctionConfig, Next};
use dataflow_rs::program::IssueCode;
use dataflow_rs::{Ctx, Expr, Function, Message, Outcome, Result};
use serde_json::json;
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

#[derive(FunctionConfig)]
struct DbRead {
    query: Expr<String>,
    #[output(default = "data.rows")]
    output: Output,
}

/// Answers each query from the rows it was built with, as the host's
/// database would.
struct MyFn {
    rows: BTreeMap<String, serde_json::Value>,
}

impl MyFn {
    fn new(rows: BTreeMap<String, serde_json::Value>) -> Self {
        Self { rows }
    }
}

impl Function for MyFn {
    type Config = DbRead;

    async fn call(&self, ctx: &mut Ctx<'_>, cfg: &DbRead) -> Result<Outcome> {
        let query = ctx.eval(&cfg.query)?;
        let Some(rows) = self.rows.get(&*query) else {
            return Ok(Outcome::Status(404));
        };
        ctx.emit(&cfg.output, rows.into())?;
        Ok(Outcome::Ok)
    }
}

/// Counts the calls it wraps.
struct Counted(Arc<AtomicUsize>);

impl CallLayer for Counted {
    fn call<'a>(&'a self, ctx: &'a mut Ctx<'_>, next: Next<'a>) -> BoxFuture<'a, Result<Outcome>> {
        self.0.fetch_add(1, Ordering::Relaxed);
        next.run(ctx)
    }
}

let deps = BTreeMap::from([(
    "select name from customers where id = 7".to_owned(),
    json!([{"name": "Ada"}]),
)]);
let calls = Arc::new(AtomicUsize::new(0));
let input = json!({
    "query": {"cat": ["select name from customers where id = ", {"var": "data.id"}]},
    "output": "data.rows",
});
let data = OwnedDataValue::from(&json!({"id": 7}));
let out = testing::call(MyFn::new(deps), input)
    .message(Message::from_parts(OwnedDataValue::Null, data, OwnedDataValue::Null))
    .layer(Counted(Arc::clone(&calls)))
    .await;

let rows = OwnedDataValue::from(&json!([{"name": "Ada"}]));
assert!(out.issues.is_empty());
assert_eq!(out.outcome.unwrap(), Outcome::Ok);
assert_eq!(out.writes, [("data.rows".parse().unwrap(), Write::Set(rows.clone()))]);
assert_eq!(out.changes.len(), 1);
assert_eq!(out.message.context().get(&"data.rows".parse().unwrap()), Some(&rows));
assert_eq!(calls.load(Ordering::Relaxed), 1);

// A config alone; `Err` carries every issue.
let cfg: DbRead = testing::config(json!({"query": "select 1"})).ok().unwrap();
assert_eq!(cfg.output.constant().unwrap().to_string(), "data.rows");
let issues = testing::config::<DbRead>(json!({"qeury": "select 1"})).err().unwrap();
let codes: Vec<IssueCode> = issues.iter().map(|i| i.code).collect();
assert_eq!(codes, [IssueCode::LikelyTypo, IssueCode::InputParse]);