pub use super::overlay::Write;
use super::registry::Entry;
use super::{
BoundCall, BoxFuture, CallInfo, CallLayer, CallStart, Function, FunctionConfig, Outcome,
Registry,
};
use crate::base::any_rejected;
use crate::base::{
AuthoredPath, CancelToken, Change, Error, ErrorKind, ErrorRecord, Instant, Issue, ProgramId,
Result, StepKind, TaskIx, WorkflowIx,
};
use crate::expr::{CompileCx, ExprEngine, Path};
use crate::program::{Callee, CalleeAt, Checks, compile_callee, refused};
use crate::run::host::{Invoked, Layers, apply_writes, invoke, keep_records};
use crate::run::{Message, TaskIds};
use crate::state::{RunContext, Tuning};
use chrono::Utc;
use datavalue::OwnedDataValue;
use serde_json::Value;
use std::fmt;
use std::future::IntoFuture;
use std::sync::Arc;
const TEST: &str = "test";
pub fn call<F: Function>(f: F, input: Value) -> Call<F> {
Call {
f,
input,
message: None,
layers: Vec::new(),
exprs: None,
name: "function".to_owned(),
capture: true,
channel: None,
routing_bucket: None,
deadline: None,
cancel: None,
}
}
#[must_use = "a call does nothing until it is awaited"]
pub struct Call<F: Function> {
f: F,
input: Value,
message: Option<Message>,
layers: Vec<Arc<dyn CallLayer>>,
exprs: Option<ExprEngine>,
name: String,
capture: bool,
channel: Option<String>,
routing_bucket: Option<u8>,
deadline: Option<Instant>,
cancel: Option<CancelToken>,
}
impl<F: Function> Call<F> {
pub fn message(mut self, m: Message) -> Self {
self.message = Some(m);
self
}
pub fn layer<L: CallLayer>(mut self, l: L) -> Self {
self.layers.push(Arc::new(l));
self
}
pub fn exprs(mut self, e: ExprEngine) -> Self {
self.exprs = Some(e);
self
}
pub fn name(mut self, n: &str) -> Self {
n.clone_into(&mut self.name);
self
}
pub fn capture_changes(mut self, on: bool) -> Self {
self.capture = on;
self
}
pub fn channel(mut self, c: &str) -> Self {
self.channel = Some(c.to_owned());
self
}
pub fn routing_bucket(mut self, b: u8) -> Self {
self.routing_bucket = Some(b);
self
}
pub fn deadline(mut self, at: Instant) -> Self {
self.deadline = Some(at);
self
}
pub fn cancel(mut self, token: CancelToken) -> Self {
self.cancel = Some(token);
self
}
async fn run(self) -> Called {
let Self {
f,
input,
message,
layers,
exprs,
name,
capture,
channel,
routing_bucket,
deadline,
cancel,
} = self;
let mut message = message.unwrap_or_else(|| Message::new(OwnedDataValue::Null));
let mut registry = Registry::new(exprs.unwrap_or_default());
registry.register(&name, f);
let (bound, issues) = compile(®istry, &name, &input);
let Some(bound) = bound else {
return Called {
issues,
outcome: Err(Error::new(
ErrorKind::Compile,
format!("the config of `{name}` was rejected; `issues` says why"),
)),
writes: Vec::new(),
changes: Vec::new(),
errors: Vec::new(),
message,
};
};
let now = Utc::now();
let mut now_text = None;
let parts = message.parts();
let mut rc = RunContext::new(parts.context, Tuning::default());
let Invoked {
result,
overlay,
changes,
errors,
..
} = {
let reader = rc.reader();
let start = CallStart {
reader,
binding: None,
payload: parts.payload,
exprs: registry.exprs(),
info: CallInfo {
program: ProgramId::next(),
workflow: WorkflowIx::new(0),
task: TaskIx::new(0),
function: &name,
workflow_id: TEST,
task_id: TEST,
channel: channel.as_deref(),
routing_bucket,
loop_counter: None,
element_index: None,
},
deadline,
cancel: cancel.as_ref(),
capture,
};
let layers = Layers {
run: &layers,
registry: &[],
};
invoke(start, &*bound, layers, None, crate::run::timers_available()).await
};
let outcome = result.into_result();
let ok = outcome.is_ok();
let writes = if ok {
overlay.clone().into_writes()
} else {
Vec::new()
};
let test_id: Arc<str> = Arc::from(TEST);
let ids = TaskIds {
workflow: &test_id,
task: &test_id,
};
let errors_before = parts.errors.len();
keep_records(ids, now, &mut now_text, parts.errors, errors, None);
let errors = parts.errors[errors_before..].to_vec();
let changes = apply_writes(&mut rc, ok, overlay, changes);
rc.finish();
Called {
issues,
outcome,
writes,
changes,
errors,
message,
}
}
}
impl<F: Function> IntoFuture for Call<F> {
type Output = Called;
type IntoFuture = BoxFuture<'static, Called>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.run())
}
}
impl<F: Function> fmt::Debug for Call<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Call")
.field("name", &self.name)
.field("input", &self.input)
.field("layers", &self.layers.len())
.field("capture_changes", &self.capture)
.field("channel", &self.channel)
.field("deadline", &self.deadline)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Called {
pub issues: Vec<Issue>,
pub outcome: Result<Outcome>,
pub writes: Vec<(Path, Write)>,
pub changes: Vec<Change>,
pub errors: Vec<ErrorRecord>,
pub message: Message,
}
pub fn config<C: FunctionConfig>(input: Value) -> std::result::Result<C, Vec<Issue>> {
config_with(&ExprEngine::default(), input)
}
pub fn config_with<C: FunctionConfig>(
exprs: &ExprEngine,
input: Value,
) -> std::result::Result<C, Vec<Issue>> {
let mut issues = Vec::new();
let compiled = {
let mut cx = CompileCx::new(exprs, &mut issues);
let before = cx.rejected();
let compiled = C::compile(&input, &mut cx).filter(|_| cx.rejected() == before);
if compiled.is_none() && cx.rejected() == before {
refused(&mut cx, std::any::type_name::<C>());
}
compiled
};
compiled.ok_or(issues)
}
fn compile(
registry: &Registry,
name: &str,
input: &Value,
) -> (Option<Box<dyn BoundCall>>, Vec<Issue>) {
let entry = registry.entry(name);
let Some(entry @ Entry::Host(_)) = entry else {
unreachable!("`{name}` was registered as a host function");
};
let mut issues = Vec::new();
let at = CalleeAt {
name,
workflow_id: TEST,
step: (StepKind::Task, TEST),
input_at: &AuthoredPath::root(),
};
let mut checks = Checks::new(registry.exprs());
let callee = compile_callee(
registry.exprs(),
entry,
input,
&at,
&mut checks,
&mut issues,
);
let rejected = any_rejected(&issues);
let bound = match callee {
Some(Callee::Host(bound)) if !rejected => Some(bound),
_ => None,
};
(bound, issues)
}