rust_docs_mcp/cache/
source.rs1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(tag = "type", content = "data")]
11pub enum SourceType {
12 CratesIo,
14 GitHub {
16 url: String,
18 repo_path: Option<String>,
20 reference: GitReference,
22 },
23 Local {
25 path: String,
27 },
28}
29
30#[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
39pub struct SourceDetector;
41
42impl SourceDetector {
43 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 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 fn parse_url(url: &str) -> SourceType {
73 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 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 SourceType::Local {
101 path: url.to_string(),
102 }
103 }
104 }
105
106 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 if parts.len() > 4 && parts[2] == "tree" {
115 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 SourceType::GitHub {
128 url: base_url,
129 repo_path: None,
130 reference: explicit_reference.unwrap_or(GitReference::Default),
131 }
132 }
133 } else {
134 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}