Skip to main content

quanttide_devops/source/
config_file.rs

1//! 从配置文件读取事实数据(版本号、语言等)。
2//!
3//! # 版本号
4//!
5//! 支持以下文件格式:
6//!
7//! | 文件 | 提取方式 |
8//! |------|---------|
9//! | `Cargo.toml` | `version = "..."` 键值对 |
10//! | `pyproject.toml` | `version = "..."` 键值对 |
11//! | `package.json` | `"version": "..."` JSON |
12//! | `pubspec.yaml` | `version: ...` YAML |
13//!
14//! # 语言检测
15//!
16//! 根据目录下的标志文件推测编程语言,是 `contract::Contract::resolve_language`
17//! 在 scope 未声明语言时的兜底方案。
18//!
19//! # 示例
20//!
21//! ```ignore
22//! use quanttide_devops::source::config_file::read_config_versions;
23//! let versions = read_config_versions(scope_dir);
24//! ```
25
26use crate::contract::Language;
27
28use std::path::Path;
29
30type VersionExtract = fn(&str) -> Option<String>;
31
32// ═══════════════════════════════════════════════════════════════════════
33// 语言检测
34// ═══════════════════════════════════════════════════════════════════════
35
36/// 根据目录下的标志文件推测编程语言(按优先级返回首个匹配)。
37///
38/// 优先级:Cargo.toml > pyproject.toml/requirements.txt > go.mod > pubspec.yaml > package.json
39///
40/// ⚠ **已废弃**:请使用 [`detect_languages`] 替代,它会独立检测所有语言,
41/// 不丢失 monorepo 多语言信息。
42///
43/// ```
44/// use std::path::Path;
45/// use quanttide_devops::source::config_file::detect_language;
46///
47/// let lang = detect_language(Path::new("/tmp/nonexistent"));
48/// assert!(matches!(lang, quanttide_devops::contract::Language::Unknown(_)));
49/// ```
50#[deprecated(note = "请使用 detect_languages,它独立检测所有语言而非按优先级返回首个")]
51pub fn detect_language(dir: &Path) -> Language {
52    if dir.join("Cargo.toml").exists() {
53        Language::Rust
54    } else if dir.join("pyproject.toml").exists() || dir.join("requirements.txt").exists() {
55        Language::Python
56    } else if dir.join("go.mod").exists() {
57        Language::Go
58    } else if dir.join("pubspec.yaml").exists() {
59        Language::Dart
60    } else if dir.join("package.json").exists() {
61        Language::TypeScript
62    } else {
63        Language::Unknown("无法识别".into())
64    }
65}
66
67/// 独立检测目录下的所有编程语言,不依赖优先级(每个标志文件独立检查)。
68///
69/// monorepo 根目录可能同时存在多种语言的配置文件(如 `Cargo.toml` + `pyproject.toml`),
70/// 此函数返回所有匹配的语言,适合需要检查所有工具链的场景(如 CLI `doctor status`)。
71///
72/// ```
73/// use std::path::Path;
74/// use quanttide_devops::source::config_file::detect_languages;
75///
76/// let langs = detect_languages(Path::new("/tmp/nonexistent"));
77/// assert!(langs.is_empty());
78/// ```
79pub fn detect_languages(dir: &Path) -> Vec<Language> {
80    let mut result = Vec::new();
81    if dir.join("Cargo.toml").exists() {
82        result.push(Language::Rust);
83    }
84    if dir.join("pyproject.toml").exists() || dir.join("requirements.txt").exists() {
85        if !result.contains(&Language::Python) {
86            result.push(Language::Python);
87        }
88    }
89    if dir.join("go.mod").exists() {
90        result.push(Language::Go);
91    }
92    if dir.join("pubspec.yaml").exists() {
93        result.push(Language::Dart);
94    }
95    if dir.join("package.json").exists() {
96        result.push(Language::TypeScript);
97    }
98    result
99}
100
101// ═══════════════════════════════════════════════════════════════════════
102// 版本号读取
103// ═══════════════════════════════════════════════════════════════════════
104
105/// 读取目录下所有已知配置文件的版本号。
106///
107/// 返回 `[(文件名, Option<版本号>)]`,文件不存在则跳过。
108///
109/// ```
110/// use std::path::Path;
111/// use quanttide_devops::source::config_file::read_config_versions;
112/// let versions = read_config_versions(Path::new("/tmp/nonexistent"));
113/// assert!(versions.is_empty());
114/// ```
115pub fn read_config_versions(dir: &Path) -> Vec<(String, Option<String>)> {
116    let checks: &[(&str, VersionExtract)] = &[
117        ("Cargo.toml", |c| extract_kv_version(c, "version")),
118        ("pyproject.toml", |c| extract_kv_version(c, "version")),
119        ("package.json", extract_json_version),
120        ("pubspec.yaml", |c| extract_kv_yaml_version(c)),
121    ];
122    checks
123        .iter()
124        .filter_map(|(name, extract)| {
125            let path = dir.join(name);
126            if path.exists() {
127                let content = std::fs::read_to_string(&path).ok()?;
128                Some((name.to_string(), extract(&content)))
129            } else {
130                None
131            }
132        })
133        .collect()
134}
135
136fn extract_kv_version(content: &str, key: &str) -> Option<String> {
137    let p = format!("{} = \"", key);
138    for line in content.lines() {
139        let t = line.trim();
140        if let Some(r) = t.strip_prefix(&p)
141            && let Some(end) = r.find('"')
142        {
143            let v = r[..end].to_string();
144            if !v.is_empty() {
145                return Some(v);
146            }
147        }
148    }
149    None
150}
151
152fn extract_json_version(content: &str) -> Option<String> {
153    let key = r#""version":"#;
154    for line in content.lines() {
155        if let Some(pos) = line.find(key) {
156            let after_key = line[pos + 10..].trim();
157            if let Some(start) = after_key.find('"') {
158                let after_open = &after_key[start + 1..];
159                if let Some(end) = after_open.find('"') {
160                    let v = &after_open[..end];
161                    if !v.is_empty() {
162                        return Some(v.to_string());
163                    }
164                }
165            }
166        }
167    }
168    None
169}
170
171fn extract_kv_yaml_version(content: &str) -> Option<String> {
172    for line in content.lines() {
173        let t = line.trim();
174        if let Some(r) = t.strip_prefix("version:") {
175            let v = r.trim();
176            if !v.is_empty() && !v.starts_with('#') {
177                return Some(v.to_string());
178            }
179        }
180    }
181    None
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    // ── 语言检测 ──────────────────────────────────────────────
189
190    #[allow(deprecated)]
191    #[test]
192    fn test_detect_language_rust() {
193        let d = tempfile::tempdir().unwrap();
194        std::fs::write(d.path().join("Cargo.toml"), "").unwrap();
195        assert_eq!(detect_language(d.path()), Language::Rust);
196    }
197
198    #[allow(deprecated)]
199    #[test]
200    fn test_detect_language_unknown() {
201        let d = tempfile::tempdir().unwrap();
202        assert!(matches!(detect_language(d.path()), Language::Unknown(_)));
203    }
204
205    #[allow(deprecated)]
206    #[test]
207    fn test_detect_language_python() {
208        let d = tempfile::tempdir().unwrap();
209        std::fs::write(d.path().join("pyproject.toml"), "").unwrap();
210        assert_eq!(detect_language(d.path()), Language::Python);
211    }
212
213    #[allow(deprecated)]
214    #[test]
215    fn test_detect_language_python_requirements() {
216        let d = tempfile::tempdir().unwrap();
217        std::fs::write(d.path().join("requirements.txt"), "").unwrap();
218        assert_eq!(detect_language(d.path()), Language::Python);
219    }
220
221    #[allow(deprecated)]
222    #[test]
223    fn test_detect_language_go() {
224        let d = tempfile::tempdir().unwrap();
225        std::fs::write(d.path().join("go.mod"), "").unwrap();
226        assert_eq!(detect_language(d.path()), Language::Go);
227    }
228
229    #[allow(deprecated)]
230    #[test]
231    fn test_detect_language_dart() {
232        let d = tempfile::tempdir().unwrap();
233        std::fs::write(d.path().join("pubspec.yaml"), "").unwrap();
234        assert_eq!(detect_language(d.path()), Language::Dart);
235    }
236
237    #[allow(deprecated)]
238    #[test]
239    fn test_detect_language_typescript() {
240        let d = tempfile::tempdir().unwrap();
241        std::fs::write(d.path().join("package.json"), "").unwrap();
242        assert_eq!(detect_language(d.path()), Language::TypeScript);
243    }
244
245    // ── 多语言检测 ──────────────────────────────────────────
246
247    #[test]
248    fn test_detect_languages_single() {
249        let d = tempfile::tempdir().unwrap();
250        std::fs::write(d.path().join("Cargo.toml"), "").unwrap();
251        assert_eq!(detect_languages(d.path()), vec![Language::Rust]);
252    }
253
254    #[test]
255    fn test_detect_languages_multi() {
256        let d = tempfile::tempdir().unwrap();
257        std::fs::write(d.path().join("Cargo.toml"), "").unwrap();
258        std::fs::write(d.path().join("pyproject.toml"), "").unwrap();
259        let langs = detect_languages(d.path());
260        assert!(langs.contains(&Language::Rust));
261        assert!(langs.contains(&Language::Python));
262        assert_eq!(langs.len(), 2);
263    }
264
265    #[test]
266    fn test_detect_languages_empty() {
267        let d = tempfile::tempdir().unwrap();
268        assert!(detect_languages(d.path()).is_empty());
269    }
270
271    #[test]
272    fn test_detect_languages_python_dedup() {
273        // pyproject.toml + requirements.txt 只应产生一个 Python
274        let d = tempfile::tempdir().unwrap();
275        std::fs::write(d.path().join("pyproject.toml"), "").unwrap();
276        std::fs::write(d.path().join("requirements.txt"), "").unwrap();
277        assert_eq!(detect_languages(d.path()), vec![Language::Python]);
278    }
279
280    // ── 版本号提取 ────────────────────────────────────────────
281
282    #[test]
283    fn test_extract_kv_version_found() {
284        let c = "[package]\nname = \"test\"\nversion = \"1.2.3\"\n";
285        assert_eq!(extract_kv_version(c, "version"), Some("1.2.3".into()));
286    }
287
288    #[test]
289    fn test_extract_kv_version_not_found() {
290        assert_eq!(extract_kv_version("", "version"), None);
291    }
292
293    #[test]
294    fn test_extract_json_version_found() {
295        assert_eq!(
296            extract_json_version(r#"{"version": "1.0.0"}"#),
297            Some("1.0.0".into())
298        );
299    }
300
301    #[test]
302    fn test_extract_json_version_not_found() {
303        assert_eq!(extract_json_version("{}"), None);
304    }
305
306    #[test]
307    fn test_extract_kv_yaml_version_found() {
308        assert_eq!(
309            extract_kv_yaml_version("version: 0.2.0"),
310            Some("0.2.0".into())
311        );
312    }
313
314    #[test]
315    fn test_extract_kv_yaml_version_commented() {
316        assert_eq!(extract_kv_yaml_version("# version: 0.2.0"), None);
317    }
318
319    #[test]
320    fn test_read_config_versions_empty_dir() {
321        let d = tempfile::tempdir().unwrap();
322        assert!(read_config_versions(d.path()).is_empty());
323    }
324
325    #[test]
326    fn test_read_config_versions_cargo() {
327        let d = tempfile::tempdir().unwrap();
328        std::fs::write(
329            d.path().join("Cargo.toml"),
330            "[package]\nname = \"test\"\nversion = \"0.1.0\"\n",
331        )
332        .unwrap();
333        let versions = read_config_versions(d.path());
334        assert_eq!(versions.len(), 1);
335        assert_eq!(versions[0].1.as_deref(), Some("0.1.0"));
336    }
337}