use super::compile::{CompileOptions, CompiledWorkflow, Compiler};
use super::fingerprint::Fingerprint;
use crate::base::{AuthoredPath, ProgramId, StepKind, TaskIx, WorkflowIx};
use crate::def::Status;
use crate::function::Registry;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
impl Compiler {
pub fn link(&self, workflows: impl IntoIterator<Item = CompiledWorkflow>) -> Program {
let mut workflows: Vec<CompiledWorkflow> = workflows.into_iter().collect();
for w in &workflows {
assert!(
w.registry.ptr_eq(&self.registry),
"cannot link workflow `{}`: it was compiled under a different registry from \
this compiler's",
w.id
);
}
workflows.sort_by_key(|w| w.priority);
let count =
u32::try_from(workflows.len()).expect("a program holds at most u32::MAX workflows");
let mut infos = Vec::with_capacity(workflows.len());
let mut workflow_ids = Vec::with_capacity(workflows.len());
let mut tasks = Vec::new();
let mut task_ids = Vec::new();
let mut task_base = Vec::with_capacity(workflows.len());
let mut all = Vec::new();
let mut by_channel: HashMap<String, Vec<WorkflowIx>> = HashMap::new();
for (ix, w) in (0..count).map(WorkflowIx::new).zip(&workflows) {
task_base
.push(u32::try_from(tasks.len()).expect("a program holds at most u32::MAX tasks"));
workflow_ids.push(Arc::from(w.id.as_str()));
infos.push(WorkflowInfo {
ix,
id: w.id.clone(),
channel: w.channel.clone(),
priority: w.priority,
status: w.status,
version: w.version,
fingerprint: w.fingerprint,
});
for meta in &w.tasks {
let task = TaskIx::new(
u32::try_from(tasks.len()).expect("a program holds at most u32::MAX tasks"),
);
task_ids.push(Arc::from(meta.id.as_str()));
tasks.push(TaskInfo {
ix: task,
id: meta.id.clone(),
function: meta.function.clone(),
workflow: ix,
kind: meta.kind,
path: meta.path.clone(),
});
}
if w.status == Status::Active {
all.push(ix);
by_channel.entry(w.channel.clone()).or_default().push(ix);
}
}
Program {
inner: Arc::new(Inner {
id: ProgramId::next(),
registry: self.registry.clone(),
options: self.options.clone(),
workflows,
infos,
workflow_ids,
tasks,
task_ids,
task_base,
all,
by_channel,
}),
}
}
}
struct Inner {
id: ProgramId,
registry: Registry,
options: CompileOptions,
workflows: Vec<CompiledWorkflow>,
infos: Vec<WorkflowInfo>,
workflow_ids: Vec<Arc<str>>,
tasks: Vec<TaskInfo>,
task_ids: Vec<Arc<str>>,
task_base: Vec<u32>,
all: Vec<WorkflowIx>,
by_channel: HashMap<String, Vec<WorkflowIx>>,
}
#[derive(Clone)]
pub struct Program {
inner: Arc<Inner>,
}
impl Program {
pub fn id(&self) -> ProgramId {
self.inner.id
}
pub fn workflows(&self) -> &[WorkflowInfo] {
&self.inner.infos
}
pub fn tasks(&self) -> &[TaskInfo] {
&self.inner.tasks
}
pub fn task(&self, ix: TaskIx) -> &TaskInfo {
&self.inner.tasks[ix.index() as usize]
}
pub fn registry(&self) -> &Registry {
&self.inner.registry
}
pub(crate) fn compiled(&self, ix: WorkflowIx) -> &CompiledWorkflow {
&self.inner.workflows[ix.index() as usize]
}
pub(crate) fn active(&self, channel: Option<&str>) -> &[WorkflowIx] {
match channel {
None => &self.inner.all,
Some(c) => self.inner.by_channel.get(c).map_or(&[], Vec::as_slice),
}
}
pub(crate) fn task_base(&self, ix: WorkflowIx) -> u32 {
self.inner.task_base[ix.index() as usize]
}
pub(crate) fn ids(&self, ix: WorkflowIx) -> (&Arc<str>, &[Arc<str>]) {
let base = self.task_base(ix) as usize;
let count = self.compiled(ix).tasks.len();
(
&self.inner.workflow_ids[ix.index() as usize],
&self.inner.task_ids[base..base + count],
)
}
pub(crate) fn options(&self) -> &CompileOptions {
&self.inner.options
}
}
impl fmt::Debug for Program {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Program")
.field("id", &self.inner.id)
.field(
"workflows",
&self.inner.infos.iter().map(|w| &w.id).collect::<Vec<_>>(),
)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct WorkflowInfo {
pub ix: WorkflowIx,
pub id: String,
pub channel: String,
pub priority: u32,
pub status: Status,
pub version: u32,
pub fingerprint: Fingerprint,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct TaskInfo {
pub ix: TaskIx,
pub id: String,
pub function: String,
pub workflow: WorkflowIx,
pub kind: StepKind,
pub path: AuthoredPath,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::def::{ParseOptions, parse};
use serde_json::{Value, json};
fn workflow(id: &str, channel: &str, status: &str, tasks: usize) -> Value {
let tasks: Vec<Value> = (0..tasks)
.map(|i| json!({"id": format!("t{i}"), "function": {"name": "log", "input": {"message": "m"}}}))
.collect();
json!({"id": id, "channel": channel, "status": status, "tasks": tasks})
}
fn link(defs: &[Value]) -> Program {
let c = Registry::default().compiler(CompileOptions::default());
c.link(defs.iter().map(|d| {
let def = parse(d, ParseOptions::default()).def.expect("parses");
c.compile_one(&def).expect("compiles").workflow
}))
}
fn ixs(list: &[WorkflowIx]) -> Vec<u32> {
list.iter().map(|ix| ix.index()).collect()
}
#[test]
fn active_lists_split_by_channel_and_status() {
let program = link(&[
workflow("a", "a", "active", 1),
workflow("b", "b", "active", 1),
workflow("p", "a", "paused", 1),
workflow("d", "default", "active", 1),
workflow("x", "default", "archived", 1),
workflow("a2", "a", "active", 1),
]);
assert_eq!(program.workflows().len(), 6);
assert_eq!(ixs(program.active(Some("a"))), [0, 5]);
assert_eq!(ixs(program.active(Some("b"))), [1]);
assert_eq!(ixs(program.active(Some("default"))), [3]);
assert_eq!(ixs(program.active(None)), [0, 1, 3, 5]);
assert!(program.active(Some("zzz")).is_empty());
}
#[test]
fn tasks_of_inactive_workflows_still_have_indices() {
let program = link(&[
workflow("a", "x", "paused", 2),
workflow("b", "x", "active", 3),
workflow("e", "x", "active", 1),
]);
let bases: Vec<u32> = (0..3)
.map(|i| program.task_base(WorkflowIx::new(i)))
.collect();
assert_eq!(bases, [0, 2, 5]);
assert_eq!(program.tasks().len(), 6);
let b = WorkflowIx::new(1);
assert_eq!(program.compiled(b).id(), "b");
assert_eq!(program.task(TaskIx::new(program.task_base(b))).id, "t0");
assert_eq!(program.task(TaskIx::new(program.task_base(b))).workflow, b);
}
}