1
use crate::cli::input::CliArgs;
2

            
3
use crate::config_file::target::TargetAttribute;
4
use crate::domain::commands::arguments::Argument;
5
use crate::domain::target::TargetIdentifier;
6
use crate::project_model::modules::SystemModule;
7
use crate::project_model::sourceset::SourceFile;
8
use crate::project_model::target::TargetModel;
9
use crate::{
10
    config_file::{
11
        build::BuildAttribute,
12
        compiler::CompilerAttribute,
13
        modules::{ModuleImplementation, ModuleInterface, ModulesAttribute},
14
        project::ProjectAttribute,
15
        ZorkConfigFile,
16
    },
17
    project_model::{
18
        build::BuildModel,
19
        compiler::CompilerModel,
20
        modules::{
21
            ModuleImplementationModel, ModuleInterfaceModel, ModulePartitionModel, ModulesModel,
22
        },
23
        project::ProjectModel,
24
        sourceset::{GlobPattern, Source, SourceSet},
25
        ZorkModel,
26
    },
27
    utils,
28
};
29
use chrono::{DateTime, Utc};
30
use color_eyre::{eyre::eyre, Result};
31
use indexmap::IndexMap;
32
use std::borrow::Cow;
33
use std::path::{Path, PathBuf};
34
use walkdir::WalkDir;
35

            
36
use super::constants::dir_names;
37

            
38
/// Details about a found configuration file on the project
39
///
40
/// This is just a configuration file with a valid name found
41
/// at a valid path in some subdirectory
42
#[derive(Debug)]
43
pub struct ConfigFile {
44
    pub path: PathBuf,
45
    pub last_time_modified: DateTime<Utc>,
46
}
47

            
48
/// Checks for the existence of the `zork_<any>.toml` configuration files
49
/// present in the same directory when the binary is called, and
50
/// returns a collection of the ones found.
51
///
52
/// *base_path* - A parameter for receive an input via command line
53
/// parameter to indicate where the configuration files lives in
54
/// the client's project. Defaults to `.`
55
///
56
/// This function fails if there's no configuration file
57
/// (or isn't present in any directory of the project)
58
2
pub fn find_config_files(
59
    base_path: &Path, // TODO:(Workspaces) create the cfg arg to specifically receive where's located the
60
    // user's Zork config files if they're not in the root of the project
61
    // nor matches the tree structure from user's root cmd arg value
62
    filename_match: &Option<String>,
63
) -> Result<Vec<ConfigFile>> {
64
2
    log::debug!("Searching for Zork++ configuration files...");
65
2
    let mut files = vec![];
66

            
67
22
    for e in WalkDir::new(base_path)
68
        .max_depth(2) // TODO:(Workspaces) so, max_depth should be zero when the cfg arg is ready
69
        .into_iter()
70
20
        .filter_map(|e| e.ok())
71
    {
72
20
        let filename = e.file_name().to_str().unwrap();
73
20
        let file_match = filename_match
74
            .as_ref()
75
            .map(|fm| fm.as_str())
76
            .unwrap_or(filename);
77
20
        if e.metadata().unwrap().is_file()
78
10
            && filename.starts_with("zork")
79
2
            && filename.ends_with(".toml")
80
2
            && filename.contains(file_match)
81
        {
82
2
            files.push(ConfigFile {
83
2
                path: e.path().to_path_buf(),
84
2
                last_time_modified: DateTime::<Utc>::from(e.metadata()?.modified()?),
85
            })
86
        }
87
22
    }
88

            
89
2
    if files.is_empty() {
90
        Err(eyre!("No configuration files found for the project"))
91
    } else {
92
2
        Ok(files)
93
    }
94
2
}
95

            
96
7
pub fn build_model<'a>(
97
    config: ZorkConfigFile<'a>,
98
    cli_args: &'a CliArgs,
99
    absolute_project_root: &Path,
100
) -> Result<ZorkModel<'a>> {
101
7
    let proj_name = config.project.name;
102

            
103
7
    let project = assemble_project_model(config.project);
104
7
    let compiler = assemble_compiler_model(config.compiler, cli_args);
105
7
    let build = assemble_build_model(config.build, absolute_project_root);
