use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
pub struct CrystalShardsParser;
impl ManifestParser for CrystalShardsParser {
fn filename(&self) -> &'static str {
"shard.yml"
}
fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
let mut name: Option<String> = None;
let mut version: Option<String> = None;
let mut deps: Vec<DeclaredDep> = Vec::new();
#[derive(PartialEq)]
enum Section {
None,
Dependencies,
DevDependencies,
}
let mut section = Section::None;
let mut current_dep_name: Option<String> = None;
let mut current_dep_kind = DepKind::Normal;
let mut current_dep_version: Option<String> = None;
for line in content.lines() {
let stripped = line.trim_end();
if stripped.trim().is_empty() || stripped.trim().starts_with('#') {
continue;
}
let leading_spaces = stripped.len() - stripped.trim_start().len();
let trimmed = stripped.trim();
if leading_spaces == 0 {
flush_dep(
&mut current_dep_name,
&mut current_dep_version,
current_dep_kind,
&mut deps,
);
if trimmed == "dependencies:" {
section = Section::Dependencies;
} else if trimmed == "development_dependencies:" {
section = Section::DevDependencies;
} else {
if let Some((key, val)) = split_key_value(trimmed) {
match key {
"name" => name = Some(val.to_string()),
"version" => version = Some(val.to_string()),
_ => {}
}
}
section = Section::None;
}
} else if leading_spaces == 2
&& (section == Section::Dependencies || section == Section::DevDependencies)
{
flush_dep(
&mut current_dep_name,
&mut current_dep_version,
current_dep_kind,
&mut deps,
);
if let Some(dep_name) = trimmed.strip_suffix(':')
&& !dep_name.starts_with('#')
{
current_dep_name = Some(dep_name.to_string());
current_dep_kind = if section == Section::DevDependencies {
DepKind::Dev
} else {
DepKind::Normal
};
current_dep_version = None;
}
} else if leading_spaces >= 4 && current_dep_name.is_some() {
if let Some((key, val)) = split_key_value(trimmed)
&& key == "version"
&& !val.is_empty()
{
current_dep_version = Some(val.to_string());
}
}
}
flush_dep(
&mut current_dep_name,
&mut current_dep_version,
current_dep_kind,
&mut deps,
);
Ok(ParsedManifest {
ecosystem: "shards",
name,
version,
dependencies: deps,
})
}
}
fn flush_dep(
name: &mut Option<String>,
version: &mut Option<String>,
kind: DepKind,
deps: &mut Vec<DeclaredDep>,
) {
if let Some(n) = name.take() {
deps.push(DeclaredDep {
name: n,
version_req: version.take(),
kind,
});
}
*version = None;
}
fn split_key_value(s: &str) -> Option<(&str, &str)> {
let colon = s.find(':')?;
let key = s[..colon].trim();
let raw_val = s[colon + 1..].trim();
let val = raw_val
.trim_start_matches('"')
.trim_end_matches('"')
.trim_start_matches('\'')
.trim_end_matches('\'');
Some((key, val))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ManifestParser;
#[test]
fn test_parse_shard_yml() {
let content = r#"name: my_project
version: 0.1.0
authors:
- Alice <alice@example.com>
dependencies:
mysql:
github: crystal-lang/crystal-mysql
version: ~> 0.13.0
kemal:
github: kemalcr/kemal
version: ~> 1.3
redis:
github: stefanwille/crystal-redis
development_dependencies:
webmock:
github: manastech/webmock.cr
version: ~> 0.3
"#;
let m = CrystalShardsParser.parse(content).unwrap();
assert_eq!(m.ecosystem, "shards");
assert_eq!(m.name.as_deref(), Some("my_project"));
assert_eq!(m.version.as_deref(), Some("0.1.0"));
let mysql = m.dependencies.iter().find(|d| d.name == "mysql").unwrap();
assert_eq!(mysql.kind, DepKind::Normal);
assert_eq!(mysql.version_req.as_deref(), Some("~> 0.13.0"));
let kemal = m.dependencies.iter().find(|d| d.name == "kemal").unwrap();
assert_eq!(kemal.version_req.as_deref(), Some("~> 1.3"));
let redis = m.dependencies.iter().find(|d| d.name == "redis").unwrap();
assert!(redis.version_req.is_none());
let webmock = m.dependencies.iter().find(|d| d.name == "webmock").unwrap();
assert_eq!(webmock.kind, DepKind::Dev);
assert_eq!(webmock.version_req.as_deref(), Some("~> 0.3"));
}
#[test]
fn test_no_dev_deps() {
let content = r#"name: minimal
version: 0.2.0
dependencies:
lucky:
github: luckyframework/lucky
version: ~> 1.0.0
"#;
let m = CrystalShardsParser.parse(content).unwrap();
assert_eq!(m.dependencies.len(), 1);
assert_eq!(m.dependencies[0].name, "lucky");
assert_eq!(m.dependencies[0].kind, DepKind::Normal);
}
}