Skip to main content

dataflow_rs/run/
builder.rs

1//! `EngineBuilder`: an engine from definitions, for a host that does not
2//! quarantine.
3
4use super::engine::{Engine, EngineConfig};
5use crate::base::Issue;
6use crate::def::{ParseOptions, Parsed, WorkflowDef, parse, parse_owned};
7use crate::function::{CallLayer, Function, Registry};
8use crate::program::CompileOptions;
9use serde_json::Value;
10use std::fmt;
11
12/// A definition given to the builder: parsed, with the issues its parse
13/// reported, which a rejection keeps, or refused by `parse` with its issues.
14enum Pending {
15    Parsed(Box<WorkflowDef>, Vec<Issue>),
16    Refused(Vec<Issue>),
17}
18
19/// Builds an [`Engine`] from definitions: functions registered on a
20/// [`Registry`], the workflows, and the config. [`build`](Self::build) fails
21/// if any workflow is rejected, with every rejected definition's issues.
22///
23/// A host that quarantines, and runs the workflows that compile, uses a
24/// [`Compiler`](crate::program::Compiler) instead.
25///
26/// ```
27/// use dataflow_rs::Engine;
28/// use dataflow_rs::program::IssueCode;
29/// use serde_json::json;
30///
31/// let issues = Engine::builder()
32///     .workflow_json(&json!({"id": "w", "tasks": [
33///         {"id": "t", "function": {"name": "fetch"}}
34///     ]}))
35///     .build()
36///     .unwrap_err();
37/// assert_eq!(issues[0].code, IssueCode::UnknownFunction);
38/// ```
39pub struct EngineBuilder {
40    registry: Registry,
41    options: CompileOptions,
42    config: EngineConfig,
43    pending: Vec<Pending>,
44}
45
46impl EngineBuilder {
47    /// A builder over `Registry::default()`, with the default options and
48    /// config and no workflows.
49    pub(crate) fn new() -> Self {
50        Self {
51            registry: Registry::default(),
52            options: CompileOptions::default(),
53            config: EngineConfig::default(),
54            pending: Vec::new(),
55        }
56    }
57
58    /// Replaces the registry, and every function registered so far with it.
59    #[must_use]
60    pub fn registry(mut self, registry: Registry) -> Self {
61        self.registry = registry;
62        self
63    }
64
65    /// Registers `f` under `name` on the registry, as
66    /// [`Registry::register`], whose panics it shares.
67    #[must_use]
68    pub fn register<F: Function>(mut self, name: &str, f: F) -> Self {
69        self.registry.register(name, f);
70        self
71    }
72
73    /// Adds a layer around every host call on the registry, as
74    /// [`Registry::layer`]: the first added is the outermost.
75    #[must_use]
76    pub fn layer<L: CallLayer>(mut self, l: L) -> Self {
77        self.registry.layer(l);
78        self
79    }
80
81    /// Sets the options the workflows compile with.
82    #[must_use]
83    pub fn options(mut self, options: CompileOptions) -> Self {
84        self.options = options;
85        self
86    }
87
88    /// Sets the engine's config.
89    #[must_use]
90    pub fn config(mut self, config: EngineConfig) -> Self {
91        self.config = config;
92        self
93    }
94
95    /// Adds a parsed definition.
96    #[must_use]
97    pub fn workflow(mut self, def: WorkflowDef) -> Self {
98        self.pending
99            .push(Pending::Parsed(Box::new(def), Vec::new()));
100        self
101    }
102
103    /// Adds a definition to parse with [`ParseOptions::default`], the compat
104    /// reader on. One `parse` refuses makes [`build`](Self::build) fail with
105    /// its parse issues, and one it reads but the compile rejects with its
106    /// parse issues, then its compile issues.
107    #[must_use]
108    pub fn workflow_json(self, raw: &Value) -> Self {
109        self.parsed(parse(raw, ParseOptions::default()))
110    }
111
112    /// Adds an owned definition, avoiding the compatibility reader's copy.
113    /// Otherwise identical to [`Self::workflow_json`].
114    #[must_use]
115    pub fn workflow_json_owned(self, raw: Value) -> Self {
116        self.parsed(parse_owned(raw, ParseOptions::default()))
117    }
118
119    fn parsed(mut self, parsed: Parsed) -> Self {
120        self.pending.push(match parsed.def {
121            Some(def) => Pending::Parsed(Box::new(def), parsed.issues),
122            None => Pending::Refused(parsed.issues),
123        });
124        self
125    }
126
127    /// Compiles every definition and links them into the engine's program.
128    ///
129    /// # Errors
130    ///
131    /// Every issue of every definition that `parse` or
132    /// [`compile_one`](crate::program::Compiler::compile_one) rejected, in
133    /// input order, a rejected definition's parse issues before its
134    /// compile issues. On success the other issues are dropped: a host that
135    /// wants them calls [`Compiler::check`](crate::program::Compiler::check).
136    pub fn build(self) -> std::result::Result<Engine, Vec<Issue>> {
137        let compiler = self.registry.compiler(self.options);
138        let mut workflows = Vec::with_capacity(self.pending.len());
139        let mut rejected = Vec::new();
140        for pending in self.pending {
141            match pending {
142                Pending::Parsed(def, mut parsed) => match compiler.compile_one(&def) {
143                    Ok(compiled) => workflows.push(compiled.workflow),
144                    Err(mut issues) => {
145                        rejected.append(&mut parsed);
146                        rejected.append(&mut issues);
147                    }
148                },
149                Pending::Refused(mut issues) => rejected.append(&mut issues),
150            }
151        }
152        if !rejected.is_empty() {
153            return Err(rejected);
154        }
155        Ok(Engine::new(compiler.link(workflows), self.config))
156    }
157}
158
159impl fmt::Debug for EngineBuilder {
160    /// The registry, the options, the config and how many definitions it
161    /// holds.
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        f.debug_struct("EngineBuilder")
164            .field("registry", &self.registry)
165            .field("options", &self.options)
166            .field("config", &self.config)
167            .field("workflows", &self.pending.len())
168            .finish()
169    }
170}