spade/
namespaced_file.rs

1use std::path::PathBuf;
2
3use logos::Logos;
4use spade_common::name::Path as SpadePath;
5use spade_parser::{lexer, Parser};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct NamespacedFile {
9    /// The namespace which is the root of this file, i.e. what is referred
10    /// to when when starting a path with lib::
11    pub base_namespace: SpadePath,
12    /// The namespace of the items added in this file.
13    pub namespace: SpadePath,
14    pub file: PathBuf,
15}
16
17/// Parses `a::b,a::b::c,test.spade` as `root_namespace: a::b, namespace: a::b::c, file: test.spade`
18/// if no , is present, attempts to parse a path and set root and namespace to vec![]
19pub fn namespaced_file(arg: &str) -> Result<NamespacedFile, String> {
20    let parts = arg.split(',').collect::<Vec<_>>();
21
22    match parts.len() {
23        0 => Err("Expected a string".to_string()),
24        1 => Ok(NamespacedFile {
25            file: arg.into(),
26            namespace: SpadePath(vec![]),
27            base_namespace: SpadePath(vec![]),
28        }),
29        3 => {
30            let root_namespace = if parts[0].is_empty() {
31                SpadePath(vec![])
32            } else {
33                let mut root_parser = Parser::new(lexer::TokenKind::lexer(parts[0]), 0);
34                root_parser
35                    .path()
36                    .map_err(|e| {
37                        format!(
38                            "\nwhen parsing '{}': {}",
39                            parts[0],
40                            e.labels.message.as_str()
41                        )
42                    })?
43                    .inner
44            };
45
46            let namespace = if parts[1].is_empty() {
47                SpadePath(vec![])
48            } else {
49                // NOTE: could be a bit smarter here and look for keywords manually
50                let mut namespace_parser = Parser::new(lexer::TokenKind::lexer(parts[1]), 0);
51                namespace_parser
52                    .path()
53                    .map_err(|e| {
54                        format!(
55                            "\nwhen parsing '{}': {}",
56                            parts[1],
57                            e.labels.message.as_str()
58                        )
59                    })?
60                    .inner
61            };
62
63            Ok(NamespacedFile {
64                base_namespace: root_namespace,
65                file: parts[2].into(),
66                namespace,
67            })
68        }
69        other => Err(format!(
70            "Expected filename or <root>,<namespace>,<filename>. Got string with {other} commas"
71        )),
72    }
73}
74
75pub fn dummy_file() -> NamespacedFile {
76    namespaced_file("a,a,a.spade").unwrap()
77}
78
79#[cfg(test)]
80mod tests {
81    use spade_common::{
82        location_info::WithLocation as _,
83        name::{Identifier, Path as SpadePath, PathSegment},
84    };
85
86    use crate::namespaced_file::NamespacedFile;
87
88    use super::namespaced_file;
89
90    #[test]
91    fn parsing_namespaced_file_works() {
92        assert_eq!(
93            namespaced_file("a,a::b,b.spade"),
94            Ok(NamespacedFile {
95                base_namespace: SpadePath(vec![PathSegment::Named(
96                    Identifier::intern("a").nowhere()
97                )]),
98                namespace: SpadePath(vec![
99                    PathSegment::Named(Identifier::intern("a").nowhere()),
100                    PathSegment::Named(Identifier::intern("b").nowhere())
101                ]),
102                file: "b.spade".into(),
103            })
104        );
105    }
106
107    #[test]
108    fn invalid_path_errors_without_panic() {
109        assert!(namespaced_file("lib,lib::pipeline,pipeline.spade").is_err());
110    }
111}