106

            
107
14
    let code_root = PathBuf::from(absolute_project_root).join(
108
7
        project
109
            .code_root
110
            .as_ref()
111
            .map(|pr| pr.to_string())
112
            .unwrap_or_default(),
113
7
    );
114

            
115
7
    let modules = assemble_modules_model(config.modules, &code_root);
116
7
    let targets = assemble_targets_model(config.targets, proj_name, &code_root);
117

            
118
7
    Ok(ZorkModel {
119
        project,
120
        compiler,
121
        build,
122
        modules,
123
        targets,
124
    })
125
7
}
126

            
127
7
fn assemble_project_model(config: ProjectAttribute) -> ProjectModel {
128
7
    ProjectModel {
129
7
        name: Cow::Borrowed(config.name),
130
7
        authors: config
131
            .authors
132
            .as_ref()
133
7
            .map_or_else(Vec::default, |authors| {
134
7
                authors
135
                    .iter()
136
7
                    .map(|auth| Cow::Borrowed(*auth))
137
                    .collect::<Vec<_>>()
138
7
            }),
139
7
        compilation_db: config.compilation_db.unwrap_or_default(),
140
7
        code_root: config.code_root.map(Cow::Borrowed),
141
    }
142
7
}
143

            
144
7
fn assemble_compiler_model<'a>(
145
    config: CompilerAttribute<'a>,
146
    cli_args: &'a CliArgs,
147
) -> CompilerModel<'a> {
148
7
    let extra_args = config
149
        .extra_args
150
1
        .map(|args| args.into_iter().map(Argument::from).collect())
151
        .unwrap_or_default();
152

            
153
7
    CompilerModel {
154
7
        cpp_compiler: config.cpp_compiler.into(),
155
7
        driver_path: Cow::Borrowed(if let Some(driver_path) = cli_args.driver_path.as_ref() {
156
            driver_path
157
        } else {
158
7
            config.driver_path.unwrap_or_default()
159
        }),
160
7
        cpp_standard: config.cpp_standard.clone().into(),
161
13
        std_lib: config.std_lib.clone().map(|lib| lib.into()),
162
7
        std_lib_installed_dir: config
163
            .std_lib_installed_dir
164
            .map(|inst_dir| Cow::Borrowed(Path::new(inst_dir))),
165
        extra_args,
166
    }
167
7
}
168

            
169
7
fn assemble_build_model(config: Option<BuildAttribute>, project_root: &Path) -> BuildModel {
170
7
    let output_dir = config
171
        .as_ref()
172
7
        .and_then(|build| build.output_dir)
173
7
        .map(|out_dir| out_dir.strip_prefix("./").unwrap_or(out_dir))
174
        .unwrap_or(dir_names::DEFAULT_OUTPUT_DIR);
175

            
176
7
    BuildModel {
177
7
        output_dir: Path::new(project_root).join(output_dir),
178
    }
179
7
}
180

            
181
7
fn assemble_modules_model<'a>(
182
    config: Option<ModulesAttribute<'a>>,
183
    code_root: &Path,
184
) -> ModulesModel<'a> {
185
7
    let modules = config.unwrap_or_default();
186

            
187
7
    let base_ifcs_dir = modules
188
        .base_ifcs_dir
189
        .map(Path::new)
190
        .map(Cow::from)
191
        .unwrap_or_default();
192

            
193
14
    let interfaces = modules
194
        .interfaces
195
14
        .map(|ifcs| {
196
14
            ifcs.into_iter()
197
27
                .map(|m_ifc| -> ModuleInterfaceModel<'_> {
198
20
                    assemble_module_interface_model(m_ifc, &base_ifcs_dir, code_root)
199
20
                })
200
                .collect()
201
7
        })
202
        .unwrap_or_default();
203

            
204
7
    let base_impls_dir = modules
205
        .base_impls_dir
206
        .map(Path::new)
207
        .map(Cow::from)
208
        .unwrap_or_default();
209

            
210
14
    let implementations = modules
211
        .implementations
212
14
        .map(|impls| {
213
14
            impls
214
                .into_iter()
215
21
                .map(|m_impl| {
216
14
                    assemble_module_implementation_model(m_impl, &base_impls_dir, code_root)
217
14
                })
218
                .collect()
219
7
        })
