Skip to main content

xchecker_utils/
source.rs

1//! Source resolution for different input types
2//!
3//! This module handles resolving different source types (GitHub, filesystem, stdin)
4//! and provides structured error reporting for resolution failures.
5
6pub use crate::error::SourceError;
7use std::path::PathBuf;
8
9/// Source types supported by xchecker
10/// Reserved for future multi-source spec ingestion (GitHub issues, filesystem, stdin)
11#[derive(Debug, Clone)]
12#[allow(dead_code)]
13pub enum SourceType {
14    GitHub { owner: String, repo: String },
15    FileSystem { path: PathBuf },
16    Stdin,
17}
18
19/// Resolved source content
20/// Reserved for future multi-source spec ingestion
21#[derive(Debug, Clone)]
22#[allow(dead_code)]
23pub struct SourceContent {
24    pub source_type: SourceType,
25    pub content: String,
26    pub metadata: std::collections::HashMap<String, String>,
27}
28
29/// Source resolver for different input types
30pub struct SourceResolver;
31
32impl SourceResolver {
33    /// Resolve a GitHub source
34    pub fn resolve_github(
35        owner: &str,
36        repo: &str,
37        issue_id: &str,
38    ) -> Result<SourceContent, SourceError> {
39        // Simulate GitHub API resolution
40        if owner.is_empty() || repo.is_empty() {
41            return Err(SourceError::InvalidFormat {
42                reason: "GitHub owner and repo must be specified".to_string(),
43            });
44        }
45
46        if issue_id.parse::<u32>().is_err() {
47            return Err(SourceError::InvalidFormat {
48                reason: "Issue ID must be a valid number".to_string(),
49            });
50        }
51
52        // For now, return a simulated response
53        // In a real implementation, this would make GitHub API calls
54        let content = format!(
55            "GitHub issue #{issue_id} from {owner}/{repo}\n\nThis is a simulated issue description that would be fetched from the GitHub API."
56        );
57
58        let mut metadata = std::collections::HashMap::new();
59        metadata.insert("owner".to_string(), owner.to_string());
60        metadata.insert("repo".to_string(), repo.to_string());
61        metadata.insert("issue_id".to_string(), issue_id.to_string());
62
63        Ok(SourceContent {
64            source_type: SourceType::GitHub {
65                owner: owner.to_string(),
66                repo: repo.to_string(),
67            },
68            content,
69            metadata,
70        })
71    }
72
73    /// Resolve a filesystem source
74    pub fn resolve_filesystem(path: &PathBuf) -> Result<SourceContent, SourceError> {
75        if !path.exists() {
76            return Err(SourceError::FileSystemNotFound {
77                path: path.display().to_string(),
78            });
79        }
80
81        let content = if path.is_file() {
82            std::fs::read_to_string(path).map_err(|_| SourceError::FileSystemNotFound {
83                path: path.display().to_string(),
84            })?
85        } else if path.is_dir() {
86            format!(
87                "Directory source: {}\n\nThis would contain a summary of the directory contents and relevant files.",
88                path.display()
89            )
90        } else {
91            return Err(SourceError::FileSystemNotFound {
92                path: path.display().to_string(),
93            });
94        };
95
96        let mut metadata = std::collections::HashMap::new();
97        metadata.insert("path".to_string(), path.display().to_string());
98        metadata.insert(
99            "type".to_string(),
100            if path.is_file() { "file" } else { "directory" }.to_string(),
101        );
102
103        Ok(SourceContent {
104            source_type: SourceType::FileSystem { path: path.clone() },
105            content,
106            metadata,
107        })
108    }
109
110    /// Resolve stdin source
111    pub fn resolve_stdin() -> Result<SourceContent, SourceError> {
112        use std::io::Read;
113
114        let mut buffer = String::new();
115        std::io::stdin()
116            .read_to_string(&mut buffer)
117            .map_err(|e| SourceError::StdinReadFailed {
118                reason: e.to_string(),
119            })?;
120
121        if buffer.trim().is_empty() {
122            return Err(SourceError::EmptyInput);
123        }
124
125        let mut metadata = std::collections::HashMap::new();
126        metadata.insert("length".to_string(), buffer.len().to_string());
127
128        Ok(SourceContent {
129            source_type: SourceType::Stdin,
130            content: buffer,
131            metadata,
132        })
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::error::UserFriendlyError;
140
141    #[test]
142    fn test_github_source_resolution() {
143        let result = SourceResolver::resolve_github("owner", "repo", "123");
144        assert!(result.is_ok());
145
146        let content = result.unwrap();
147        assert!(content.content.contains("GitHub issue #123"));
148        assert_eq!(content.metadata.get("owner"), Some(&"owner".to_string()));
149    }
150
151    #[test]
152    fn test_github_source_invalid_issue() {
153        let result = SourceResolver::resolve_github("owner", "repo", "invalid");
154        assert!(result.is_err());
155
156        if let Err(SourceError::InvalidFormat { reason }) = result {
157            assert!(reason.contains("valid number"));
158        } else {
159            panic!("Expected InvalidFormat error");
160        }
161    }
162
163    #[test]
164    fn test_filesystem_source_not_found() {
165        // Use a path that doesn't exist on any platform (including Windows where /nonexistent
166        // might resolve to a drive-relative path like D:\nonexistent)
167        let path = PathBuf::from("__nonexistent_test_path_7f8e9d6c5b4a3__");
168        let result = SourceResolver::resolve_filesystem(&path);
169        assert!(result.is_err());
170
171        if let Err(SourceError::FileSystemNotFound { path: error_path }) = result {
172            assert!(error_path.contains("nonexistent"));
173        } else {
174            panic!("Expected FileSystemNotFound error");
175        }
176    }
177
178    #[test]
179    fn test_source_error_user_friendly_messages() {
180        let error = SourceError::GitHubAuthFailed {
181            reason: "authentication failed".to_string(),
182        };
183
184        assert!(error.user_message().contains("authentication failed"));
185        assert!(!error.suggestions().is_empty());
186        assert!(error.suggestions().iter().any(|s| s.contains("auth")));
187    }
188}