normalize_manifest/
composer.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
4use serde_json::Value;
5
6pub struct ComposerParser;
8
9impl ManifestParser for ComposerParser {
10 fn filename(&self) -> &'static str {
11 "composer.json"
12 }
13
14 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
15 let json: Value =
16 serde_json::from_str(content).map_err(|e| ManifestError(e.to_string()))?;
17
18 let name = json
19 .get("name")
20 .and_then(|v| v.as_str())
21 .map(|s| s.to_string());
22 let version = json
23 .get("version")
24 .and_then(|v| v.as_str())
25 .map(|s| s.to_string());
26
27 let mut deps = Vec::new();
28 extract_composer_deps(&json, "require", DepKind::Normal, &mut deps);
29 extract_composer_deps(&json, "require-dev", DepKind::Dev, &mut deps);
30
31 Ok(ParsedManifest {
32 ecosystem: "composer",
33 name,
34 version,
35 dependencies: deps,
36 })
37 }
38}
39
40fn extract_composer_deps(json: &Value, field: &str, kind: DepKind, out: &mut Vec<DeclaredDep>) {
41 let Some(obj) = json.get(field).and_then(|v| v.as_object()) else {
42 return;
43 };
44 for (name, ver) in obj {
45 if name == "php" || name.starts_with("ext-") || name.starts_with("lib-") {
47 continue;
48 }
49 out.push(DeclaredDep {
50 name: name.clone(),
51 version_req: ver.as_str().map(|s| s.to_string()),
52 kind,
53 });
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60 use crate::ManifestParser;
61
62 #[test]
63 fn test_parse_composer_json() {
64 let content = r#"{
65 "name": "vendor/my-package",
66 "version": "1.0.0",
67 "require": {
68 "php": ">=8.1",
69 "ext-json": "*",
70 "symfony/framework-bundle": "^6.0",
71 "doctrine/orm": "^2.13"
72 },
73 "require-dev": {
74 "phpunit/phpunit": "^10.0"
75 }
76}"#;
77 let m = ComposerParser.parse(content).unwrap();
78 assert_eq!(m.ecosystem, "composer");
79 assert_eq!(m.name.as_deref(), Some("vendor/my-package"));
80 assert_eq!(m.version.as_deref(), Some("1.0.0"));
81
82 let normal: Vec<_> = m
84 .dependencies
85 .iter()
86 .filter(|d| d.kind == DepKind::Normal)
87 .collect();
88 assert_eq!(normal.len(), 2);
89 assert!(normal.iter().any(|d| d.name == "symfony/framework-bundle"));
90 assert!(normal.iter().any(|d| d.name == "doctrine/orm"));
91
92 let dev: Vec<_> = m
93 .dependencies
94 .iter()
95 .filter(|d| d.kind == DepKind::Dev)
96 .collect();
97 assert_eq!(dev.len(), 1);
98 assert_eq!(dev[0].name, "phpunit/phpunit");
99 }
100}