220
        .unwrap_or_default();
221

            
222
7
    let sys_modules = modules
223
        .sys_modules
224
        .as_ref()
225
2
        .map_or_else(Default::default, |headers| {
226
2
            headers
227
                .iter()
228
4
                .map(|sys_header| SystemModule {
229
2
                    file_stem: Cow::from(*sys_header),
230
2
                    ..Default::default()
231
2
                })
232
                .collect()
233
2
        });
234

            
235
7
    ModulesModel {
236
        base_ifcs_dir,
237
        interfaces,
238
        base_impls_dir,
239
        implementations,
240
        sys_modules,
241
    }
242
7
}
243

            
244
20
fn assemble_module_interface_model<'a>(
245
    config: ModuleInterface<'a>,
246
    base_ifcs_dir_path: &Path,
247
    code_root: &Path,
248
) -> ModuleInterfaceModel<'a> {
249
20
    let file = config.file;
250

            
251
20
    let file_path = get_file_path(code_root, Some(base_ifcs_dir_path), file);
252

            
253
20
    let module_name = if let Some(mod_name) = config.module_name {
254
1
        Cow::Borrowed(mod_name)
255
    } else {
256
19
        Path::new(file)
257
            .file_stem()
258
            .unwrap_or_else(|| panic!("Found ill-formed file_stem data for: {file}"))
259
            .to_string_lossy()
260
    };
261
20
    let dependencies = config
262
        .dependencies
263
        .map(|deps| deps.into_iter().map(Cow::Borrowed).collect())
264
        .unwrap_or_default();
265
20
    let partition = config.partition.map(ModulePartitionModel::from);
266

            
267
20
    let file_details = utils::fs::get_file_details(&file_path).unwrap_or_else(|_| {
268
        panic!("An unexpected error happened getting the file details for {file_path:?}")
269
    });
270

            
271
20
    ModuleInterfaceModel {
272
20
        path: file_details.0,
273
20
        file_stem: Cow::from(file_details.1),
274
20
        extension: Cow::from(file_details.2),
275
20
        module_name,
276
        partition,
277
        dependencies,
278
    }
279
20
}
280

            
281
14
fn assemble_module_implementation_model<'a>(
282
    config: ModuleImplementation<'a>,
283
    base_impls_dir_path: &Path,
284
    code_root: &Path,
285
) -> ModuleImplementationModel<'a> {
286
14
    let file = config.file;
287

            
288
14
    let mut dependencies = config
289
        .dependencies
290
        .unwrap_or_default()
291
        .into_iter()
292
        .map(Cow::Borrowed)
293
        .collect::<Vec<Cow<str>>>();
294

            
295
14
    let file_path = get_file_path(code_root, Some(base_impls_dir_path), file);
296

            
297
14
    if dependencies.is_empty() {
298
1
        let last_dot_index = config.file.rfind('.');
299
1
        if let Some(idx) = last_dot_index {
300
1
            let implicit_dependency = config.file.split_at(idx);
301
1
            dependencies.push(Cow::Owned(implicit_dependency.0.to_owned()))
302
        } else {
303
            dependencies.push(Cow::Borrowed(config.file));
304
        }
305
    }
306

            
307
14
    let file_details = utils::fs::get_file_details(&file_path).unwrap_or_else(|_| {
308
        panic!("An unexpected error happened getting the file details for {file_path:?}")
309
    });
310

            
311
14
    ModuleImplementationModel {
312
14
        path: file_details.0,
313
14
        file_stem: Cow::Owned(file_details.1),
314
14
        extension: Cow::Owned(file_details.2),
315
14
        dependencies,
316
    }
317
14
}
318

            
319
7
fn assemble_targets_model<'a>(
320
    targets: IndexMap<&'a str, TargetAttribute<'a>>,
321
    project_name: &'a str,
322
    code_root: &Path,
