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/// ```
39/// use std::path::Path;
40/// use quanttide_devops::source::config_file::detect_language;
41///
42/// let lang = detect_language(Path::new("/tmp/nonexistent"));
43/// assert!(matches!(lang, quanttide_devops::contract::Language::Unknown(_)));
44/// ```
45pub fn detect_language(dir: &Path) -> Language {
46    if dir.join("Cargo.toml").exists() {
47        Language::Rust
48    } else if dir.join("pyproject.toml").exists() || dir.join("requirements.txt").exists() {
49        Language::Python
50    } else if dir.join("go.mod").exists() {
51        Language::Go
52    } else if dir.join("pubspec.yaml").exists() {
53        Language::Dart
54    } else if dir.join("package.json").exists() {
55        Language::TypeScript
56    } else {
57        Language::Unknown("无法识别".into())
58    }
59}
60
61// ═══════════════════════════════════════════════════════════════════════
62// 版本号读取
63// ═══════════════════════════════════════════════════════════════════════
64
65/// 读取目录下所有已知配置文件的版本号。
66///
67/// 返回 `[(文件名, Option<版本号>)]`,文件不存在则跳过。
68///
69/// ```
70/// use std::path::Path;
71/// use quanttide_devops::source::config_file::read_config_versions;
72/// let versions = read_config_versions(Path::new("/tmp/nonexistent"));
73/// assert!(versions.is_empty());
74/// ```
75pub fn read_config_versions(dir: &Path) -> Vec<(String, Option<String>)> {
76    let checks: &[(&str, VersionExtract)] = &[
77        ("Cargo.toml", |c| extract_kv_version(c, "version")),
78        ("pyproject.toml", |c| extract_kv_version(c, "version")),
79        ("package.json", extract_json_version),
80        ("pubspec.yaml", |c| extract_kv_yaml_version(c)),
81    ];
82    checks
83        .iter()
84        .filter_map(|(name, extract)| {
85            let path = dir.join(name);
86            if path.exists() {
87                let content = std::fs::read_to_string(&path).ok()?;
88                Some((name.to_string(), extract(&content)))
89            } else {
90                None
91            }
92        })
93        .collect()
94}
95
96fn extract_kv_version(content: &str, key: &str) -> Option<String> {
97    let p = format!("{} = \"", key);
98    for line in content.lines() {
99        let t = line.trim();
100        if let Some(r) = t.strip_prefix(&p)
101            && let Some(end) = r.find('"')
102        {
103            let v = r[..end].to_string();
104            if !v.is_empty() {
105                return Some(v);
106            }
107        }
108    }
109    None
110}
111
112fn extract_json_version(content: &str) -> Option<String> {
113    let key = r#""version":"#;
114    for line in content.lines() {
115        if let Some(pos) = line.find(key) {
116            let after_key = line[pos + 10..].trim();
117            if let Some(start) = after_key.find('"') {
118                let after_open = &after_key[start + 1..];
119                if let Some(end) = after_open.find('"') {
120                    let v = &after_open[..end];
121                    if !v.is_empty() {
122                        return Some(v.to_string());
123                    }
124                }
125            }
126        }
127    }
128    None
129}
130
131fn extract_kv_yaml_version(content: &str) -> Option<String> {
132    for line in content.lines() {
133        let t = line.trim();
134        if let Some(r) = t.strip_prefix("version:") {
135            let v = r.trim();
136            if !v.is_empty() && !v.starts_with('#') {
137                return Some(v.to_string());
138            }
139        }
140    }
141    None
142}
143
144#[cfg(test)]
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    // ── 语言检测 ──────────────────────────────────────────────
150
151    #[test]
152    fn test_detect_language_rust() {
153        let d = tempfile::tempdir().unwrap();
154        std::fs::write(d.path().join("Cargo.toml"), "").unwrap();
155        assert_eq!(detect_language(d.path()), Language::Rust);
156    }
157
158    #[test]
159    fn test_detect_language_unknown() {
160        let d = tempfile::tempdir().unwrap();
161        assert!(matches!(detect_language(d.path()), Language::Unknown(_)));
162    }
163
164    #[test]
165    fn test_detect_language_python() {
166        let d = tempfile::tempdir().unwrap();
167        std::fs::write(d.path().join("pyproject.toml"), "").unwrap();
168        assert_eq!(detect_language(d.path()), Language::Python);
169    }
170
171    #[test]
172    fn test_detect_language_python_requirements() {
173        let d = tempfile::tempdir().unwrap();
174        std::fs::write(d.path().join("requirements.txt"), "").unwrap();
175        assert_eq!(detect_language(d.path()), Language::Python);
176    }
177
178    #[test]
179    fn test_detect_language_go() {
180        let d = tempfile::tempdir().unwrap();
181        std::fs::write(d.path().join("go.mod"), "").unwrap();
182        assert_eq!(detect_language(d.path()), Language::Go);
183    }
184
185    #[test]
186    fn test_detect_language_dart() {
187        let d = tempfile::tempdir().unwrap();
188        std::fs::write(d.path().join("pubspec.yaml"), "").unwrap();
189        assert_eq!(detect_language(d.path()), Language::Dart);
190    }
191
192    #[test]
193    fn test_detect_language_typescript() {
194        let d = tempfile::tempdir().unwrap();
195        std::fs::write(d.path().join("package.json"), "").unwrap();
196        assert_eq!(detect_language(d.path()), Language::TypeScript);
197    }
198
199    // ── 版本号提取 ────────────────────────────────────────────
200
201    #[test]
202    fn test_extract_kv_version_found() {
203        let c = "[package]\nname = \"test\"\nversion = \"1.2.3\"\n";
204        assert_eq!(extract_kv_version(c, "version"), Some("1.2.3".into()));
205    }
206
207    #[test]
208    fn test_extract_kv_version_not_found() {
209        assert_eq!(extract_kv_version("", "version"), None);
210    }
211
212    #[test]
213    fn test_extract_json_version_found() {
214        assert_eq!(
215            extract_json_version(r#"{"version": "1.0.0"}"#),
216            Some("1.0.0".into())
217        );
218    }
219
220    #[test]
221    fn test_extract_json_version_not_found() {
222        assert_eq!(extract_json_version("{}"), None);
223    }
224
225    #[test]
226    fn test_extract_kv_yaml_version_found() {
227        assert_eq!(
228            extract_kv_yaml_version("version: 0.2.0"),
229            Some("0.2.0".into())
230        );
231    }
232
233    #[test]
234    fn test_extract_kv_yaml_version_commented() {
235        assert_eq!(extract_kv_yaml_version("# version: 0.2.0"), None);
236    }
237
238    #[test]
239    fn test_read_config_versions_empty_dir() {
240        let d = tempfile::tempdir().unwrap();
241        assert!(read_config_versions(d.path()).is_empty());
242    }
243
244    #[test]
245    fn test_read_config_versions_cargo() {
246        let d = tempfile::tempdir().unwrap();
247        std::fs::write(
248            d.path().join("Cargo.toml"),
249            "[package]\nname = \"test\"\nversion = \"0.1.0\"\n",
250        )
251        .unwrap();
252        let versions = read_config_versions(d.path());
253        assert_eq!(versions.len(), 1);
254        assert_eq!(versions[0].1.as_deref(), Some("0.1.0"));
255    }
256}