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
48pub 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 #[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 #[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 #[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}