323
) -> IndexMap<TargetIdentifier<'a>, TargetModel<'a>> {
324
14
    targets
325
        .into_iter()
326
21
        .map(|(k, v)| {
327
14
            (
328
14
                TargetIdentifier(Cow::Borrowed(k)),
329
14
                assemble_target_model(v, project_name, code_root),
330
            )
331
14
        })
332
        .collect()
333
7
}
334

            
335
14
fn assemble_target_model<'a>(
336
    target_config: TargetAttribute<'a>,
337
    project_name: &'a str,
338
    code_root: &Path,
339
) -> TargetModel<'a> {
340
14
    let sources = target_config
341
        .sources
342
        .into_iter()
343
        .map(Cow::Borrowed)
344
        .collect();
345

            
346
14
    let sources = get_sources_for_target(sources, code_root);
347

            
348
14
    let extra_args = target_config
349
        .extra_args
350
2
        .map(|args| args.iter().map(|arg| Argument::from(*arg)).collect())
351
        .unwrap_or_default();
352

            
353
14
    TargetModel {
354
14
        output_name: Cow::Borrowed(target_config.output_name.unwrap_or(project_name)),
355
        sources,
356
        extra_args,
357
14
        kind: target_config.kind.unwrap_or_default(),
358
        enabled_for_current_program_iteration: true, // NOTE: For now, it can only be manually
359
                                                     // disabled by cli args
360
    }
