Skip to main content

typr_cli/
fs_provider.rs

1//! FileSystem-based implementations of typr-core traits
2//!
3//! These implementations allow the CLI app to use the filesystem
4//! for reading sources and writing outputs.
5
6use std::fs;
7use std::path::PathBuf;
8use typr_core::{OutputError, OutputHandler, PackageChecker, PackageError, SourceProvider};
9
10/// Filesystem-based source provider for native compilation.
11///
12/// Reads TypR source files from the filesystem.
13#[derive(Debug, Clone)]
14pub struct FileSystemSourceProvider {
15    base_path: PathBuf,
16}
17
18impl FileSystemSourceProvider {
19    /// Create a new provider with a base path for resolving relative paths
20    pub fn new(base_path: PathBuf) -> Self {
21        Self { base_path }
22    }
23
24    /// Create a provider for the current directory
25    pub fn current_dir() -> std::io::Result<Self> {
26        Ok(Self {
27            base_path: std::env::current_dir()?,
28        })
29    }
30
31    /// Get the full path for a source file
32    fn resolve_path(&self, path: &str) -> PathBuf {
33        if PathBuf::from(path).is_absolute() {
34            PathBuf::from(path)
35        } else {
36            self.base_path.join(path)
37        }
38    }
39}
40
41impl SourceProvider for FileSystemSourceProvider {
42    fn get_source(&self, path: &str) -> Option<String> {
43        let full_path = self.resolve_path(path);
44        fs::read_to_string(&full_path).ok()
45    }
46
47    fn exists(&self, path: &str) -> bool {
48        let full_path = self.resolve_path(path);
49        full_path.exists() && full_path.is_file()
50    }
51
52    fn list_sources(&self) -> Vec<String> {
53        self.list_sources_recursive(&self.base_path)
54    }
55}
56
57impl FileSystemSourceProvider {
58    /// Recursively list all .ty files under a directory
59    fn list_sources_recursive(&self, dir: &PathBuf) -> Vec<String> {
60        let mut sources = Vec::new();
61
62        if let Ok(entries) = fs::read_dir(dir) {
63            for entry in entries.flatten() {
64                let path = entry.path();
65                if path.is_dir() {
66                    sources.extend(self.list_sources_recursive(&path));
67                } else if path.extension().map_or(false, |ext| ext == "ty") {
68                    if let Ok(relative) = path.strip_prefix(&self.base_path) {
69                        sources.push(relative.to_string_lossy().to_string());
70                    }
71                }
72            }
73        }
74
75        sources
76    }
77}
78
79/// Filesystem-based output handler for native compilation.
80///
81/// Writes transpiled R code and related files to the filesystem.
82#[derive(Debug, Clone)]
83pub struct FileSystemOutputHandler {
84    output_dir: PathBuf,
85}
86
87impl FileSystemOutputHandler {
88    /// Create a new handler that writes to the specified directory
89    pub fn new(output_dir: PathBuf) -> Self {
90        Self { output_dir }
91    }
92
93    /// Create a handler for the current directory
94    pub fn current_dir() -> std::io::Result<Self> {
95        Ok(Self {
96            output_dir: std::env::current_dir()?,
97        })
98    }
99
100    /// Ensure the output directory exists
101    fn ensure_dir(&self) -> Result<(), OutputError> {
102        fs::create_dir_all(&self.output_dir).map_err(|e| OutputError {
103            message: format!("Failed to create output directory: {}", e),
104        })
105    }
106
107    /// Write content to a file in the output directory
108    fn write_file(&self, filename: &str, content: &str) -> Result<(), OutputError> {
109        self.ensure_dir()?;
110        let path = self.output_dir.join(filename);
111        fs::write(&path, content).map_err(|e| OutputError {
112            message: format!("Failed to write {}: {}", path.display(), e),
113        })
114    }
115}
116
117impl OutputHandler for FileSystemOutputHandler {
118    fn write_r_code(&mut self, filename: &str, content: &str) -> Result<(), OutputError> {
119        self.write_file(filename, content)
120    }
121
122    fn write_type_annotations(&mut self, filename: &str, content: &str) -> Result<(), OutputError> {
123        self.write_file(
124            &format!("{}_types.R", filename.trim_end_matches(".R")),
125            content,
126        )
127    }
128
129    fn write_generic_functions(
130        &mut self,
131        filename: &str,
132        content: &str,
133    ) -> Result<(), OutputError> {
134        self.write_file(
135            &format!("{}_generics.R", filename.trim_end_matches(".R")),
136            content,
137        )
138    }
139}
140
141/// Native R package checker that can query R for package availability.
142///
143/// Uses Rscript to check if packages are installed.
144#[derive(Debug, Clone)]
145pub struct NativePackageChecker {
146    /// Cache of known package types
147    package_types: std::collections::HashMap<String, String>,
148}
149
150impl NativePackageChecker {
151    pub fn new() -> Self {
152        Self {
153            package_types: std::collections::HashMap::new(),
154        }
155    }
156}
157
158impl Default for NativePackageChecker {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164impl PackageChecker for NativePackageChecker {
165    fn is_package_available(&self, name: &str) -> bool {
166        use std::process::Command;
167
168        // Use Rscript to check if package is available
169        let result = Command::new("Rscript")
170            .arg("-e")
171            .arg(format!("cat(requireNamespace('{}', quietly = TRUE))", name))
172            .output();
173
174        match result {
175            Ok(output) => {
176                let stdout = String::from_utf8_lossy(&output.stdout);
177                stdout.trim() == "TRUE"
178            }
179            Err(_) => false,
180        }
181    }
182
183    fn install_package(&mut self, name: &str) -> Result<(), PackageError> {
184        use std::process::Command;
185
186        let result = Command::new("Rscript")
187            .arg("-e")
188            .arg(format!(
189                "install.packages('{}', repos='https://cloud.r-project.org')",
190                name
191            ))
192            .output();
193
194        match result {
195            Ok(output) => {
196                if output.status.success() {
197                    Ok(())
198                } else {
199                    let stderr = String::from_utf8_lossy(&output.stderr);
200                    Err(PackageError {
201                        message: format!("Failed to install package '{}': {}", name, stderr),
202                    })
203                }
204            }
205            Err(e) => Err(PackageError {
206                message: format!("Failed to run Rscript: {}", e),
207            }),
208        }
209    }
210
211    fn get_package_types(&self, name: &str) -> Option<String> {
212        self.package_types.get(name).cloned()
213    }
214}