flowrclib/generator/
generate.rs

1#[cfg(feature = "debugger")]
2use std::collections::BTreeMap;
3use std::fs::File;
4use std::io::Write;
5use std::path::{Path, PathBuf};
6
7use log::info;
8use url::Url;
9
10use flowcore::model::flow_definition::FlowDefinition;
11use flowcore::model::flow_manifest::{DEFAULT_MANIFEST_FILENAME, FlowManifest};
12use flowcore::model::function_definition::FunctionDefinition;
13use flowcore::model::input::Input;
14use flowcore::model::metadata::MetaData;
15#[cfg(feature = "debugger")]
16use flowcore::model::name::HasName;
17#[cfg(feature = "debugger")]
18use flowcore::model::route::HasRoute;
19use flowcore::model::runtime_function::RuntimeFunction;
20
21use crate::compiler::compile::CompilerTables;
22use crate::errors::*;
23
24/// Paths in the manifest are relative to the location of the manifest file, to make the file
25/// and associated files relocatable (and maybe packaged into a ZIP etc). So we use manifest_url
26/// as the location other file paths are made relative to.
27pub fn create_manifest(
28    flow: &FlowDefinition,
29    debug_symbols: bool,
30    manifest_url: &Url,
31    tables: &CompilerTables,
32    #[cfg(feature = "debugger")] source_urls: BTreeMap<String, Url>,
33) -> Result<FlowManifest> {
34    info!("Writing flow manifest to '{}'", manifest_url);
35
36    let mut manifest = FlowManifest::new(MetaData::from(flow));
37
38    // Generate run-time Function struct for each of the compile-time functions
39    for function in &tables.functions {
40        manifest.add_function(function_to_runtimefunction(
41            manifest_url,
42            function,
43            debug_symbols,
44        ).chain_err(|| "Could not convert function to runtime function")?);
45    }
46
47    manifest.set_lib_references(&tables.libs);
48    manifest.set_context_references(&tables.context_functions);
49    #[cfg(feature = "debugger")]
50    manifest.set_source_urls(source_urls);
51
52    Ok(manifest)
53}
54
55/// Generate a manifest for the flow in JSON that can be used to execute it
56// TODO this is tied to being a file:// - generalize this to write to a URL, moving the code
57// TODO into the provider and implementing for file and http
58pub fn write_flow_manifest(
59    flow: FlowDefinition,
60    debug_symbols: bool,
61    destination: &Path,
62    tables: &CompilerTables,
63    #[cfg(feature = "debugger")] source_urls: BTreeMap<String, Url>,
64) -> Result<PathBuf> {
65    info!("\n==== Generating Manifest");
66
67    let mut filename = destination.to_path_buf();
68    filename.push(DEFAULT_MANIFEST_FILENAME);
69    filename.set_extension("json");
70    let mut manifest_file =
71        File::create(&filename).chain_err(|| "Could not create manifest file")?;
72    let manifest_url =
73        Url::from_file_path(&filename).map_err(|_| "Could not parse Url from file path")?;
74    let manifest = create_manifest(
75        &flow,
76        debug_symbols,
77        &manifest_url,
78        tables,
79        #[cfg(feature = "debugger")] source_urls,
80    )
81    .chain_err(|| "Could not create manifest from parsed flow and compiler tables")?;
82
83    manifest_file
84        .write_all(
85            serde_json::to_string_pretty(&manifest)
86                .chain_err(|| "Could not pretty format the manifest JSON contents")?
87                .as_bytes(),
88        )
89        .chain_err(|| "Could not write manifest data bytes to created manifest file")?;
90
91    Ok(filename)
92}
93
94/*
95    Create a run-time function struct from a compile-time function struct.
96    manifest_dir is the directory that paths will be made relative to.
97*/
98fn function_to_runtimefunction(
99    manifest_url: &Url,
100    function: &FunctionDefinition,
101    debug_symbols: bool,
102) -> Result<RuntimeFunction> {
103    #[cfg(feature = "debugger")]
104    let name = if debug_symbols {
105        function.alias().to_string()
106    } else {
107        "".to_string()
108    };
109
110    #[cfg(feature = "debugger")]
111    let route = if debug_symbols {
112        function.route().to_string()
113    } else {
114        "".to_string()
115    };
116
117    // make the location of implementation relative to the output directory if it is under it
118    let implementation_location = implementation_location_relative(function, manifest_url)
119        .chain_err(|| "Could not create Url for relative implementation location")?;
120
121    let mut runtime_inputs = vec![];
122    for input in function.get_inputs() {
123        runtime_inputs.push(Input::from(input));
124    }
125
126    Ok(RuntimeFunction::new(
127        #[cfg(feature = "debugger")]
128        name,
129        #[cfg(feature = "debugger")]
130        route,
131        implementation_location,
132        runtime_inputs,
133        function.get_id(),
134        function.get_flow_id(),
135        function.get_output_connections(),
136        debug_symbols,
137    ))
138}
139
140/*
141    Get the location of the implementation - relative to the Manifest if it is a provided implementation
142*/
143fn implementation_location_relative(function: &FunctionDefinition, manifest_url: &Url) -> Result<String> {
144    if let Some(ref lib_reference) = function.get_lib_reference() {
145        Ok(lib_reference.to_string())
146    } else if let Some(ref context_reference) = function.get_context_reference() {
147        Ok(context_reference.to_string())
148    } else {
149        let implementation_path = function.get_implementation();
150        let implementation_url = Url::from_file_path(implementation_path)
151            .map_err(|_| { format!("Could not create Url from file path: {implementation_path}") })?
152            .to_string();
153
154        let mut manifest_base_url = manifest_url.clone();
155        manifest_base_url
156            .path_segments_mut()
157            .map_err(|_| "cannot be base")?
158            .pop();
159
160        info!("Manifest base = '{}'", manifest_base_url.to_string());
161        info!("Absolute implementation path = '{implementation_path}'");
162        let relative_path =
163            implementation_url.replace(&format!("{}/", manifest_base_url.as_str()), "");
164        info!("Relative implementation path = '{}'", relative_path);
165        Ok(relative_path)
166    }
167}
168
169#[cfg(test)]
170mod test {
171    use serde_json::json;
172    use url::Url;
173
174    use flowcore::model::datatype::{ARRAY_TYPE, GENERIC_TYPE, STRING_TYPE};
175    use flowcore::model::function_definition::FunctionDefinition;
176    use flowcore::model::input::InputInitializer;
177    use flowcore::model::io::IO;
178    use flowcore::model::name::Name;
179    use flowcore::model::output_connection::{OutputConnection, Source};
180    use flowcore::model::output_connection::Source::Output;
181    use flowcore::model::route::Route;
182
183    use super::function_to_runtimefunction;
184
185    #[test]
186    fn function_with_sub_route_output_generation() {
187        let function = FunctionDefinition::new(
188            Name::from("Stdout"),
189            false,
190            "context://stdio/stdout".to_string(),
191            Name::from("print"),
192            vec![],
193            vec![
194                IO::new(vec!(GENERIC_TYPE.into()), Route::default()),
195                IO::new(vec!(STRING_TYPE.into()), Route::default()),
196            ],
197            Url::parse("file:///fake/file").expect("Could not parse Url"),
198            Route::from("/flow0/stdout"),
199            None,
200            Some(Url::parse("context://stdio/stdout").expect("Could not parse Url")),
201            vec![
202                OutputConnection::new(
203                    Source::default(),
204                    1,
205                    0,
206                    0,
207                    String::default(),
208                    #[cfg(feature = "debugger")]
209                    String::default(),
210                ),
211                OutputConnection::new(
212                    Output("sub_route".into()),
213                    2,
214                    0,
215                    0,
216                    String::default(),
217                    #[cfg(feature = "debugger")]
218                    String::default(),
219                ),
220            ],
221            0,
222            0,
223        );
224
225        let expected = "{
226  'function_id': 0,
227  'flow_id': 0,
228  'implementation_location': 'context://stdio/stdout',
229  'output_connections': [
230    {
231      'destination_id': 1,
232      'destination_io_number': 0,
233      'destination_flow_id': 0
234    },
235    {
236      'source': {
237        'Output': 'sub_route'
238      },
239      'destination_id': 2,
240      'destination_io_number': 0,
241      'destination_flow_id': 0
242    }
243  ]
244}";
245
246        let br = Box::new(function) as Box<FunctionDefinition>;
247
248        let runtime_process = function_to_runtimefunction(
249            &Url::parse("file://test").expect("Couldn't parse test Url"),
250            &br,
251            false,
252        )
253        .expect("Could not convert compile time function to runtime function");
254
255        let serialized_process = serde_json::to_string_pretty(&runtime_process)
256            .expect("Could not convert function content to json");
257        assert_eq!(serialized_process, expected.replace('\'', "\""));
258    }
259
260    #[test]
261    fn function_generation() {
262        let function = FunctionDefinition::new(
263            Name::from("Stdout"),
264            false,
265            "context://stdio/stdout".to_string(),
266            Name::from("print"),
267            vec![],
268            vec![IO::new(vec!(STRING_TYPE.into()), Route::default())],
269            Url::parse("file:///fake/file").expect("Could not parse Url"),
270            Route::from("/flow0/stdout"),
271            None,
272            Some(Url::parse("context://stdio/stdout").expect("Could not parse Url")),
273            vec![OutputConnection::new(
274                Source::default(),
275                1,
276                0,
277                0,
278                String::default(),
279                #[cfg(feature = "debugger")]
280                String::default(),
281            )],
282            0,
283            0,
284        );
285
286        let expected = "{
287  'function_id': 0,
288  'flow_id': 0,
289  'implementation_location': 'context://stdio/stdout',
290  'output_connections': [
291    {
292      'destination_id': 1,
293      'destination_io_number': 0,
294      'destination_flow_id': 0
295    }
296  ]
297}";
298
299        let br = Box::new(function) as Box<FunctionDefinition>;
300
301        let process = function_to_runtimefunction(
302            &Url::parse("file://test").expect("Couldn't parse test Url"),
303            &br,
304            false,
305        )
306        .expect("Could not convert compile time function to runtime function");
307
308        let serialized_process = serde_json::to_string_pretty(&process)
309            .expect("Could not convert function content to json");
310        assert_eq!(serialized_process, expected.replace('\'', "\""));
311    }
312
313    #[test]
314    fn function_with_initialized_input_generation() {
315        let mut io = IO::new(vec!(STRING_TYPE.into()), Route::default());
316        io.set_initializer(Some(InputInitializer::Once(json!("Hello")))).expect("Could not set initializer");
317
318        let function = FunctionDefinition::new(
319            Name::from("Stdout"),
320            false,
321            "context://stdio/stdout".to_string(),
322            Name::from("print"),
323            vec![io],
324            vec![],
325            Url::parse("file:///fake/file").expect("Could not parse Url"),
326            Route::from("/flow0/stdout"),
327            None,
328            Some(Url::parse("context://stdio/stdout").expect("Could not parse Url")),
329            vec![],
330            0,
331            0,
332        );
333
334        let expected = "{
335  'function_id': 0,
336  'flow_id': 0,
337  'implementation_location': 'context://stdio/stdout',
338  'inputs': [
339    {
340      'initializer': {
341        'once': 'Hello'
342      }
343    }
344  ]
345}";
346
347        let br = Box::new(function) as Box<FunctionDefinition>;
348        let process = function_to_runtimefunction(
349            &Url::parse("file://test").expect("Couldn't parse test Url"),
350            &br,
351            false,
352        )
353        .expect("Could not convert compile time function to runtime function");
354
355        let serialized_process = serde_json::to_string_pretty(&process)
356            .expect("Could not convert function content to json");
357        assert_eq!(expected.replace('\'', "\""), serialized_process);
358    }
359
360    #[test]
361    fn function_with_constant_input_generation() {
362        let mut io = IO::new(vec!(STRING_TYPE.into()), Route::default());
363        io.set_initializer(Some(InputInitializer::Always(json!("Hello")))).expect("Could not set initializer");
364
365        let function = FunctionDefinition::new(
366            Name::from("Stdout"),
367            false,
368            "context://stdio/stdout".to_string(),
369            Name::from("print"),
370            vec![io],
371            vec![],
372            Url::parse("file:///fake/file").expect("Could not parse Url"),
373            Route::from("/flow0/stdout"),
374            None,
375            Some(Url::parse("context://stdio/stdout").expect("Could not parse Url")),
376            vec![],
377            0,
378            0,
379        );
380
381        let expected = "{
382  'function_id': 0,
383  'flow_id': 0,
384  'implementation_location': 'context://stdio/stdout',
385  'inputs': [
386    {
387      'initializer': {
388        'always': 'Hello'
389      }
390    }
391  ]
392}";
393
394        let br = Box::new(function) as Box<FunctionDefinition>;
395        let process = function_to_runtimefunction(
396            &Url::parse("file://test").expect("Couldn't parse test Url"),
397            &br,
398            false,
399        )
400        .expect("Could not convert compile time function to runtime function");
401
402        let serialized_process = serde_json::to_string_pretty(&process)
403            .expect("Could not convert function content to json");
404        assert_eq!(expected.replace('\'', "\""), serialized_process);
405    }
406
407    #[test]
408    fn function_with_array_input_generation() {
409        let io = IO::new(vec!("array/string".into()), Route::default());
410
411        let function = FunctionDefinition::new(
412            Name::from("Stdout"),
413            false,
414            "context://stdio/stdout".to_string(),
415            Name::from("print"),
416            vec![io],
417            vec![],
418            Url::parse("file:///fake/file").expect("Could not parse Url"),
419            Route::from("/flow0/stdout"),
420            None,
421            Some(Url::parse("context://stdio/stdout").expect("Could not parse Url")),
422            vec![],
423            0,
424            0,
425        );
426
427        let expected = "{
428  'function_id': 0,
429  'flow_id': 0,
430  'implementation_location': 'context://stdio/stdout',
431  'inputs': [
432    {
433      'array_order': 1
434    }
435  ]
436}";
437
438        let br = Box::new(function) as Box<FunctionDefinition>;
439        let process = function_to_runtimefunction(
440            &Url::parse("file://test").expect("Couldn't parse test Url"),
441            &br,
442            false,
443        )
444        .expect("Could not convert compile time function to runtime function");
445
446        let serialized_process = serde_json::to_string_pretty(&process)
447            .expect("Could not convert function content to json");
448        assert_eq!(serialized_process, expected.replace('\'', "\""));
449    }
450
451    fn test_function() -> FunctionDefinition {
452        FunctionDefinition::new(
453            Name::from("Stdout"),
454            false,
455            "context://stdio/stdout".to_string(),
456            Name::from("print"),
457            vec![],
458            vec![IO::new(vec!(STRING_TYPE.into()), Route::default())],
459            Url::parse("file:///fake/file").expect("Could not parse Url"),
460            Route::from("/flow0/stdout"),
461            None,
462            Some(Url::parse("context://stdio/stdout")
463                .expect("Could not parse Url")),
464            vec![OutputConnection::new(
465                Source::default(),
466                1,
467                0,
468                0,
469                String::default(),
470                #[cfg(feature = "debugger")]
471                String::default(),
472            )],
473            0,
474            0,
475        )
476    }
477
478    #[test]
479    fn function_to_code_with_debug_generation() {
480        let function = test_function();
481
482        #[cfg(feature = "debugger")]
483        let expected = "{
484  'name': 'print',
485  'route': '/flow0/stdout',
486  'function_id': 0,
487  'flow_id': 0,
488  'implementation_location': 'context://stdio/stdout',
489  'output_connections': [
490    {
491      'destination_id': 1,
492      'destination_io_number': 0,
493      'destination_flow_id': 0
494    }
495  ]
496}";
497        #[cfg(not(feature = "debugger"))]
498        let expected = "{
499  'function_id': 0,
500  'flow_id': 0,
501  'implementation_location': 'context://stdio/stdout',
502  'output_connections': [
503    {
504      'destination_id': 1,
505      'destination_io_number': 0,
506      'destination_flow_id': 0
507    }
508  ]
509}";
510        let br = Box::new(function) as Box<FunctionDefinition>;
511
512        let process = function_to_runtimefunction(
513            &Url::parse("file://test").expect("Couldn't parse test Url"),
514            &br,
515            true,
516        )
517        .expect("Could not convert compile time function to runtime function");
518
519        let serialized_process = serde_json::to_string_pretty(&process)
520            .expect("Could not convert function content to json");
521        assert_eq!(serialized_process, expected.replace('\'', "\""));
522    }
523
524    #[test]
525    fn function_with_array_element_output_generation() {
526        let function = FunctionDefinition::new(
527            Name::from("Stdout"),
528            false,
529            "context://stdio/stdout".to_string(),
530            Name::from("print"),
531            vec![],
532            vec![IO::new(vec!(ARRAY_TYPE.into()), Route::default())],
533            Url::parse("file:///fake/file").expect("Could not parse Url"),
534            Route::from("/flow0/stdout"),
535            None,
536            Some(Url::parse("context://stdio/stdout").expect("Could not parse Url")),
537            vec![OutputConnection::new(
538                Output("/0".into()),
539                1,
540                0,
541                0,
542                String::default(),
543                #[cfg(feature = "debugger")]
544                String::default(),
545            )],
546            0,
547            0,
548        );
549
550        let expected = "{
551  'function_id': 0,
552  'flow_id': 0,
553  'implementation_location': 'context://stdio/stdout',
554  'output_connections': [
555    {
556      'source': {
557        'Output': '/0'
558      },
559      'destination_id': 1,
560      'destination_io_number': 0,
561      'destination_flow_id': 0
562    }
563  ]
564}";
565
566        let br = Box::new(function) as Box<FunctionDefinition>;
567
568        let process = function_to_runtimefunction(
569            &Url::parse("file://test").expect("Couldn't parse test Url"),
570            &br,
571            false,
572        )
573        .expect("Could not convert compile time function to runtime function");
574
575        let serialized_process = serde_json::to_string_pretty(&process)
576            .expect("Could not convert function content to json");
577        assert_eq!(serialized_process, expected.replace('\'', "\""));
578    }
579}