Skip to main content

light_fw_cli_core/
report.rs

1use std::collections::BTreeMap;
2
3use serde_json::json;
4
5use crate::manifest::Dependency;
6use crate::resolve::RequiredComponent;
7use crate::scan::ScanResult;
8
9/// Проблема, обнаруженная при анализе проекта.
10#[derive(Debug, Clone)]
11pub struct Issue {
12    /// Человекочитаемое описание проблемы.
13    pub message: String,
14}
15
16/// Рекомендация добавить компонент.
17#[derive(Debug, Clone)]
18pub struct Recommendation {
19    /// Имя крейта-компонента.
20    pub name: String,
21    /// Строка требования, например `light-fw-element-form ^0.3`.
22    pub requirement: String,
23}
24
25/// Полный результат анализа проекта.
26#[derive(Debug)]
27pub struct ProjectReport {
28    /// Результат сканирования шаблонов.
29    pub scan: ScanResult,
30    /// Установленные компоненты.
31    pub installed: Vec<Dependency>,
32    /// Используемые, но не установленные компоненты.
33    pub missing: Vec<RequiredComponent>,
34    /// Установленные, но не используемые компоненты.
35    pub unused: Vec<Dependency>,
36    /// Обнаруженные проблемы.
37    pub issues: Vec<Issue>,
38    /// Рекомендации по добавлению отсутствующих компонентов.
39    pub recommendations: Vec<Recommendation>,
40}
41
42impl ProjectReport {
43    /// Есть ли проблемы, из-за которых `check --strict` должен вернуть код 1.
44    pub fn has_problems(&self) -> bool {
45        !self.missing.is_empty() || !self.issues.is_empty() || !self.unused.is_empty()
46    }
47
48    /// Текстовый отчёт для терминала.
49    pub fn to_text(&self) -> String {
50        let mut out = String::new();
51        out.push_str("Отчёт о проекте Light\n\n");
52        out.push_str(&format!("Шаблонов: {}\n", self.scan.files.len()));
53
54        out.push_str("\nТеги:\n");
55        if self.scan.tags.is_empty() {
56            out.push_str("  (нет)\n");
57        }
58        for (name, count) in &self.scan.tags {
59            out.push_str(&format!("  {name}: {count}\n"));
60        }
61
62        out.push_str("\nАтрибуты:\n");
63        if self.scan.attributes.is_empty() {
64            out.push_str("  (нет)\n");
65        }
66        for (name, count) in &self.scan.attributes {
67            out.push_str(&format!("  {name}: {count}\n"));
68        }
69
70        out.push_str("\nУстановленные компоненты:\n");
71        if self.installed.is_empty() {
72            out.push_str("  (нет)\n");
73        }
74        for c in &self.installed {
75            match &c.version {
76                Some(v) => out.push_str(&format!("  {} {}\n", c.name, v)),
77                None => out.push_str(&format!("  {}\n", c.name)),
78            }
79        }
80
81        out.push_str("\nОтсутствующие компоненты:\n");
82        if self.missing.is_empty() {
83            out.push_str("  (нет)\n");
84        }
85        for c in &self.missing {
86            out.push_str(&format!("  {}\n", c.name));
87        }
88
89        out.push_str("\nНеиспользуемые компоненты:\n");
90        if self.unused.is_empty() {
91            out.push_str("  (нет)\n");
92        }
93        for c in &self.unused {
94            out.push_str(&format!("  {}\n", c.name));
95        }
96
97        if !self.issues.is_empty() {
98            out.push_str("\nПроблемы:\n");
99            for issue in &self.issues {
100                out.push_str(&format!("  {}\n", issue.message));
101            }
102        }
103
104        if !self.recommendations.is_empty() {
105            out.push_str("\nРекомендации:\n");
106            for r in &self.recommendations {
107                out.push_str(&format!("  добавить {}\n", r.requirement));
108            }
109        }
110
111        out
112    }
113
114    /// Отчёт в формате JSON (для `--format json`).
115    pub fn to_json(&self) -> serde_json::Value {
116        let mut tags: BTreeMap<String, usize> = BTreeMap::new();
117        for (k, v) in &self.scan.tags {
118            tags.insert(k.clone(), *v);
119        }
120        let mut attrs: BTreeMap<String, usize> = BTreeMap::new();
121        for (k, v) in &self.scan.attributes {
122            attrs.insert(k.clone(), *v);
123        }
124        json!({
125            "files": self.scan.files.len(),
126            "tags": tags,
127            "attributes": attrs,
128            "installed": self.installed.iter().map(|c| {
129                json!({ "name": c.name, "version": c.version })
130            }).collect::<Vec<_>>(),
131            "missing": self.missing.iter().map(|c| c.name.clone()).collect::<Vec<_>>(),
132            "unused": self.unused.iter().map(|c| c.name.clone()).collect::<Vec<_>>(),
133            "issues": self.issues.iter().map(|i| i.message.clone()).collect::<Vec<_>>(),
134            "recommendations": self.recommendations.iter().map(|r| {
135                json!({ "name": r.name, "requirement": r.requirement })
136            }).collect::<Vec<_>>(),
137        })
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::scan::ScanResult;
145
146    fn empty_report() -> ProjectReport {
147        ProjectReport {
148            scan: ScanResult::default(),
149            installed: Vec::new(),
150            missing: Vec::new(),
151            unused: Vec::new(),
152            issues: Vec::new(),
153            recommendations: Vec::new(),
154        }
155    }
156
157    #[test]
158    fn empty_report_has_no_problems() {
159        assert!(!empty_report().has_problems());
160    }
161
162    #[test]
163    fn report_with_missing_component_has_problems() {
164        let mut report = empty_report();
165        report.missing.push(RequiredComponent {
166            name: "light-fw-element-form".into(),
167        });
168        assert!(report.has_problems());
169    }
170
171    #[test]
172    fn json_contains_expected_keys() {
173        let value = empty_report().to_json();
174        assert_eq!(value["files"], 0);
175        assert!(value["tags"].is_object());
176        assert!(value["missing"].is_array());
177    }
178}