rust_docs_mcp/cache/
source.rs

1//! Source type detection and parsing for crates
2//!
3//! This module handles the detection and parsing of different crate sources,
4//! including crates.io, GitHub repositories, and local paths.
5
6use serde::{Deserialize, Serialize};
7
8/// Represents the different sources from which a crate can be obtained
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(tag = "type", content = "data")]
11pub enum SourceType {
12    /// Crate from crates.io registry
13    CratesIo,
14    /// Crate from a GitHub repository
15    GitHub {
16        /// The base repository URL (e.g., https://github.com/user/repo)
17        url: String,
18        /// Optional path within the repository to the crate
19        repo_path: Option<String>,
20        /// Branch or tag reference
21        reference: GitReference,
22    },
23    /// Crate from a local file system path
24    Local {
25        /// The local path to the crate
26        path: String,
27    },
28}
29
30/// Git reference type (branch or tag)
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32#[serde(tag = "type", content = "value")]
33pub enum GitReference {
34    Branch(String),
35    Tag(String),
36    Default,
37}
38
39/// Detects the source type from a source string
40pub struct SourceDetector;
41
42impl SourceDetector {
43    /// Detect the source type from an optional source string
44    pub fn detect(source: Option<&str>) -> SourceType {
45        match source {
46            None => SourceType::CratesIo,
47            Some(s) => {
48                if s.starts_with("http://") || s.starts_with("https://") {
49                    Self::parse_url(s)
50                } else if Self::is_local_path(s) {
51                    SourceType::Local {
52                        path: s.to_string(),
53                    }
54                } else {
55                    SourceType::CratesIo
56                }
57            }
58        }
59    }
60
61    /// Check if a string represents a local path
62    fn is_local_path(s: &str) -> bool {
63        s.starts_with('/')
64            || s.starts_with("~/")
65            || s.starts_with("../")
66            || s.starts_with("./")
67            || s.contains('/')
68            || s.contains('\\')
69    }
70
71    /// Parse a URL to determine if it's a GitHub URL
72    fn parse_url(url: &str) -> SourceType {
73        // Check for #branch: or #tag: suffix
74        let (base_url, reference) = if let Some(pos) = url.find("#branch:") {
75            let (base, branch_part) = url.split_at(pos);
76            let branch = branch_part.trim_start_matches("#branch:");
77            (
78                base.to_string(),
79                Some(GitReference::Branch(branch.to_string())),
80            )
81        } else if let Some(pos) = url.find("#tag:") {
82            let (base, tag_part) = url.split_at(pos);
83            let tag = tag_part.trim_start_matches("#tag:");
84            (base.to_string(), Some(GitReference::Tag(tag.to_string())))
85        } else {
86            (url.to_string(), None)
87        };
88
89        // Normalize http to https for GitHub
90        let normalized_url = if base_url.starts_with("http://github.com/") {
91            base_url.replace("http://", "https://")
92        } else {
93            base_url
94        };
95
96        if let Some(github_part) = normalized_url.strip_prefix("https://github.com/") {
97            Self::parse_github_url(github_part, reference)
98        } else {
99            // Not a GitHub URL, treat as local path
100            SourceType::Local {
101                path: url.to_string(),
102            }
103        }
104    }
105
106    /// Parse GitHub URL components
107    fn parse_github_url(github_part: &str, explicit_reference: Option<GitReference>) -> SourceType {
108        let parts: Vec<&str> = github_part.split('/').collect();
109
110        if parts.len() >= 2 {
111            let base_url = format!("https://github.com/{}/{}", parts[0], parts[1]);
112
113            // Check if there's a path specification (tree/branch/path)
114            if parts.len() > 4 && parts[2] == "tree" {
115                // URL format: github.com/user/repo/tree/branch/path/to/crate
116                let branch = parts[3];
117                let repo_path = parts[4..].join("/");
118
119                SourceType::GitHub {
120                    url: base_url,
121                    repo_path: Some(repo_path),
122                    reference: explicit_reference
123                        .unwrap_or_else(|| GitReference::Branch(branch.to_string())),
124                }
125            } else {
126                // Simple repository URL
127                SourceType::GitHub {
128                    url: base_url,
129                    repo_path: None,
130                    reference: explicit_reference.unwrap_or(GitReference::Default),
131                }
132            }
133        } else {
134            // Invalid GitHub URL format
135            SourceType::Local {
136                path: format!("https://github.com/{github_part}"),
137            }
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_detect_crates_io() {
148        assert_eq!(SourceDetector::detect(None), SourceType::CratesIo);
149        assert_eq!(SourceDetector::detect(Some("serde")), SourceType::CratesIo);
150    }
151
152    #[test]
153    fn test_detect_local_paths() {
154        assert!(matches!(
155            SourceDetector::detect(Some("/absolute/path")),
156            SourceType::Local { .. }
157        ));
158        assert!(matches!(
159            SourceDetector::detect(Some("~/home/path")),
160            SourceType::Local { .. }
161        ));
162        assert!(matches!(
163            SourceDetector::detect(Some("./relative/path")),
164            SourceType::Local { .. }
165        ));
166        assert!(matches!(
167            SourceDetector::detect(Some("../parent/path")),
168            SourceType::Local { .. }
169        ));
170    }
171
172    #[test]
173    fn test_detect_github_urls() {
174        match SourceDetector::detect(Some("https://github.com/rust-lang/rust")) {
175            SourceType::GitHub {
176                url,
177                repo_path,
178                reference,
179            } => {
180                assert_eq!(url, "https://github.com/rust-lang/rust");
181                assert_eq!(repo_path, None);
182                assert_eq!(reference, GitReference::Default);
183            }
184            _ => panic!("Expected GitHub source"),
185        }
186
187        match SourceDetector::detect(Some(
188            "https://github.com/rust-lang/rust/tree/master/src/libstd",
189        )) {
190            SourceType::GitHub {
191                url,
192                repo_path,
193                reference,
194            } => {
195                assert_eq!(url, "https://github.com/rust-lang/rust");
196                assert_eq!(repo_path, Some("src/libstd".to_string()));
197                assert!(matches!(reference, GitReference::Branch(b) if b == "master"));
198            }
199            _ => panic!("Expected GitHub source with path"),
200        }
201    }
202
203    #[test]
204    fn test_detect_github_with_tag() {
205        match SourceDetector::detect(Some("https://github.com/serde-rs/serde#tag:v1.0.136")) {
206            SourceType::GitHub {
207                url,
208                repo_path,
209                reference,
210            } => {
211                assert_eq!(url, "https://github.com/serde-rs/serde");
212                assert_eq!(repo_path, None);
213                assert!(matches!(reference, GitReference::Tag(t) if t == "v1.0.136"));
214            }
215            _ => panic!("Expected GitHub source with tag"),
216        }
217    }
218
219    #[test]
220    fn test_detect_github_with_branch() {
221        match SourceDetector::detect(Some(
222            "https://github.com/rust-lang/rust-clippy#branch:master",
223        )) {
224            SourceType::GitHub {
225                url,
226                repo_path,
227                reference,
228            } => {
229                assert_eq!(url, "https://github.com/rust-lang/rust-clippy");
230                assert_eq!(repo_path, None);
231                assert!(matches!(reference, GitReference::Branch(b) if b == "master"));
232            }
233            _ => panic!("Expected GitHub source with branch"),
234        }
235    }
236}