Skip to main content

sdf_imports/
lib.rs

1use std::path::Path;
2
3use anyhow::{anyhow, Result};
4use tracing::info;
5
6use sdf_common::constants::DEFAULT_PACKAGE_FILE;
7use sdf_metadata::wit::{
8    dataflow::{DataflowDefinition, DevConfig, Header, PackageImport},
9    package_interface::PackageDefinition,
10};
11use sdf_parser_host::host::HostParser;
12
13pub fn load_package_dev_configs(
14    dataflow: &mut DataflowDefinition,
15    parser: &mut HostParser,
16    pkg_dir: &Path,
17) -> Result<()> {
18    let package_configs = fetch_package_configs(
19        pkg_dir,
20        &mut dataflow.imports,
21        dataflow.dev.as_ref(),
22        true,
23        parser,
24    )?;
25
26    info!(?package_configs, "loaded package dev configs");
27
28    dataflow.packages = package_configs;
29
30    Ok(())
31}
32
33pub fn load_package_imports(
34    package: &mut PackageDefinition,
35    parser: &mut HostParser,
36    debug: bool,
37    path: &Path,
38) -> Result<Vec<PackageDefinition>> {
39    fetch_package_configs(
40        path,
41        &mut package.imports,
42        package.dev.as_ref(),
43        debug,
44        parser,
45    )
46}
47
48// crawl the dependency tree to find package configs.
49//
50// will copy the `path` variable from the dev overrides
51// into the import declarations as necessary, to find local imports when in dev mode
52pub fn fetch_package_configs(
53    pkg_dir: &Path,
54    imports: &mut [PackageImport],
55    dev_imports: Option<&DevConfig>,
56    debug: bool,
57    parser: &mut HostParser,
58) -> Result<Vec<PackageDefinition>> {
59    info!(pkg_dir=%pkg_dir.display(), "fetching package configs");
60
61    let mut res = Vec::new();
62    add_package_tree(&mut res, pkg_dir, imports, dev_imports, debug, parser)?;
63
64    Ok(res)
65}
66
67fn add_package_tree(
68    res: &mut Vec<PackageDefinition>,
69    pkg_dir: &Path,
70    imports: &mut [PackageImport],
71    dev_imports: Option<&DevConfig>,
72    debug: bool,
73    parser: &mut HostParser,
74) -> Result<()> {
75    info!(pkg_dir=%pkg_dir.display(), "adding package tree");
76    if debug {
77        if let Some(dev_config) = dev_imports {
78            info!(dev_config=?dev_config, "overriding with dev config");
79            set_dev_overrides(imports, &dev_config.imports);
80        }
81    }
82
83    for import in imports.iter() {
84        match &import.path {
85            Some(path) => {
86                let pkg_dir = pkg_dir.join(path);
87                let mut package = read_imported_package_config(&import.metadata, &pkg_dir, parser)?;
88
89                add_package_tree(
90                    res,
91                    &pkg_dir,
92                    &mut package.imports,
93                    package.dev.as_ref(),
94                    debug,
95                    parser,
96                )?;
97
98                res.push(package);
99            }
100            None => {
101                let not_found_msg = format!("Package {} not found on Hub.", import.metadata);
102
103                if let Some(dev_config) = dev_imports {
104                    if dev_override_path_for_package(&import.metadata, &dev_config.imports)
105                        .is_some()
106                    {
107                        return Err(anyhow!("{} {}", not_found_msg, unused_dev_override_msg()));
108                    }
109                }
110
111                if debug {
112                    return Err(anyhow!(
113                        "{} {}",
114                        not_found_msg,
115                        override_missing_in_dev_mode_msg()
116                    ));
117                } else {
118                    return Err(anyhow!(
119                        "{} {}",
120                        not_found_msg,
121                        dev_override_instruction_msg()
122                    ));
123                }
124            }
125        }
126    }
127
128    Ok(())
129}
130
131fn set_dev_overrides(imports: &mut [PackageImport], dev_imports: &[PackageImport]) {
132    for import in imports.iter_mut() {
133        if let Some(path) = dev_override_path_for_package(&import.metadata, dev_imports) {
134            import.path = Some(path.to_string());
135        }
136    }
137}
138
139fn dev_override_path_for_package<'a>(
140    package_metadata: &Header,
141    dev_imports: &'a [PackageImport],
142) -> Option<&'a str> {
143    for dev_import in dev_imports.iter() {
144        if package_metadata == &dev_import.metadata {
145            return dev_import.path.as_deref();
146        }
147    }
148
149    None
150}
151
152pub fn read_imported_package_config(
153    header: &Header,
154    package_dir: impl AsRef<Path>,
155    parser: &mut HostParser,
156) -> Result<PackageDefinition> {
157    let pkg_dir_path = package_dir.as_ref();
158    let package_path = pkg_dir_path.join(DEFAULT_PACKAGE_FILE);
159
160    info!(file=?package_path.display(), "reading package file");
161
162    let config_string = std::fs::read_to_string(&package_path).map_err(|_e| {
163        anyhow!(
164            "failed to load package config: expected to find package: {} at {}",
165            header,
166            package_path.display()
167        )
168    })?;
169
170    parser.parse_package(&config_string)
171}
172
173fn override_missing_in_dev_mode_msg() -> String {
174    [
175        "SDF is running in Dev mode.",
176        " Did you mean to import the package from the local filesystem?",
177        " If so, please specify a path in the development config",
178    ]
179    .concat()
180}
181
182fn dev_override_instruction_msg() -> String {
183    [
184        "If you would like to import the package from the local filesystem,",
185        " please specify a path in the development config and pass the `--dev` flag",
186    ]
187    .concat()
188}
189
190fn unused_dev_override_msg() -> String {
191    [
192        "An override path for this package was specified in the development config.",
193        " please pass `--dev` to use the local override of the package.",
194    ]
195    .concat()
196}
197
198#[cfg(test)]
199mod test {
200    use std::path::PathBuf;
201
202    use super::{fetch_package_configs, set_dev_overrides};
203
204    use sdf_metadata::wit::package_interface::{DevConfig, Header, PackageImport};
205    use sdf_parser_host::host::HostParser;
206
207    fn imports() -> Vec<PackageImport> {
208        vec![PackageImport {
209            metadata: Header {
210                namespace: "example".to_string(),
211                name: "bank-update".to_string(),
212                version: "0.1.0".to_string(),
213            },
214            path: None,
215            types: vec![],
216            states: vec![],
217            functions: vec![],
218        }]
219    }
220
221    #[test]
222    fn test_set_dev_overrides_sets_path() {
223        let mut imports = imports();
224
225        let dev_imports = vec![PackageImport {
226            metadata: Header {
227                namespace: "example".to_string(),
228                name: "bank-update".to_string(),
229                version: "0.1.0".to_string(),
230            },
231            path: Some("test/bank-update".to_string()),
232            types: vec![],
233            states: vec![],
234            functions: vec![],
235        }];
236
237        set_dev_overrides(&mut imports, &dev_imports);
238
239        assert_eq!(imports[0].path, Some("test/bank-update".to_string()));
240    }
241
242    #[fluvio_future::test]
243    async fn test_fetch_package_configs_fetches_dev_configs() {
244        let debug = true;
245        let mut imports = imports();
246
247        let dev_config = DevConfig {
248            converter: None,
249            imports: vec![PackageImport {
250                metadata: Header {
251                    namespace: "example".to_string(),
252                    name: "bank-update".to_string(),
253                    version: "0.1.0".to_string(),
254                },
255                path: Some("test/bank-update".to_string()),
256                types: vec![],
257                states: vec![],
258                functions: vec![],
259            }],
260            topics: vec![],
261        };
262
263        let pkg_dir = PathBuf::from(".");
264
265        let parser = &mut HostParser::new();
266        let res = fetch_package_configs(&pkg_dir, &mut imports, Some(&dev_config), debug, parser)
267            .expect("Failed to fetch package configs");
268
269        assert_eq!(res.len(), 2);
270        assert_eq!(res[1].meta.name, "bank-update");
271        assert_eq!(res[0].meta.name, "bank");
272    }
273
274    #[fluvio_future::test]
275    async fn test_fetch_package_configs_details_missing_local_config_file() {
276        let debug = true;
277        let mut imports = imports();
278
279        let dev_config = DevConfig {
280            converter: None,
281            imports: vec![PackageImport {
282                metadata: Header {
283                    namespace: "example".to_string(),
284                    name: "bank-update".to_string(),
285                    version: "0.1.0".to_string(),
286                },
287                path: Some("foobar-path".to_string()),
288                types: vec![],
289                states: vec![],
290                functions: vec![],
291            }],
292            topics: vec![],
293        };
294
295        let pkg_dir = PathBuf::from(".");
296        let parser = &mut HostParser::new();
297
298        let res = fetch_package_configs(&pkg_dir, &mut imports, Some(&dev_config), debug, parser)
299            .expect_err("should fail to read config file");
300
301        assert_eq!(
302            res.to_string(),
303            "failed to load package config: expected to find package: example/bank-update@0.1.0 at ./foobar-path/sdf-package.yaml"
304        )
305    }
306
307    // dev mode, no override set
308    #[fluvio_future::test]
309    async fn test_fetch_package_configs_suggests_defining_local_override_in_debug_mode() {
310        let debug = true;
311        let mut imports = imports();
312
313        let dev_config = DevConfig {
314            converter: None,
315            imports: vec![],
316            topics: vec![],
317        };
318
319        let pkg_dir = PathBuf::from(".");
320        let parser = &mut HostParser::new();
321        let res = fetch_package_configs(&pkg_dir, &mut imports, Some(&dev_config), debug, parser)
322            .expect_err("should fail to find package on hub");
323
324        assert_eq!(
325            res.to_string(),
326            [
327                "Package example/bank-update@0.1.0 not found on Hub. SDF is running in Dev mode.",
328                " Did you mean to import the package from the local filesystem?",
329                " If so, please specify a path in the development config"
330            ]
331            .concat()
332        )
333    }
334
335    // prod mode, no override set
336    #[fluvio_future::test]
337    async fn test_fetch_package_configs_explains_dev_override_when_hub_package_not_found() {
338        let debug = false;
339        let mut imports = imports();
340
341        let dev_config = DevConfig {
342            converter: None,
343            imports: vec![],
344            topics: vec![],
345        };
346
347        let pkg_dir = PathBuf::from(".");
348        let parser = &mut HostParser::new();
349        let res = fetch_package_configs(&pkg_dir, &mut imports, Some(&dev_config), debug, parser)
350            .expect_err("should fail to find package on hub");
351
352        assert_eq!(
353            res.to_string(),
354            [
355                "Package example/bank-update@0.1.0 not found on Hub.",
356                " If you would like to import the package from the local filesystem,",
357                " please specify a path in the development config and pass the `--dev` flag"
358            ]
359            .concat()
360        )
361    }
362
363    // prod mode, override found
364    #[fluvio_future::test]
365    async fn test_fetch_package_configs_suggests_using_existing_local_override() {
366        let debug = false;
367        let mut imports = imports();
368
369        let dev_config = DevConfig {
370            converter: None,
371            imports: vec![PackageImport {
372                metadata: Header {
373                    namespace: "example".to_string(),
374                    name: "bank-update".to_string(),
375                    version: "0.1.0".to_string(),
376                },
377                path: Some("foobar-path".to_string()),
378                types: vec![],
379                states: vec![],
380                functions: vec![],
381            }],
382            topics: vec![],
383        };
384
385        let pkg_dir = PathBuf::from(".");
386        let parser = &mut HostParser::new();
387        let res = fetch_package_configs(&pkg_dir, &mut imports, Some(&dev_config), debug, parser)
388            .expect_err("should fail to find package on hub");
389
390        assert_eq!(
391            res.to_string(),
392            [
393                "Package example/bank-update@0.1.0 not found on Hub.",
394                " An override path for this package was specified in the development config.",
395                " please pass `--dev` to use the local override of the package."
396            ]
397            .concat()
398        )
399    }
400}