python_proto_importer/verification/
package_structure.rs

1use anyhow::Result;
2use std::path::{Path, PathBuf};
3
4/// Legacy package structure determination
5/// Simply uses parent as PYTHONPATH and out_name as package_name
6pub fn determine_package_structure_legacy(out_abs: &Path) -> Result<(PathBuf, String)> {
7    let out_name = out_abs
8        .file_name()
9        .and_then(|n| n.to_str())
10        .unwrap_or("generated");
11
12    tracing::debug!(
13        "determine_package_structure_legacy: using simple structure: PYTHONPATH={}, package_name={}",
14        out_abs
15            .parent()
16            .map(|p| p.display().to_string())
17            .unwrap_or_else(|| "<none>".to_string()),
18        out_name
19    );
20
21    let parent = out_abs.parent();
22    if let Some(parent_dir) = parent.filter(|p| p.exists()) {
23        return Ok((parent_dir.to_path_buf(), out_name.to_string()));
24    }
25
26    Ok((out_abs.to_path_buf(), String::new()))
27}
28
29/// Intelligent package structure determination
30/// Prefers PYTHONPATH to point at the directory which contains the "package root".
31/// If the parent of `out_abs` is a package (has __init__.py), use its parent as
32/// PYTHONPATH and set package_name to "{parent}.{out}". Otherwise use the parent
33/// as PYTHONPATH and package_name to `out`.
34pub fn determine_package_structure(out_abs: &Path) -> Result<(PathBuf, String)> {
35    let out_name = out_abs
36        .file_name()
37        .and_then(|n| n.to_str())
38        .unwrap_or("generated");
39
40    tracing::debug!(
41        "determine_package_structure: analyzing out_abs={}",
42        out_abs.display()
43    );
44    tracing::debug!("determine_package_structure: out_name={}", out_name);
45
46    if let Some(parent_dir) = out_abs.parent() {
47        tracing::debug!(
48            "determine_package_structure: parent_dir={}",
49            parent_dir.display()
50        );
51        if parent_dir.exists() {
52            let parent_init = parent_dir.join("__init__.py");
53            tracing::debug!(
54                "determine_package_structure: checking for parent_init={}",
55                parent_init.display()
56            );
57            if parent_init.exists() {
58                tracing::debug!(
59                    "determine_package_structure: parent is a package (has __init__.py)"
60                );
61                if let Some(grand) = parent_dir.parent() {
62                    tracing::debug!(
63                        "determine_package_structure: grandparent_dir={}",
64                        grand.display()
65                    );
66                    if grand.exists() {
67                        let parent_name = parent_dir
68                            .file_name()
69                            .and_then(|n| n.to_str())
70                            .unwrap_or("");
71                        let pkg = if parent_name.is_empty() {
72                            out_name.to_string()
73                        } else {
74                            format!("{}.{}", parent_name, out_name)
75                        };
76                        tracing::debug!(
77                            "determine_package_structure: using nested package structure: PYTHONPATH={}, package_name={}",
78                            grand.display(),
79                            pkg
80                        );
81                        return Ok((grand.to_path_buf(), pkg));
82                    } else {
83                        tracing::debug!(
84                            "determine_package_structure: grandparent does not exist, falling back to standard structure"
85                        );
86                    }
87                } else {
88                    tracing::debug!(
89                        "determine_package_structure: no grandparent, falling back to standard structure"
90                    );
91                }
92            } else {
93                tracing::debug!(
94                    "determine_package_structure: parent is not a package (no __init__.py)"
95                );
96            }
97            tracing::debug!(
98                "determine_package_structure: using standard structure: PYTHONPATH={}, package_name={}",
99                parent_dir.display(),
100                out_name
101            );
102            return Ok((parent_dir.to_path_buf(), out_name.to_string()));
103        } else {
104            tracing::debug!("determine_package_structure: parent directory does not exist");
105        }
106    } else {
107        tracing::debug!("determine_package_structure: no parent directory");
108    }
109
110    tracing::debug!(
111        "determine_package_structure: fallback to out_abs as PYTHONPATH: PYTHONPATH={}, package_name=empty",
112        out_abs.display()
113    );
114    Ok((out_abs.to_path_buf(), String::new()))
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use std::fs;
121    use std::io;
122    use tempfile::TempDir;
123
124    #[test]
125    fn test_determine_package_structure_legacy_with_parent() -> io::Result<()> {
126        let temp_dir = TempDir::new()?;
127        let parent_path = temp_dir.path();
128        let out_path = parent_path.join("my_package");
129        fs::create_dir(&out_path)?;
130
131        let result = determine_package_structure_legacy(&out_path).unwrap();
132
133        assert_eq!(result.0, parent_path);
134        assert_eq!(result.1, "my_package");
135
136        Ok(())
137    }
138
139    #[test]
140    fn test_determine_package_structure_legacy_no_parent() {
141        let root_path = PathBuf::from("/");
142        let result = determine_package_structure_legacy(&root_path).unwrap();
143
144        // For root path, there's no parent, so it falls back to using the path itself
145        assert_eq!(result.0, root_path);
146        assert_eq!(result.1, String::new());
147    }
148
149    #[test]
150    fn test_determine_package_structure_simple_case() -> io::Result<()> {
151        let temp_dir = TempDir::new()?;
152        let parent_path = temp_dir.path();
153        let out_path = parent_path.join("simple_package");
154        fs::create_dir(&out_path)?;
155
156        let result = determine_package_structure(&out_path).unwrap();
157
158        // Parent has no __init__.py, so should use standard structure
159        assert_eq!(result.0, parent_path);
160        assert_eq!(result.1, "simple_package");
161
162        Ok(())
163    }
164
165    #[test]
166    fn test_determine_package_structure_nested_package() -> io::Result<()> {
167        let temp_dir = TempDir::new()?;
168        let grandparent_path = temp_dir.path();
169        let parent_path = grandparent_path.join("parent_pkg");
170        let out_path = parent_path.join("child_pkg");
171
172        fs::create_dir_all(&out_path)?;
173        fs::write(parent_path.join("__init__.py"), "")?; // Make parent a package
174
175        let result = determine_package_structure(&out_path).unwrap();
176
177        // Parent has __init__.py, so should use nested structure
178        assert_eq!(result.0, grandparent_path);
179        assert_eq!(result.1, "parent_pkg.child_pkg");
180
181        Ok(())
182    }
183
184    #[test]
185    fn test_determine_package_structure_nested_package_no_grandparent() -> io::Result<()> {
186        let temp_dir = TempDir::new()?;
187        let parent_path = temp_dir.path().join("parent_pkg");
188        let out_path = parent_path.join("child_pkg");
189
190        fs::create_dir_all(&out_path)?;
191        fs::write(parent_path.join("__init__.py"), "")?;
192
193        let result = determine_package_structure(&out_path).unwrap();
194
195        // Grandparent exists (temp_dir), so should use nested structure
196        assert_eq!(result.0, temp_dir.path());
197        assert_eq!(result.1, "parent_pkg.child_pkg");
198
199        Ok(())
200    }
201
202    #[test]
203    fn test_determine_package_structure_fallback_to_self() {
204        let nonexistent_path = PathBuf::from("/nonexistent/path/package");
205
206        let result = determine_package_structure(&nonexistent_path).unwrap();
207
208        // No parent exists, should fallback to using path itself with empty package name
209        assert_eq!(result.0, nonexistent_path);
210        assert_eq!(result.1, String::new());
211    }
212
213    #[test]
214    fn test_determine_package_structure_empty_parent_name() -> io::Result<()> {
215        let temp_dir = TempDir::new()?;
216        let parent_path = temp_dir.path().join("");
217        let out_path = parent_path.join("package");
218
219        // This creates an unusual situation where parent name might be empty
220        let result = determine_package_structure(&out_path);
221
222        // Should handle gracefully
223        assert!(result.is_ok());
224
225        Ok(())
226    }
227}