wowlab_engine_application/audit/
mod.rs1mod checks;
4mod data_values;
5mod generic_folds;
6
7use checks::{audit_auras, audit_auto_attacks, audit_hero_talents, audit_spells, audit_talents};
8use data_values::check_data_values;
9use generic_folds::check_duplicate_generic_folds;
10use wowlab_engine_ports::DynDataResolver;
11use wowlab_manifest_schema::{
12 Manifest, ManifestDiagnosticLocation, ManifestLoadError, ManifestRepository,
13};
14
15#[derive(Debug)]
17pub struct AuditIssue {
18 pub category: &'static str,
19 pub name: String,
20 pub id: u32,
21 pub message: String,
22}
23
24#[derive(Debug, Default)]
25struct AuditSink {
26 errors: Vec<AuditIssue>,
27 warnings: Vec<Box<str>>,
28}
29
30struct AuditCtx<'a> {
31 manifest: &'a Manifest,
32 resolver: &'a DynDataResolver<'a>,
33 sink: &'a mut AuditSink,
34}
35
36impl AuditSink {
37 fn error(&mut self, issue: AuditIssue) {
38 self.errors.push(issue);
39 }
40
41 fn warning(&mut self, message: String) {
42 self.warnings.push(message.into_boxed_str());
43 }
44
45 fn warning_count(&self) -> usize {
46 self.warnings.len()
47 }
48}
49
50#[derive(Debug)]
52pub struct SpecAudit {
53 pub display_name: String,
54 pub spec_id: u32,
55 pub errors: Vec<AuditIssue>,
56 pub warnings: Vec<String>,
57 pub spell_count: usize,
58 pub aura_count: usize,
59 pub talent_count: usize,
60 pub hero_spell_count: usize,
61 pub hero_aura_count: usize,
62}
63
64pub async fn audit_manifest_repository(
66 repository: &ManifestRepository,
67 resolver: &DynDataResolver<'_>,
68) -> Result<Vec<SpecAudit>, ManifestLoadError> {
69 let paths = repository.spec_paths()?;
70 let mut audits = Vec::with_capacity(paths.len());
71
72 for path in paths {
73 let file_stem = repository.spec_slug(&path)?;
74 let manifest = repository.load_spec(&path)?;
75
76 audits.push(audit_manifest(&file_stem, &manifest, resolver).await);
77 }
78
79 Ok(audits)
80}
81
82pub async fn audit_manifest(
84 file_stem: &str,
85 manifest: &Manifest,
86 resolver: &DynDataResolver<'_>,
87) -> SpecAudit {
88 let mut sink = AuditSink::default();
89
90 audit_manifest_diagnostics(manifest, &mut sink);
91 let mut ctx = AuditCtx {
92 manifest,
93 resolver,
94 sink: &mut sink,
95 };
96
97 audit_spells(&mut ctx).await;
98 audit_auras(&mut ctx).await;
99 audit_talents(&mut ctx).await;
100 audit_auto_attacks(&mut ctx).await;
101 let (hero_spell_count, hero_aura_count) = audit_hero_talents(&mut ctx).await;
102
103 check_data_values(&mut ctx).await;
104 check_duplicate_generic_folds(&mut ctx).await;
105
106 SpecAudit {
107 display_name: slug_to_display(file_stem),
108 spec_id: manifest.spec.id,
109 errors: sink.errors,
110 warnings: sink.warnings.into_iter().map(Into::into).collect(),
111 spell_count: manifest.spells.len(),
112 aura_count: manifest.auras.len(),
113 talent_count: manifest.talents.len(),
114 hero_spell_count,
115 hero_aura_count,
116 }
117}
118
119fn audit_manifest_diagnostics(manifest: &Manifest, sink: &mut AuditSink) {
121 for diagnostic in manifest.diagnostics() {
122 let (category, name, id) = diagnostic_context(manifest, diagnostic.location());
123
124 sink.error(AuditIssue {
125 category,
126 name,
127 id,
128 message: diagnostic.to_string(),
129 });
130 }
131}
132
133fn diagnostic_context(
134 manifest: &Manifest,
135 location: &ManifestDiagnosticLocation,
136) -> (&'static str, String, u32) {
137 match location {
138 ManifestDiagnosticLocation::SpecPrecombatAuras
139 | ManifestDiagnosticLocation::SpecStealthAura => {
140 ("spec", "spec".to_owned(), manifest.spec.id)
141 }
142 ManifestDiagnosticLocation::Aura { name } => (
143 "aura",
144 name.clone(),
145 manifest.auras.get(name).map_or(0, |aura| aura.id),
146 ),
147 ManifestDiagnosticLocation::Spell { name } => (
148 "spell",
149 name.clone(),
150 manifest.spells.get(name).map_or(0, |spell| spell.id),
151 ),
152 ManifestDiagnosticLocation::AutoAttack { name } => (
153 "auto_attack",
154 name.clone(),
155 manifest
156 .auto_attacks
157 .get(name)
158 .map_or(0, |auto_attack| auto_attack.spell_id),
159 ),
160 ManifestDiagnosticLocation::TalentCompanionAuras { talent } => (
161 "talent",
162 talent.clone(),
163 manifest.talents.get(talent).copied().unwrap_or(0),
164 ),
165 ManifestDiagnosticLocation::SpellGroup { index } => ("spell_group", index.to_string(), 0),
166 ManifestDiagnosticLocation::ImpactProc { index } => ("impact_proc", index.to_string(), 0),
167 _ => ("manifest", location.to_string(), 0),
168 }
169}
170
171fn slug_to_display(slug: &str) -> String {
172 slug.split('_')
173 .map(|w| {
174 let mut c = w.chars();
175
176 match c.next() {
177 None => String::new(),
178 Some(first) => first.to_uppercase().to_string() + c.as_str(),
179 }
180 })
181 .collect::<Vec<_>>()
182 .join(" ")
183}
184
185#[cfg(test)]
186mod tests;