Skip to main content

quanttide_devops/contract/
scope.rs

1//! Scope 配置:语言、构建工具、注册表类型的枚举与反序列化。
2
3use serde::de::{Deserializer, MapAccess, Visitor};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7use super::platform::Registry;
8use super::stage::StageRelease;
9
10// ── Scopes(上下文维度)───────────────────────────────────────────────
11
12/// 作用域(上下文维度)。
13///
14/// 通过 scope 为不同组件挂载不同的 Stage、Platform、Source 组合。
15#[derive(Debug, Clone, Serialize)]
16#[serde(rename_all = "snake_case")]
17pub struct Scope {
18    pub name: String,
19    pub dir: String,
20    #[serde(default)]
21    pub language: Language,
22    #[serde(default)]
23    pub framework: String,
24    #[serde(default)]
25    pub build_tool: BuildTool,
26    #[serde(default)]
27    pub registry: Registry,
28    #[serde(default)]
29    pub release: StageRelease,
30    #[serde(default)]
31    pub test_threshold: Option<f64>,
32    #[serde(default)]
33    pub ci_workflow: Option<String>,
34}
35
36/// 编程语言。
37#[derive(Debug, Clone, PartialEq, Serialize)]
38#[serde(rename_all = "snake_case")]
39pub enum Language {
40    Rust,
41    Python,
42    Go,
43    Dart,
44    #[serde(rename = "typescript")]
45    TypeScript,
46    Unknown(String),
47}
48
49impl Default for Language {
50    fn default() -> Self {
51        Self::Unknown("auto".into())
52    }
53}
54
55impl Language {
56    /// 返回语言的显示名称。
57    pub fn as_str(&self) -> &str {
58        match self {
59            Self::Rust => "rust",
60            Self::Python => "python",
61            Self::Go => "go",
62            Self::Dart => "dart",
63            Self::TypeScript => "typescript",
64            Self::Unknown(s) => s,
65        }
66    }
67
68    /// 返回该语言最常用的构建工具。
69    ///
70    /// ```
71    /// use quanttide_devops::contract::{BuildTool, Language};
72    /// assert_eq!(Language::Rust.default_build_tool(), BuildTool::Cargo);
73    /// assert_eq!(Language::Python.default_build_tool(), BuildTool::Uv);
74    /// assert_eq!(Language::Go.default_build_tool(), BuildTool::Go);
75    /// assert_eq!(Language::Dart.default_build_tool(), BuildTool::Flutter);
76    /// assert_eq!(Language::TypeScript.default_build_tool(), BuildTool::Npm);
77    /// assert!(matches!(Language::Unknown("x".into()).default_build_tool(), BuildTool::Unknown(_)));
78    /// ```
79    pub fn default_build_tool(&self) -> BuildTool {
80        match self {
81            Self::Rust => BuildTool::Cargo,
82            Self::Python => BuildTool::Uv,
83            Self::Go => BuildTool::Go,
84            Self::Dart => BuildTool::Flutter,
85            Self::TypeScript => BuildTool::Npm,
86            Self::Unknown(_) => BuildTool::Unknown("auto".into()),
87        }
88    }
89
90    /// 返回该语言最常用的包注册中心。
91    ///
92    /// ```
93    /// use quanttide_devops::contract::{Language, Registry};
94    /// assert_eq!(Language::Rust.default_registry(), Registry::Crates);
95    /// assert_eq!(Language::Python.default_registry(), Registry::PyPI);
96    /// assert_eq!(Language::Dart.default_registry(), Registry::PubDev);
97    /// assert_eq!(Language::TypeScript.default_registry(), Registry::Npm);
98    /// assert_eq!(Language::Unknown("x".into()).default_registry(), Registry::None);
99    /// ```
100    pub fn default_registry(&self) -> Registry {
101        match self {
102            Self::Rust => Registry::Crates,
103            Self::Python => Registry::PyPI,
104            Self::Go => Registry::GitHubReleases,
105            Self::Dart => Registry::PubDev,
106            Self::TypeScript => Registry::Npm,
107            Self::Unknown(_) => Registry::None,
108        }
109    }
110}
111
112/// 构建工具。
113#[derive(Debug, Clone, PartialEq, Serialize)]
114#[serde(rename_all = "snake_case")]
115pub enum BuildTool {
116    Cargo,
117    Uv,
118    Go,
119    Flutter,
120    Npm,
121    Unknown(String),
122}
123
124impl Default for BuildTool {
125    fn default() -> Self {
126        Self::Unknown("auto".into())
127    }
128}
129
130impl BuildTool {
131    /// 返回构建工具的显示名称。
132    pub fn as_str(&self) -> &str {
133        match self {
134            Self::Cargo => "cargo",
135            Self::Uv => "uv",
136            Self::Go => "go",
137            Self::Flutter => "flutter",
138            Self::Npm => "npm",
139            Self::Unknown(s) => s,
140        }
141    }
142}
143
144// ── 自定义反序列化(Language / BuildTool)────────────────────────────
145
146impl<'de> Deserialize<'de> for Language {
147    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
148    where
149        D: Deserializer<'de>,
150    {
151        let s = String::deserialize(deserializer)?;
152        Ok(match s.as_str() {
153            "rust" => Language::Rust,
154            "python" => Language::Python,
155            "go" => Language::Go,
156            "dart" => Language::Dart,
157            "typescript" | "ts" | "node" => Language::TypeScript,
158            other => Language::Unknown(other.to_string()),
159        })
160    }
161}
162
163impl<'de> Deserialize<'de> for BuildTool {
164    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
165    where
166        D: Deserializer<'de>,
167    {
168        let s = String::deserialize(deserializer)?;
169        Ok(match s.as_str() {
170            "cargo" => BuildTool::Cargo,
171            "uv" | "poetry" | "pdm" => BuildTool::Uv,
172            "go" => BuildTool::Go,
173            "flutter" => BuildTool::Flutter,
174            "npm" | "pnpm" | "yarn" | "bun" => BuildTool::Npm,
175            other => BuildTool::Unknown(other.to_string()),
176        })
177    }
178}
179
180// ── 自定义反序列化(scopes: map → Vec<Scope>)────────────────────────
181
182/// YAML 中的 scope 原始配置(map 格式的中间表示)。
183#[derive(Debug, Deserialize)]
184#[serde(rename_all = "snake_case")]
185struct ScopeConfig {
186    dir: String,
187    #[serde(default)]
188    language: Option<Language>,
189    #[serde(default)]
190    framework: Option<String>,
191    #[serde(default)]
192    build_tool: Option<BuildTool>,
193    #[serde(default)]
194    registry: Option<Registry>,
195    #[serde(default)]
196    release: Option<StageRelease>,
197    #[serde(default)]
198    test_threshold: Option<f64>,
199    #[serde(default)]
200    ci_workflow: Option<String>,
201}
202
203pub fn deserialize_scopes<'de, D>(deserializer: D) -> Result<Vec<Scope>, D::Error>
204where
205    D: Deserializer<'de>,
206{
207    /// 访问器:将 `{ name: { dir, ... } }` 转为 `[Scope, ...]`。
208    struct ScopesVisitor;
209
210    impl<'de> Visitor<'de> for ScopesVisitor {
211        type Value = Vec<Scope>;
212
213        fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
214            f.write_str("作用域映射")
215        }
216
217        fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
218        where
219            M: MapAccess<'de>,
220        {
221            let mut scopes = Vec::new();
222            while let Some((name, config)) = access.next_entry::<String, ScopeConfig>()? {
223                scopes.push(Scope {
224                    name,
225                    dir: config.dir,
226                    language: config.language.unwrap_or(Language::Unknown("auto".into())),
227                    framework: config.framework.unwrap_or_default(),
228                    build_tool: config
229                        .build_tool
230                        .unwrap_or(BuildTool::Unknown("auto".into())),
231                    registry: config.registry.unwrap_or(Registry::None),
232                    release: config.release.unwrap_or_default(),
233                    test_threshold: config.test_threshold,
234                    ci_workflow: config.ci_workflow,
235                });
236            }
237            Ok(scopes)
238        }
239    }
240
241    deserializer.deserialize_map(ScopesVisitor)
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_language_as_str() {
250        assert_eq!(Language::Rust.as_str(), "rust");
251        assert_eq!(Language::Python.as_str(), "python");
252        assert_eq!(Language::Go.as_str(), "go");
253        assert_eq!(Language::Dart.as_str(), "dart");
254        assert_eq!(Language::TypeScript.as_str(), "typescript");
255        assert_eq!(Language::Unknown("zig".into()).as_str(), "zig");
256    }
257
258    #[test]
259    fn test_build_tool_as_str() {
260        assert_eq!(BuildTool::Cargo.as_str(), "cargo");
261        assert_eq!(BuildTool::Uv.as_str(), "uv");
262        assert_eq!(BuildTool::Go.as_str(), "go");
263        assert_eq!(BuildTool::Flutter.as_str(), "flutter");
264        assert_eq!(BuildTool::Npm.as_str(), "npm");
265        assert_eq!(BuildTool::Unknown("make".into()).as_str(), "make");
266    }
267
268    #[test]
269    fn test_language_deserialize() {
270        let lang: Language = serde_yaml::from_str("rust").unwrap();
271        assert_eq!(lang, Language::Rust);
272        let lang: Language = serde_yaml::from_str("zig").unwrap();
273        assert_eq!(lang, Language::Unknown("zig".into()));
274    }
275
276    #[test]
277    fn test_build_tool_deserialize() {
278        let tool: BuildTool = serde_yaml::from_str("cargo").unwrap();
279        assert_eq!(tool, BuildTool::Cargo);
280        let tool: BuildTool = serde_yaml::from_str("make").unwrap();
281        assert_eq!(tool, BuildTool::Unknown("make".into()));
282    }
283
284    #[test]
285    fn test_language_default() {
286        assert_eq!(Language::default(), Language::Unknown("auto".into()));
287    }
288
289    #[test]
290    fn test_build_tool_default() {
291        assert_eq!(BuildTool::default(), BuildTool::Unknown("auto".into()));
292    }
293
294    #[test]
295    fn test_language_default_build_tool() {
296        assert_eq!(Language::Rust.default_build_tool(), BuildTool::Cargo);
297        assert_eq!(Language::Python.default_build_tool(), BuildTool::Uv);
298        assert_eq!(Language::Go.default_build_tool(), BuildTool::Go);
299        assert_eq!(Language::Dart.default_build_tool(), BuildTool::Flutter);
300        assert_eq!(Language::TypeScript.default_build_tool(), BuildTool::Npm);
301        assert!(matches!(
302            Language::Unknown("x".into()).default_build_tool(),
303            BuildTool::Unknown(_)
304        ));
305    }
306
307    #[test]
308    fn test_language_default_registry() {
309        assert_eq!(Language::Rust.default_registry(), Registry::Crates);
310        assert_eq!(Language::Python.default_registry(), Registry::PyPI);
311        assert_eq!(Language::Go.default_registry(), Registry::GitHubReleases);
312        assert_eq!(Language::Dart.default_registry(), Registry::PubDev);
313        assert_eq!(Language::TypeScript.default_registry(), Registry::Npm);
314        assert_eq!(
315            Language::Unknown("x".into()).default_registry(),
316            Registry::None
317        );
318    }
319}