Skip to main content

quanttide_devops/contract/
scope.rs

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