flowrclib/compiler/
parser.rs

1#[cfg(feature = "debugger")]
2use std::collections::BTreeMap;
3
4use log::{debug, info, trace};
5use url::Url;
6
7use flowcore::deserializers::deserializer::get_deserializer;
8use flowcore::model::flow_definition::FlowDefinition;
9use flowcore::model::flow_manifest::Cargo;
10use flowcore::model::input::InputInitializer;
11use flowcore::model::metadata::MetaData;
12use flowcore::model::name::HasName;
13use flowcore::model::name::Name;
14use flowcore::model::process::Process;
15use flowcore::model::process::Process::FlowProcess;
16use flowcore::model::process::Process::FunctionProcess;
17use flowcore::model::route::Route;
18use flowcore::provider::Provider;
19
20use crate::errors::*;
21
22/// `LibType` describes what format the Flow Library is written in
23#[derive(PartialEq, Eq)]
24pub enum LibType {
25    /// `RustLib` indicates that the library is written in rust with a Cargo.toml to compile it natively
26    RustLib,
27}
28
29/// Load a `Flow` definition from a `Url`, recursively loading all sub-processes referenced.
30///
31/// The return is a `Result` containing the `Process`, or a `String` describing the error
32/// found while loading.
33///
34/// # Example
35/// ```
36/// use flowcore::provider::Provider;
37/// use flowcore::errors::Result;
38/// use std::env;
39/// use url::Url;
40/// use std::collections::BTreeMap;
41///
42/// // Clients need to provide a Provider of content for the loader as flowlibc is independent of
43/// // file systems and io.
44/// struct DummyProvider;
45///
46/// // A Provider must implement the `Provider` trait, with the methods to `resolve` a URL and to
47/// // `get` the contents for parsing.
48/// impl Provider for DummyProvider {
49///     fn resolve_url(&self, url: &Url, default_filename: &str, _ext: &[&str]) -> Result<(Url, Option<Url>)> {
50///        // Just fake the url resolution in this example
51///        Ok((url.clone(), None))
52///     }
53///
54///    fn get_contents(&self, url: &Url) -> Result<Vec<u8>> {
55///        // Return the simplest flow definition possible - ignoring the url passed in
56///        Ok("flow = \"test\"".as_bytes().to_owned())
57///     }
58/// }
59///
60/// // Create an instance of the `DummyProvider`
61/// let dummy_provider = DummyProvider{};
62///
63/// // load the flow from `url = file:///example.toml` using the `dummy_provider`
64/// flowrclib::compiler::parser::parse(&Url::parse("file:///example.toml").unwrap(), &dummy_provider)
65/// .unwrap();
66/// ```
67pub fn parse(
68    url: &Url,
69    provider: &dyn Provider,
70) -> Result<Process> {
71    parse_process(
72        &Route::default(),
73        &Name::default(),
74        0,
75        &mut 0,
76        url,
77        provider,
78        &BTreeMap::new(),
79        0,
80    )
81}
82
83#[allow(clippy::too_many_arguments)]
84fn parse_process(
85    parent_route: &Route,
86    alias: &Name,
87    parent_flow_id: usize,
88    flow_count: &mut usize,
89    url: &Url,
90    provider: &dyn Provider,
91    initializations: &BTreeMap<String, InputInitializer>,
92    level: usize,
93) -> Result<Process> {
94    let (resolved_url, reference) = provider
95        .resolve_url(url, "root", &["toml"])
96        .chain_err(|| format!("Could not resolve the url: '{url}'"))?;
97
98    let contents = provider
99        .get_contents(&resolved_url)
100        .chain_err(|| format!("Could not get contents of resolved url: '{resolved_url}'"))?;
101
102    if !alias.is_empty() {
103        info!("Loading process with alias = '{alias}'");
104    }
105
106    let content = String::from_utf8(contents).chain_err(|| "Could not read UTF8 contents")?;
107    let deserializer = get_deserializer::<Process>(&resolved_url)?;
108    debug!(
109        "Loading process from url = '{resolved_url}' with deserializer: '{}'", deserializer.name());
110    let mut process = deserializer
111        .deserialize(&content, Some(&resolved_url))
112        .chain_err(|| format!("Could not parse a valid flow process from '{url}'"))?;
113
114    match process {
115        FlowProcess(ref mut flow) => {
116            flow.config(
117                &resolved_url,
118                parent_route,
119                alias,
120                *flow_count,
121                initializations,
122            )?;
123            *flow_count += 1;
124            debug!("Deserialized the Flow, now parsing sub-processes");
125            parse_process_refs(
126                flow,
127                flow_count,
128                provider,
129                level,
130            )?;
131            flow.build_connections(level)?;
132        }
133        FunctionProcess(ref mut function) => {
134            function.config(
135                url,
136                &resolved_url,
137                parent_route,
138                alias,
139                parent_flow_id,
140                reference,
141                initializations,
142            )?;
143        }
144    }
145
146    Ok(process)
147}
148
149/// load library metadata from the given url using the provider.
150/// Currently it uses the `package` table of Cargo.toml as a source but it could
151/// easily use another file as along as it has the required fields to satisfy `MetaData` struct
152pub fn parse_metadata(url: &Url, provider: &dyn Provider) -> Result<(MetaData, LibType)> {
153    trace!("Loading Metadata");
154    let (resolved_url, _) = provider
155        .resolve_url(url, "Cargo", &["toml"])
156        .chain_err(|| format!("Could not resolve the url: '{url}'"))?;
157
158    if &resolved_url != url {
159        debug!("Source URL '{url}' resolved to: '{resolved_url}'");
160    }
161
162    let contents = provider
163        .get_contents(&resolved_url)
164        .chain_err(|| format!("Could not get contents of resolved url: '{resolved_url}'"))?;
165    let content = String::from_utf8(contents).chain_err(|| "Could not read UTF8 contents")?;
166
167    let deserializer = get_deserializer::<Cargo>(&resolved_url)?;
168
169    let cargo: Cargo = deserializer.deserialize(&content, Some(&resolved_url))?;
170
171    Ok((cargo.package, LibType::RustLib))
172}
173
174/*
175    Parse sub-processes from the process_refs in a flow
176*/
177fn parse_process_refs(
178    flow: &mut FlowDefinition,
179    flow_count: &mut usize,
180    provider: &dyn Provider,
181    level: usize,
182) -> Result<()> {
183    for process_ref in &mut flow.process_refs {
184        let subprocess_url = flow
185            .source_url
186            .join(&process_ref.source)
187            .map_err(|e| e.to_string())?;
188        let process = parse_process(
189            &flow.route,
190            process_ref.alias(),
191            flow.id,
192            flow_count,
193            &subprocess_url,
194            provider,
195            &process_ref.initializations,
196            level + 1,
197        )?;
198        process_ref.set_alias(process.name());
199
200        // runtime needs references to library functions to be able to load the implementations at load time
201        // library flow definitions are "compiled down" to just library function references at compile time.
202        if let FunctionProcess(function) = &process {
203            if let Some(lib_ref) = function.get_lib_reference() {
204                flow.lib_references.insert(lib_ref.clone());
205            }
206
207            if let Some(context_ref) = function.get_context_reference() {
208                flow.context_references.insert(context_ref.clone());
209            }
210        }
211
212        flow.subprocesses
213            .insert(process_ref.alias().to_owned(), process);
214    }
215
216    Ok(())
217}
218
219#[cfg(test)]
220mod test {
221    use url::Url;
222
223    use flowcore::deserializers::deserializer::get_deserializer;
224    use flowcore::model::flow_manifest::Cargo;
225    use flowcore::model::metadata::MetaData;
226
227    #[test]
228    fn deserialize_library() {
229        let cargo_toml = r#"[package]
230name = "Flow Standard Library"
231version = "0.11.0"
232authors = ["Andrew Mackenzie <andrew@mackenzie-serres.net>"]
233description = "The standard library for 'flow' programs compiled with the 'flowc' compiler"
234
235exclude = "../..""#;
236        let url = Url::parse("file:///fake.toml").expect("Could not parse URL");
237        let deserializer = get_deserializer::<Cargo>(&url).expect("Could not get deserializer");
238        let cargo: Cargo = deserializer
239            .deserialize(cargo_toml, Some(&url))
240            .expect("Could not deserialize");
241        let _: MetaData = cargo.package;
242    }
243}