Skip to main content

forge/manifest_ledger/
types.rs

1use clap::ValueEnum;
2use serde::Serialize;
3use wowlab_fs::path::Path;
4
5#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
6pub(super) enum LedgerFormat {
7    Json,
8    #[default]
9    Markdown,
10}
11
12#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
13#[serde(rename_all = "snake_case")]
14pub(super) enum OperationCategory {
15    Damage,
16    AuraApply,
17    AuraRemove,
18    AuraTick,
19    Resource,
20    Cooldown,
21    Proc,
22    Gate,
23    Replacement,
24    Targeting,
25    ActorOrPet,
26    Mastery,
27    TelemetryOnly,
28    OrderedContent,
29}
30
31impl OperationCategory {
32    #[expect(
33        clippy::trivially_copy_pass_by_ref,
34        reason = "conversion-style accessors conventionally borrow self"
35    )]
36    pub(super) const fn as_str(&self) -> &'static str {
37        match self {
38            Self::Damage => "damage",
39            Self::AuraApply => "aura_apply",
40            Self::AuraRemove => "aura_remove",
41            Self::AuraTick => "aura_tick",
42            Self::Resource => "resource",
43            Self::Cooldown => "cooldown",
44            Self::Proc => "proc",
45            Self::Gate => "gate",
46            Self::Replacement => "replacement",
47            Self::Targeting => "targeting",
48            Self::ActorOrPet => "actor_or_pet",
49            Self::Mastery => "mastery",
50            Self::TelemetryOnly => "telemetry_only",
51            Self::OrderedContent => "ordered_content",
52        }
53    }
54}
55
56#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
57#[serde(rename_all = "snake_case")]
58pub(super) enum LedgerDisposition {
59    RedundantGeneric,
60    GenericGap,
61    DataAbsent,
62    ContentOrdering,
63    TelemetryBinding,
64    Unresolved,
65}
66
67impl LedgerDisposition {
68    #[expect(
69        clippy::trivially_copy_pass_by_ref,
70        reason = "conversion-style accessors conventionally borrow self"
71    )]
72    pub(super) const fn as_str(&self) -> &'static str {
73        match self {
74            Self::RedundantGeneric => "redundant_generic",
75            Self::GenericGap => "generic_gap",
76            Self::DataAbsent => "data_absent",
77            Self::ContentOrdering => "content_ordering",
78            Self::TelemetryBinding => "telemetry_binding",
79            Self::Unresolved => "unresolved",
80        }
81    }
82
83    pub(super) const fn sort_rank(self) -> u8 {
84        const GENERIC_GAP: u8 = 0;
85        const REDUNDANT_GENERIC: u8 = 1;
86        const DATA_ABSENT: u8 = 2;
87        const CONTENT_ORDERING: u8 = 3;
88        const TELEMETRY_BINDING: u8 = 4;
89        const UNRESOLVED: u8 = 5;
90
91        match self {
92            Self::GenericGap => GENERIC_GAP,
93            Self::RedundantGeneric => REDUNDANT_GENERIC,
94            Self::DataAbsent => DATA_ABSENT,
95            Self::ContentOrdering => CONTENT_ORDERING,
96            Self::TelemetryBinding => TELEMETRY_BINDING,
97            Self::Unresolved => UNRESOLVED,
98        }
99    }
100}
101
102#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
103pub(super) struct LedgerRow {
104    pub(super) spec_or_item: String,
105    pub(super) manifest_path: String,
106    pub(super) manifest_key: String,
107    #[serde(skip_serializing_if = "Vec::is_empty")]
108    pub(super) owning_spell_ids: Vec<u32>,
109    #[serde(skip_serializing_if = "Vec::is_empty")]
110    pub(super) child_spell_ids: Vec<u32>,
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub(super) hook_file: Option<String>,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub(super) hook_function: Option<String>,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub(super) hook_operation: Option<usize>,
117    pub(super) category: OperationCategory,
118    #[serde(skip_serializing_if = "Vec::is_empty")]
119    pub(super) semantic_ids: Vec<Box<str>>,
120    pub(super) disposition: LedgerDisposition,
121    pub(super) evidence: String,
122    pub(super) spec_count: usize,
123    pub(super) operation_count: usize,
124    #[serde(skip)]
125    pub(super) effect_coordinates: Vec<(u32, u8)>,
126}
127
128pub(super) struct LedgerSource {
129    spec_or_item: String,
130    manifest_path: String,
131    manifest_key: String,
132}
133
134impl LedgerSource {
135    pub(super) fn new(
136        spec_or_item: impl Into<String>,
137        manifest_path: impl Into<String>,
138        manifest_key: impl Into<String>,
139    ) -> Self {
140        Self {
141            spec_or_item: spec_or_item.into(),
142            manifest_path: manifest_path.into(),
143            manifest_key: manifest_key.into(),
144        }
145    }
146}
147
148impl<Spec, Path, Key> From<(Spec, Path, Key)> for LedgerSource
149where
150    Spec: Into<String>,
151    Path: Into<String>,
152    Key: Into<String>,
153{
154    fn from((spec_or_item, manifest_path, manifest_key): (Spec, Path, Key)) -> Self {
155        Self::new(spec_or_item, manifest_path, manifest_key)
156    }
157}
158
159pub(super) struct LedgerFinding {
160    category: OperationCategory,
161    disposition: LedgerDisposition,
162    evidence: String,
163}
164
165impl LedgerFinding {
166    pub(super) fn new(
167        category: OperationCategory,
168        disposition: LedgerDisposition,
169        evidence: impl Into<String>,
170    ) -> Self {
171        Self {
172            category,
173            disposition,
174            evidence: evidence.into(),
175        }
176    }
177}
178
179impl<Evidence> From<(OperationCategory, LedgerDisposition, Evidence)> for LedgerFinding
180where
181    Evidence: Into<String>,
182{
183    fn from(
184        (category, disposition, evidence): (OperationCategory, LedgerDisposition, Evidence),
185    ) -> Self {
186        Self::new(category, disposition, evidence)
187    }
188}
189
190impl LedgerRow {
191    pub(super) fn new(source: impl Into<LedgerSource>, finding: impl Into<LedgerFinding>) -> Self {
192        let source = source.into();
193        let finding = finding.into();
194
195        Self {
196            spec_or_item: source.spec_or_item,
197            manifest_path: source.manifest_path,
198            manifest_key: source.manifest_key,
199            owning_spell_ids: Vec::new(),
200            child_spell_ids: Vec::new(),
201            hook_file: None,
202            hook_function: None,
203            hook_operation: None,
204            category: finding.category,
205            semantic_ids: Vec::new(),
206            disposition: finding.disposition,
207            evidence: finding.evidence,
208            spec_count: 0,
209            operation_count: 0,
210            effect_coordinates: Vec::new(),
211        }
212    }
213
214    pub(super) fn owner(mut self, spell_id: u32) -> Self {
215        self.owning_spell_ids.push(spell_id);
216
217        self
218    }
219
220    pub(super) fn child(mut self, spell_id: u32) -> Self {
221        self.child_spell_ids.push(spell_id);
222
223        self
224    }
225
226    pub(super) fn effect(mut self, spell_id: u32, effect: u8) -> Self {
227        self.effect_coordinates.push((spell_id, effect));
228
229        self
230    }
231
232    pub(super) fn hook(mut self, file: &Path, function: String) -> Self {
233        self.hook_file = Some(file.display().to_string());
234        self.hook_function = Some(function);
235
236        self
237    }
238}
239
240#[derive(Debug, Serialize)]
241pub(super) struct BehaviorShapeSummary {
242    pub(super) shape: String,
243    pub(super) observations: usize,
244    pub(super) specs: usize,
245}
246
247#[derive(Debug, Serialize)]
248pub(super) struct ManifestLedger {
249    pub(super) composed_specs: usize,
250    pub(super) items: usize,
251    pub(super) behavior_shapes: Vec<BehaviorShapeSummary>,
252    pub(super) category_totals: std::collections::BTreeMap<String, usize>,
253    pub(super) disposition_totals: std::collections::BTreeMap<String, usize>,
254    pub(super) rows: Vec<LedgerRow>,
255}