normalize_manifest/
vlang.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
7
8pub struct VModParser;
10
11impl ManifestParser for VModParser {
12 fn filename(&self) -> &'static str {
13 "v.mod"
14 }
15
16 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
17 let mut name: Option<String> = None;
18 let mut version: Option<String> = None;
19 let mut deps: Vec<DeclaredDep> = Vec::new();
20
21 let mut in_deps = false;
22
23 for line in content.lines() {
24 let trimmed = line.trim();
25
26 if trimmed.is_empty() || trimmed.starts_with("//") {
27 continue;
28 }
29
30 if trimmed.starts_with("dependencies:") {
32 in_deps = true;
33 if let Some(rest) = trimmed.strip_prefix("dependencies:") {
35 let rest = rest.trim();
36 if rest.starts_with('[') {
37 extract_vmod_list(rest, &mut deps);
38 if rest.contains(']') {
39 in_deps = false;
40 }
41 }
42 }
43 continue;
44 }
45
46 if in_deps {
47 extract_vmod_list(trimmed, &mut deps);
48 if trimmed.contains(']') {
49 in_deps = false;
50 }
51 continue;
52 }
53
54 if let Some(rest) = trimmed.strip_prefix("name:") {
56 if name.is_none() {
57 let v = extract_single_quoted(rest.trim())
58 .or_else(|| extract_double_quoted(rest.trim()))
59 .unwrap_or_else(|| rest.trim().to_string());
60 if !v.is_empty() {
61 name = Some(v);
62 }
63 }
64 continue;
65 }
66
67 if let Some(rest) = trimmed.strip_prefix("version:") {
69 if version.is_none() {
70 let v = extract_single_quoted(rest.trim())
71 .or_else(|| extract_double_quoted(rest.trim()))
72 .unwrap_or_else(|| rest.trim().to_string());
73 if !v.is_empty() {
74 version = Some(v);
75 }
76 }
77 continue;
78 }
79 }
80
81 Ok(ParsedManifest {
82 ecosystem: "vpm",
83 name,
84 version,
85 dependencies: deps,
86 })
87 }
88}
89
90fn extract_single_quoted(s: &str) -> Option<String> {
92 let s = s.trim();
93 let inner = s.strip_prefix('\'')?;
94 let end = inner.find('\'')?;
95 Some(inner[..end].to_string())
96}
97
98fn extract_double_quoted(s: &str) -> Option<String> {
100 let s = s.trim();
101 let inner = s.strip_prefix('"')?;
102 let end = inner.find('"')?;
103 Some(inner[..end].to_string())
104}
105
106fn extract_vmod_list(fragment: &str, deps: &mut Vec<DeclaredDep>) {
108 let mut s = fragment;
109 while let Some(start) = s.find('\'') {
110 s = &s[start + 1..];
111 if let Some(end) = s.find('\'') {
112 let dep_name = s[..end].trim().to_string();
113 if !dep_name.is_empty() {
114 deps.push(DeclaredDep {
115 name: dep_name,
116 version_req: None,
117 kind: DepKind::Normal,
118 });
119 }
120 s = &s[end + 1..];
121 } else {
122 break;
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::ManifestParser;
131
132 const SAMPLE: &str = r#"Module {
133 name: 'mymodule'
134 description: 'My V module'
135 version: '0.1.0'
136 license: 'MIT'
137 dependencies: ['vweb', 'json', 'db.sqlite']
138}
139"#;
140
141 #[test]
142 fn test_parse_vmod() {
143 let m = VModParser.parse(SAMPLE).unwrap();
144 assert_eq!(m.ecosystem, "vpm");
145 assert_eq!(m.name.as_deref(), Some("mymodule"));
146 assert_eq!(m.version.as_deref(), Some("0.1.0"));
147
148 let names: Vec<&str> = m.dependencies.iter().map(|d| d.name.as_str()).collect();
149 assert!(names.contains(&"vweb"), "{names:?}");
150 assert!(names.contains(&"json"), "{names:?}");
151 assert!(names.contains(&"db.sqlite"), "{names:?}");
152 assert_eq!(m.dependencies.len(), 3);
153 }
154
155 #[test]
156 fn test_multiline_deps() {
157 let content = r#"Module {
158 name: 'multi'
159 version: '0.2.0'
160 dependencies: [
161 'a',
162 'b',
163 'c'
164 ]
165}
166"#;
167 let m = VModParser.parse(content).unwrap();
168 let names: Vec<&str> = m.dependencies.iter().map(|d| d.name.as_str()).collect();
169 assert_eq!(names, vec!["a", "b", "c"], "{names:?}");
170 }
171
172 #[test]
173 fn test_no_deps() {
174 let content = r#"Module {
175 name: 'simple'
176 version: '1.0.0'
177}
178"#;
179 let m = VModParser.parse(content).unwrap();
180 assert_eq!(m.name.as_deref(), Some("simple"));
181 assert!(m.dependencies.is_empty());
182 }
183}