normalize_manifest/
crystal.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
10
11pub struct CrystalShardsParser;
13
14impl ManifestParser for CrystalShardsParser {
15 fn filename(&self) -> &'static str {
16 "shard.yml"
17 }
18
19 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
20 let mut name: Option<String> = None;
21 let mut version: Option<String> = None;
22 let mut deps: Vec<DeclaredDep> = Vec::new();
23
24 #[derive(PartialEq)]
25 enum Section {
26 None,
27 Dependencies,
28 DevDependencies,
29 }
30
31 let mut section = Section::None;
32 let mut current_dep_name: Option<String> = None;
33 let mut current_dep_kind = DepKind::Normal;
34 let mut current_dep_version: Option<String> = None;
35
36 for line in content.lines() {
37 let stripped = line.trim_end();
39 if stripped.trim().is_empty() || stripped.trim().starts_with('#') {
40 continue;
41 }
42
43 let leading_spaces = stripped.len() - stripped.trim_start().len();
45 let trimmed = stripped.trim();
46
47 if leading_spaces == 0 {
48 flush_dep(
50 &mut current_dep_name,
51 &mut current_dep_version,
52 current_dep_kind,
53 &mut deps,
54 );
55
56 if trimmed == "dependencies:" {
57 section = Section::Dependencies;
58 } else if trimmed == "development_dependencies:" {
59 section = Section::DevDependencies;
60 } else {
61 if let Some((key, val)) = split_key_value(trimmed) {
63 match key {
64 "name" => name = Some(val.to_string()),
65 "version" => version = Some(val.to_string()),
66 _ => {}
67 }
68 }
69 section = Section::None;
70 }
71 } else if leading_spaces == 2
72 && (section == Section::Dependencies || section == Section::DevDependencies)
73 {
74 flush_dep(
76 &mut current_dep_name,
77 &mut current_dep_version,
78 current_dep_kind,
79 &mut deps,
80 );
81
82 if let Some(dep_name) = trimmed.strip_suffix(':')
84 && !dep_name.starts_with('#')
85 {
86 current_dep_name = Some(dep_name.to_string());
87 current_dep_kind = if section == Section::DevDependencies {
88 DepKind::Dev
89 } else {
90 DepKind::Normal
91 };
92 current_dep_version = None;
93 }
94 } else if leading_spaces >= 4 && current_dep_name.is_some() {
95 if let Some((key, val)) = split_key_value(trimmed)
97 && key == "version"
98 && !val.is_empty()
99 {
100 current_dep_version = Some(val.to_string());
101 }
102 }
103 }
104
105 flush_dep(
107 &mut current_dep_name,
108 &mut current_dep_version,
109 current_dep_kind,
110 &mut deps,
111 );
112
113 Ok(ParsedManifest {
114 ecosystem: "shards",
115 name,
116 version,
117 dependencies: deps,
118 })
119 }
120}
121
122fn flush_dep(
123 name: &mut Option<String>,
124 version: &mut Option<String>,
125 kind: DepKind,
126 deps: &mut Vec<DeclaredDep>,
127) {
128 if let Some(n) = name.take() {
129 deps.push(DeclaredDep {
130 name: n,
131 version_req: version.take(),
132 kind,
133 });
134 }
135 *version = None;
136}
137
138fn split_key_value(s: &str) -> Option<(&str, &str)> {
140 let colon = s.find(':')?;
141 let key = s[..colon].trim();
142 let raw_val = s[colon + 1..].trim();
143 let val = raw_val
145 .trim_start_matches('"')
146 .trim_end_matches('"')
147 .trim_start_matches('\'')
148 .trim_end_matches('\'');
149 Some((key, val))
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::ManifestParser;
156
157 #[test]
158 fn test_parse_shard_yml() {
159 let content = r#"name: my_project
160version: 0.1.0
161
162authors:
163 - Alice <alice@example.com>
164
165dependencies:
166 mysql:
167 github: crystal-lang/crystal-mysql
168 version: ~> 0.13.0
169 kemal:
170 github: kemalcr/kemal
171 version: ~> 1.3
172 redis:
173 github: stefanwille/crystal-redis
174
175development_dependencies:
176 webmock:
177 github: manastech/webmock.cr
178 version: ~> 0.3
179"#;
180 let m = CrystalShardsParser.parse(content).unwrap();
181 assert_eq!(m.ecosystem, "shards");
182 assert_eq!(m.name.as_deref(), Some("my_project"));
183 assert_eq!(m.version.as_deref(), Some("0.1.0"));
184
185 let mysql = m.dependencies.iter().find(|d| d.name == "mysql").unwrap();
186 assert_eq!(mysql.kind, DepKind::Normal);
187 assert_eq!(mysql.version_req.as_deref(), Some("~> 0.13.0"));
188
189 let kemal = m.dependencies.iter().find(|d| d.name == "kemal").unwrap();
190 assert_eq!(kemal.version_req.as_deref(), Some("~> 1.3"));
191
192 let redis = m.dependencies.iter().find(|d| d.name == "redis").unwrap();
193 assert!(redis.version_req.is_none());
194
195 let webmock = m.dependencies.iter().find(|d| d.name == "webmock").unwrap();
196 assert_eq!(webmock.kind, DepKind::Dev);
197 assert_eq!(webmock.version_req.as_deref(), Some("~> 0.3"));
198 }
199
200 #[test]
201 fn test_no_dev_deps() {
202 let content = r#"name: minimal
203version: 0.2.0
204
205dependencies:
206 lucky:
207 github: luckyframework/lucky
208 version: ~> 1.0.0
209"#;
210 let m = CrystalShardsParser.parse(content).unwrap();
211 assert_eq!(m.dependencies.len(), 1);
212 assert_eq!(m.dependencies[0].name, "lucky");
213 assert_eq!(m.dependencies[0].kind, DepKind::Normal);
214 }
215}