361
14
}
362

            
363
/// Utilery function to map all the source files declared on the [`ZorkConfigFile::targets`]
364
/// attribute to the domain model entity, including resolving any [`GlobPattern`] declared as
365
/// any file on the input collection
366
14
fn get_sources_for_target<'a>(srcs: Vec<Cow<str>>, code_root: &Path) -> SourceSet<'a> {
367
14
    let sources = srcs
368
        .iter()
369
14
        .map(|src| {
370
14
            let target_src = get_file_path(code_root, None, src.as_ref());
371
14
            if src.contains('*') {
372
12
                Source::Glob(GlobPattern(target_src))
373
            } else {
374
2
                Source::File(target_src)
375
            }
376
14
        })
377
14
        .flat_map(|source| {
378
14
            source
379
                .paths()
380
                .expect("Error getting the declared paths for the source files")
381
14
        })
382
6
        .map(|pb| {
383
6
            let file_details = utils::fs::get_file_details(&pb).unwrap_or_else(|_| {
384
                panic!("An unexpected error happened getting the file details for {pb:?}")
385
            });
386
6
            SourceFile {
387
6
                path: file_details.0,
388
6
                file_stem: Cow::Owned(file_details.1),
389
6
                extension: Cow::Owned(file_details.2),
390
            }
391
6
        })
392
        .collect();
393

            
394
14
    SourceSet::new(sources)
395
14
}
396

            
397
/// Helper to build the file path of a [`TranslationUnit`]
398
/// Parameter *reduction* is any intermediate path offered by configuration that lives after the
399
/// code root and before the file itself
400
48
fn get_file_path(code_root: &Path, reduction: Option<&Path>, declared_file_path: &str) -> PathBuf {
401
48
    let declared_file_path = Path::new(declared_file_path);
402
48
    if declared_file_path.is_absolute() {
403
        declared_file_path.to_owned()
404
48
    } else if let Some(reduction_path) = reduction {
405
34
        code_root.join(reduction_path).join(declared_file_path)
406
34
    } else {
407
14
        code_root.join(declared_file_path)
408
    }
409
48
}
410

            
411
#[cfg(test)]
412
mod test {
413
    use std::borrow::Cow;
414

            
415
    use crate::config_file;
416
    use crate::domain::target::TargetKind;
417
    use crate::utils::fs;
418
    use crate::{
419
        project_model::compiler::{CppCompiler, LanguageLevel, StdLib},
420
        utils,
421
    };
422
    use clap::Parser;
423

            
424
    use super::*;
425

            
426
    #[test]
427
2
    fn test_project_model_with_full_config() -> Result<()> {
428
        let config: ZorkConfigFile =
429
1
            config_file::zork_cfg_from_file(utils::constants::CONFIG_FILE_MOCK)?;
430
1
        let cli_args = CliArgs::parse_from(["", "-vv", "run"]);
431
1
        let abs_path_for_mock = fs::get_project_root_absolute_path(Path::new("."))?;
432
1
        let model = build_model(config, &cli_args, &abs_path_for_mock);
433

            
434
1
        let mut targets = IndexMap::new();
435
1
        targets.insert(
436
1
            TargetIdentifier::from("executable"),
437
1
            TargetModel {
438
1
                output_name: "zork".into(),
439
2
                sources: SourceSet::new(vec![SourceFile {
440
1
                    path: abs_path_for_mock.clone(),
441
1
                    file_stem: Cow::Borrowed("main"),
442
1
                    extension: Cow::Borrowed("cpp"),
443
                }]),
444
1
                extra_args: vec!["-Werr".into()],
445
1
                kind: TargetKind::Executable,
446
                enabled_for_current_program_iteration: true,
447
            },
448
1
        );
449
1
        targets.insert(
450
1
            TargetIdentifier::from("tests"),
451
1
            TargetModel {
452
1
                output_name: "zork_tests".into(),
453
2
                sources: SourceSet::new(vec![SourceFile {
454
1
                    path: abs_path_for_mock.clone(),
455
1
                    file_stem: Cow::Borrowed("tests_main"),
456
1
                    extension: Cow::Borrowed("cpp"),
457
                }]),
458
1
                extra_args: vec![],
459
1
                kind: TargetKind::Executable,
460
                enabled_for_current_program_iteration: true,
461
            },
462
1
        );
463

            
464
1
        let expected = ZorkModel {
465
1
            project: ProjectModel {
466
1
                name: "Zork++".into(),
467
1
                authors: vec!["zerodaycode.gz@gmail.com".into()],
468
                compilation_db: true,
469
1
                code_root: None,
470
            },
471
1
            compiler: CompilerModel {
472
1
                cpp_compiler: CppCompiler::CLANG,
473
1
                driver_path: Cow::Borrowed(""),
474
1
                cpp_standard: LanguageLevel::CPP2B,
475
1
                std_lib: Some(StdLib::LIBCPP),
476
1
                std_lib_installed_dir: None,
477
1
                extra_args: vec![Argument::from("-Wall")],
478
            },
479
1
            build: BuildModel {
480
1
                output_dir: abs_path_for_mock.clone(),
481
            },
482
1
            modules: ModulesModel {
483
1
                base_ifcs_dir: Cow::Borrowed(Path::new("ifcs")),
484
2
                interfaces: vec![
485
1
                    ModuleInterfaceModel {
486
1
                        path: abs_path_for_mock.join("ifcs"),
487
1
                        file_stem: Cow::Borrowed("maths"),
488
1
                        extension: Cow::Borrowed("cppm"),
489
1
                        module_name: "maths".into(),
490
1
                        partition: None,
491
1
                        dependencies: vec![],
492
                    },
493
1
                    ModuleInterfaceModel {
494
1
                        path: abs_path_for_mock.join("ifcs"),
495
1
                        file_stem: Cow::Borrowed("some_module"),
496
1
                        extension: Cow::Borrowed("cppm"),
497
1
                        module_name: "maths".into(),
498
1
                        partition: None,
499
1
                        dependencies: vec![],
500
                    },
501
                ],
502
1
                base_impls_dir: Cow::Borrowed(Path::new("srcs")),
503
2
                implementations: vec![
504
1
                    ModuleImplementationModel {
505
1
                        path: abs_path_for_mock.join("srcs"),
506
1
                        file_stem: Cow::from("maths"),
507
1
                        extension: Cow::from("cpp"),
508
1
                        dependencies: vec!["maths".into()],
509
                    },
510
1
                    ModuleImplementationModel {
511
1
                        path: abs_path_for_mock.join("srcs"),
512
1
                        file_stem: Cow::from("some_module_impl"),
513
1
                        extension: Cow::from("cpp"),
514
1
                        dependencies: vec!["iostream".into()],
515
                    },
516
                ],
517
2
                sys_modules: vec![SystemModule {
518
1
                    file_stem: Cow::Borrowed("iostream"),
519
1
                    ..Default::default()
520
                }],
521
            },
522
1
            targets,
523
1
        };
524

            
525
1
        assert_eq!(model.unwrap(), expected);
526

            
527
1
        Ok(())
528
2
    }
529
}