Skip to main content

wowlab_parsers/parsers/dbc/
loader.rs

1// #t(file: rust_alloc_in_loop) DBC loader builds per-row diagnostics and column names during parsing
2
3use wowlab_fs::{file, path::Path};
4use wowlab_types::sim::IntMap;
5
6use super::{
7    super::errors::DbcError,
8    gametable::{GameTable, load_game_tables},
9    rows::{
10        ArmorLocationRow, AssistedCombatRow, AssistedCombatRuleRow, AssistedCombatStepRow,
11        ChallengeModeItemBonusOverrideRow, ChrClassesRow, ChrRacesRow, ChrSpecializationRow,
12        ContentTuningRow, ContentTuningXDifficultyRow, ContentTuningXExpectedRow, CooldownSetRow,
13        CooldownSetSpellRow, CraftingDataEnchantQualityRow, CraftingDataItemQualityRow,
14        CraftingDataRow, CraftingQualityRow, CreatureDifficultyRow, CreatureRow,
15        CurrencyCategoryRow, CurrencyTypesRow, CurvePointRow, CurveRow, DelvesSeasonRow,
16        DifficultyRow, DisplaySeasonRow, DungeonEncounterRow, ExpectedStatModRow, ExpectedStatRow,
17        GemPropertiesRow, GlobalColorRow, GlobalStringsRow, ItemAppearanceRow, ItemArmorQualityRow,
18        ItemArmorShieldRow, ItemArmorTotalRow, ItemBonusListGroupEntryRow, ItemBonusListGroupRow,
19        ItemBonusRow, ItemBonusSequenceSpellRow, ItemBonusTreeNodeRow, ItemClassRow,
20        ItemConversionEntryRow, ItemConversionRow, ItemDamageAmmoRow, ItemDamageOneHandCasterRow,
21        ItemDamageOneHandRow, ItemDamageTwoHandCasterRow, ItemDamageTwoHandRow, ItemEffectRow,
22        ItemGroupIlvlScalingEntryRow, ItemLevelSelectorQualityRow, ItemLevelSelectorQualitySetRow,
23        ItemLevelSelectorRow, ItemModifiedAppearanceRow, ItemOffsetCurveRow, ItemRow,
24        ItemScalingConfigRow, ItemSetRow, ItemSetSpellRow, ItemSparseRow, ItemSquishEraRow,
25        ItemSubClassRow, ItemXBonusTreeRow, ItemXItemEffectRow, JournalEncounterCreatureRow,
26        JournalEncounterItemRow, JournalEncounterRow, JournalInstanceRow, JournalTierRow,
27        JournalTierXInstanceRow, ManifestInterfaceDataRow, MapChallengeModeRow, MapRow,
28        ModifiedCraftingItemRow, MythicPlusSeasonKeyFloorRow, MythicPlusSeasonRewardLevelsRow,
29        MythicPlusSeasonRow, MythicPlusSeasonTrackedAffixRow, MythicPlusSeasonTrackedMapRow,
30        PowerTypeRow, ProfessionRow, PvpSeasonRewardLevelsRow, PvpSeasonRow, PvpTierRow,
31        RandPropPointsRow, SkillLineAbilityRow, SkillLineXTraitTreeRow, SkillRaceClassInfoRow,
32        SpecSetMemberRow, SpecializationSpellsRow, SpellAuraOptionsRow, SpellAuraRestrictionsRow,
33        SpellCastTimesRow, SpellCategoriesRow, SpellCategoryRow, SpellClassOptionsRow,
34        SpellCooldownsRow, SpellDescriptionVariablesRow, SpellDurationRow, SpellEffectRow,
35        SpellEmpowerRow, SpellEmpowerStageRow, SpellEquippedItemsRow, SpellInterruptsRow,
36        SpellItemEnchantmentRow, SpellLabelRow, SpellLearnSpellRow, SpellLevelsRow, SpellMiscRow,
37        SpellNameRow, SpellPowerRow, SpellProcsPerMinuteModRow, SpellProcsPerMinuteRow,
38        SpellRadiusRow, SpellRangeRow, SpellReplacementRow, SpellRow, SpellScalingRow,
39        SpellShapeshiftFormRow, SpellShapeshiftRow, SpellTargetRestrictionsRow, SpellTotemsRow,
40        SpellXDescriptionVariablesRow, TraitCondRow, TraitCostRow, TraitCurrencyRow,
41        TraitCurrencySourceRow, TraitDefinitionEffectPointsRow, TraitDefinitionRow, TraitEdgeRow,
42        TraitNodeEntryRow, TraitNodeGroupXTraitCondRow, TraitNodeGroupXTraitCostRow,
43        TraitNodeGroupXTraitNodeRow, TraitNodeRow, TraitNodeXTraitCondRow,
44        TraitNodeXTraitNodeEntryRow, TraitSubTreeRow, TraitTreeLoadoutEntryRow,
45        TraitTreeLoadoutRow, TraitTreeXTraitCurrencyRow, UiTextureAtlasElementRow,
46    },
47};
48
49macro_rules! dbc_field_ty {
50    (by_id $row:ty) => { IntMap<i32, $row> };
51    (one_by_fk $row:ty) => { IntMap<i32, $row> };
52    (by_fk $row:ty) => { IntMap<i32, Vec<$row>> };
53}
54
55macro_rules! dbc_load {
56    (by_id $row:ty, $dir:expr, $csv:literal) => {
57        load_by_id::<$row>($dir, $csv)?
58    };
59    (one_by_fk $row:ty, $dir:expr, $csv:literal) => {
60        load_one_by_fk::<$row>($dir, $csv)?
61    };
62    (by_fk $row:ty, $dir:expr, $csv:literal) => {
63        load_by_fk::<$row>($dir, $csv)?
64    };
65}
66
67/// The caller-supplied `dir` identifier shares a hygiene context with the compute expressions.
68macro_rules! dbc_tables {
69    (
70        dir $dir:ident;
71        csv { $( $ckind:ident $cfield:ident : $crow:ty = $csv:literal ; )+ }
72        derived { $( $dfield:ident : $dty:ty = $dcompute:expr ; )+ }
73    ) => {
74        /// Container for all loaded DBC data with indexed lookups.
75        #[derive(Debug, Default)]
76        pub struct DbcData {
77            $( pub $cfield: dbc_field_ty!($ckind $crow), )+
78            $( pub $dfield: $dty, )+
79        }
80
81        impl DbcData {
82            /// Load all DBC tables from `{data_dir}/data/tables/*.csv`.
83            ///
84            /// # Errors
85            ///
86            /// Returns [`DbcError`] when a present table cannot be read or deserialized.
87            // docref:start dbc-overview-load-all
88            pub fn load_all(data_dir: &Path) -> Result<Self, DbcError> {
89                let $dir = data_dir.join("data").join("tables");
90                // docref:end dbc-overview-load-all
91
92                $( let $cfield = dbc_load!($ckind $crow, &$dir, $csv); )+
93                $( let $dfield = $dcompute; )+
94
95                Ok(Self {
96                    $( $cfield, )+
97                    $( $dfield, )+
98                })
99            }
100        }
101    };
102}
103
104dbc_tables! {
105    dir tables_dir;
106    csv {
107        by_id spell_name: SpellNameRow = "SpellName";
108        by_id spell: SpellRow = "Spell";
109        by_id spell_cast_times: SpellCastTimesRow = "SpellCastTimes";
110        by_id spell_duration: SpellDurationRow = "SpellDuration";
111        by_id spell_range: SpellRangeRow = "SpellRange";
112        by_id spell_radius: SpellRadiusRow = "SpellRadius";
113        by_id spell_category: SpellCategoryRow = "SpellCategory";
114        by_id spell_description_variables: SpellDescriptionVariablesRow = "SpellDescriptionVariables";
115        by_id difficulty: DifficultyRow = "Difficulty";
116        by_id manifest_interface_data: ManifestInterfaceDataRow = "ManifestInterfaceData";
117        one_by_fk spell_misc: SpellMiscRow = "SpellMisc";
118        by_fk spell_effect: SpellEffectRow = "SpellEffect";
119        by_fk spell_power: SpellPowerRow = "SpellPower";
120        by_fk spell_cooldowns: SpellCooldownsRow = "SpellCooldowns";
121        by_fk spell_categories: SpellCategoriesRow = "SpellCategories";
122        one_by_fk spell_class_options: SpellClassOptionsRow = "SpellClassOptions";
123        one_by_fk spell_aura_restrictions: SpellAuraRestrictionsRow = "SpellAuraRestrictions";
124        one_by_fk spell_interrupts: SpellInterruptsRow = "SpellInterrupts";
125        one_by_fk spell_empower: SpellEmpowerRow = "SpellEmpower";
126        by_fk spell_empower_stage: SpellEmpowerStageRow = "SpellEmpowerStage";
127        one_by_fk spell_target_restrictions: SpellTargetRestrictionsRow = "SpellTargetRestrictions";
128        one_by_fk spell_equipped_items: SpellEquippedItemsRow = "SpellEquippedItems";
129        by_fk spell_levels: SpellLevelsRow = "SpellLevels";
130        by_fk spell_learn_spell: SpellLearnSpellRow = "SpellLearnSpell";
131        one_by_fk spell_replacement: SpellReplacementRow = "SpellReplacement";
132        one_by_fk spell_shapeshift: SpellShapeshiftRow = "SpellShapeshift";
133        by_id spell_shapeshift_form: SpellShapeshiftFormRow = "SpellShapeshiftForm";
134        by_fk spell_totems: SpellTotemsRow = "SpellTotems";
135        by_fk spell_x_description_variables: SpellXDescriptionVariablesRow = "SpellXDescriptionVariables";
136        one_by_fk spell_aura_options: SpellAuraOptionsRow = "SpellAuraOptions";
137        by_id chr_specialization: ChrSpecializationRow = "ChrSpecialization";
138        by_id chr_classes: ChrClassesRow = "ChrClasses";
139        by_id chr_races: ChrRacesRow = "ChrRaces";
140        by_fk specialization_spells: SpecializationSpellsRow = "SpecializationSpells";
141        by_id assisted_combat: AssistedCombatRow = "AssistedCombat";
142        by_fk assisted_combat_step: AssistedCombatStepRow = "AssistedCombatStep";
143        by_fk assisted_combat_rule: AssistedCombatRuleRow = "AssistedCombatRule";
144        by_id trait_node: TraitNodeRow = "TraitNode";
145        by_id trait_node_entry: TraitNodeEntryRow = "TraitNodeEntry";
146        by_id trait_definition: TraitDefinitionRow = "TraitDefinition";
147        by_fk trait_definition_effect_points: TraitDefinitionEffectPointsRow = "TraitDefinitionEffectPoints";
148        by_id trait_tree_loadout: TraitTreeLoadoutRow = "TraitTreeLoadout";
149        by_id trait_sub_tree: TraitSubTreeRow = "TraitSubTree";
150        by_id trait_currency: TraitCurrencyRow = "TraitCurrency";
151        by_id trait_cost: TraitCostRow = "TraitCost";
152        by_id trait_cond: TraitCondRow = "TraitCond";
153        by_id ui_texture_atlas_element: UiTextureAtlasElementRow = "UiTextureAtlasElement";
154        by_fk trait_tree_loadout_entry: TraitTreeLoadoutEntryRow = "TraitTreeLoadoutEntry";
155        by_fk trait_edge: TraitEdgeRow = "TraitEdge";
156        by_fk trait_node_x_trait_node_entry: TraitNodeXTraitNodeEntryRow = "TraitNodeXTraitNodeEntry";
157        by_fk trait_tree_x_trait_currency: TraitTreeXTraitCurrencyRow = "TraitTreeXTraitCurrency";
158        by_fk trait_currency_source: TraitCurrencySourceRow = "TraitCurrencySource";
159        by_fk trait_node_group_x_trait_node: TraitNodeGroupXTraitNodeRow = "TraitNodeGroupXTraitNode";
160        by_fk trait_node_group_x_trait_cost: TraitNodeGroupXTraitCostRow = "TraitNodeGroupXTraitCost";
161        by_fk trait_node_group_x_trait_cond: TraitNodeGroupXTraitCondRow = "TraitNodeGroupXTraitCond";
162        by_fk trait_node_x_trait_cond: TraitNodeXTraitCondRow = "TraitNodeXTraitCond";
163        by_fk skill_line_x_trait_tree: SkillLineXTraitTreeRow = "SkillLineXTraitTree";
164        by_fk spec_set_member: SpecSetMemberRow = "SpecSetMember";
165        by_id item: ItemRow = "Item";
166        by_id item_sparse: ItemSparseRow = "ItemSparse";
167        by_id item_effect: ItemEffectRow = "ItemEffect";
168        by_id item_set: ItemSetRow = "ItemSet";
169        by_id item_class: ItemClassRow = "ItemClass";
170        by_id item_sub_class: ItemSubClassRow = "ItemSubClass";
171        by_id item_appearance: ItemAppearanceRow = "ItemAppearance";
172        by_id journal_encounter: JournalEncounterRow = "JournalEncounter";
173        by_id journal_instance: JournalInstanceRow = "JournalInstance";
174        by_id dungeon_encounter: DungeonEncounterRow = "DungeonEncounter";
175        by_fk item_x_item_effect: ItemXItemEffectRow = "ItemXItemEffect";
176        by_fk item_set_spell: ItemSetSpellRow = "ItemSetSpell";
177        one_by_fk item_modified_appearance: ItemModifiedAppearanceRow = "ItemModifiedAppearance";
178        by_fk journal_encounter_creature: JournalEncounterCreatureRow = "JournalEncounterCreature";
179        by_fk journal_encounter_item: JournalEncounterItemRow = "JournalEncounterItem";
180        by_id global_color: GlobalColorRow = "GlobalColor";
181        by_id global_strings: GlobalStringsRow = "GlobalStrings";
182        by_fk item_bonus: ItemBonusRow = "ItemBonus";
183        by_id curve: CurveRow = "Curve";
184        by_fk curve_point: CurvePointRow = "CurvePoint";
185        by_id rand_prop_points: RandPropPointsRow = "RandPropPoints";
186        by_id item_scaling_config: ItemScalingConfigRow = "ItemScalingConfig";
187        by_id item_offset_curve: ItemOffsetCurveRow = "ItemOffsetCurve";
188        by_id item_squish_era: ItemSquishEraRow = "ItemSquishEra";
189        one_by_fk spell_scaling: SpellScalingRow = "SpellScaling";
190        by_id spell_procs_per_minute: SpellProcsPerMinuteRow = "SpellProcsPerMinute";
191        by_fk spell_procs_per_minute_mod: SpellProcsPerMinuteModRow = "SpellProcsPerMinuteMod";
192        by_id spell_item_enchantment: SpellItemEnchantmentRow = "SpellItemEnchantment";
193        by_id gem_properties: GemPropertiesRow = "GemProperties";
194        by_id item_damage_one_hand: ItemDamageOneHandRow = "ItemDamageOneHand";
195        by_id item_damage_two_hand: ItemDamageTwoHandRow = "ItemDamageTwoHand";
196        by_id item_damage_ammo: ItemDamageAmmoRow = "ItemDamageAmmo";
197        by_id cooldown_set: CooldownSetRow = "CooldownSet";
198        by_fk cooldown_set_spell: CooldownSetSpellRow = "CooldownSetSpell";
199        by_id expected_stat: ExpectedStatRow = "ExpectedStat";
200        by_id expected_stat_mod: ExpectedStatModRow = "ExpectedStatMod";
201        by_id creature: CreatureRow = "Creature";
202        by_fk creature_difficulty: CreatureDifficultyRow = "CreatureDifficulty";
203        by_id content_tuning: ContentTuningRow = "ContentTuning";
204        by_fk content_tuning_x_difficulty: ContentTuningXDifficultyRow = "ContentTuningXDifficulty";
205        by_fk content_tuning_x_expected: ContentTuningXExpectedRow = "ContentTuningXExpected";
206        by_fk spell_label: SpellLabelRow = "SpellLabel";
207        by_id power_type: PowerTypeRow = "PowerType";
208        by_id item_armor_quality: ItemArmorQualityRow = "ItemArmorQuality";
209        by_id item_armor_shield: ItemArmorShieldRow = "ItemArmorShield";
210        by_id item_armor_total: ItemArmorTotalRow = "ItemArmorTotal";
211        by_id armor_location: ArmorLocationRow = "ArmorLocation";
212        by_id item_damage_one_hand_caster: ItemDamageOneHandCasterRow = "ItemDamageOneHandCaster";
213        by_id item_damage_two_hand_caster: ItemDamageTwoHandCasterRow = "ItemDamageTwoHandCaster";
214        by_id journal_tier: JournalTierRow = "JournalTier";
215        by_fk journal_tier_x_instance: JournalTierXInstanceRow = "JournalTierXInstance";
216        by_id map: MapRow = "Map";
217        by_id map_challenge_mode: MapChallengeModeRow = "MapChallengeMode";
218        by_fk item_bonus_tree_node: ItemBonusTreeNodeRow = "ItemBonusTreeNode";
219        by_fk item_bonus_list_group_entry: ItemBonusListGroupEntryRow = "ItemBonusListGroupEntry";
220        by_id item_level_selector: ItemLevelSelectorRow = "ItemLevelSelector";
221        by_id item_level_selector_quality: ItemLevelSelectorQualityRow = "ItemLevelSelectorQuality";
222        by_id item_level_selector_quality_set: ItemLevelSelectorQualitySetRow = "ItemLevelSelectorQualitySet";
223        by_fk item_x_bonus_tree: ItemXBonusTreeRow = "ItemXBonusTree";
224        by_fk mythic_plus_season_reward_levels: MythicPlusSeasonRewardLevelsRow = "MythicPlusSeasonRewardLevels";
225        by_id pvp_season: PvpSeasonRow = "PvpSeason";
226        by_fk pvp_season_reward_levels: PvpSeasonRewardLevelsRow = "PvpSeasonRewardLevels";
227        by_id pvp_tier: PvpTierRow = "PvpTier";
228        by_id crafting_data: CraftingDataRow = "CraftingData";
229        by_fk crafting_data_enchant_quality: CraftingDataEnchantQualityRow = "CraftingDataEnchantQuality";
230        by_fk crafting_data_item_quality: CraftingDataItemQualityRow = "CraftingDataItemQuality";
231        by_fk skill_line_ability: SkillLineAbilityRow = "SkillLineAbility";
232        by_fk skill_race_class_info: SkillRaceClassInfoRow = "SkillRaceClassInfo";
233        by_id profession: ProfessionRow = "Profession";
234        by_id modified_crafting_item: ModifiedCraftingItemRow = "ModifiedCraftingItem";
235        by_id crafting_quality: CraftingQualityRow = "CraftingQuality";
236        by_id currency_types: CurrencyTypesRow = "CurrencyTypes";
237        by_id currency_category: CurrencyCategoryRow = "CurrencyCategory";
238        by_id item_bonus_list_group: ItemBonusListGroupRow = "ItemBonusListGroup";
239        by_fk item_group_ilvl_scaling_entry: ItemGroupIlvlScalingEntryRow = "ItemGroupIlvlScalingEntry";
240        by_id item_conversion: ItemConversionRow = "ItemConversion";
241        by_fk item_conversion_entry: ItemConversionEntryRow = "ItemConversionEntry";
242        by_id challenge_mode_item_bonus_override: ChallengeModeItemBonusOverrideRow = "ChallengeModeItemBonusOverride";
243        by_id mythic_plus_season: MythicPlusSeasonRow = "MythicPlusSeason";
244        by_fk mythic_plus_season_tracked_map: MythicPlusSeasonTrackedMapRow = "MythicPlusSeasonTrackedMap";
245        by_fk mythic_plus_season_tracked_affix: MythicPlusSeasonTrackedAffixRow = "MythicPlusSeasonTrackedAffix";
246        by_fk mythic_plus_season_key_floor: MythicPlusSeasonKeyFloorRow = "MythicPlusSeasonKeyFloor";
247        by_id display_season: DisplaySeasonRow = "DisplaySeason";
248        by_id delves_season: DelvesSeasonRow = "DelvesSeason";
249        by_id item_bonus_sequence_spell: ItemBonusSequenceSpellRow = "ItemBonusSequenceSpell";
250    }
251    derived {
252        game_tables: Vec<GameTable> = load_game_tables(&tables_dir)?;
253        trait_node_by_tree: IntMap<i32, Vec<TraitNodeRow>> = group_by(&trait_node, |n| n.TraitTreeID);
254        trait_tree_loadout_by_spec: IntMap<i32, Vec<TraitTreeLoadoutRow>> =
255            group_by(&trait_tree_loadout, |l| l.ChrSpecializationID);
256        trait_cond_by_node_group: IntMap<i32, Vec<TraitCondRow>> =
257            group_by_some(&trait_cond, |condition| {
258                (condition.TraitNodeGroupID > 0).then_some(condition.TraitNodeGroupID)
259            });
260        item_class_by_class_id: IntMap<i32, ItemClassRow> = group_one_by(&item_class, |c| c.ClassID);
261        item_sub_class_by_class_id: IntMap<i32, Vec<ItemSubClassRow>> = group_by(&item_sub_class, |c| c.ClassID);
262    }
263}
264
265fn count_csv_rows(data: &[u8]) -> usize {
266    let count = data.split(|&byte| byte == b'\n').count().saturating_sub(1);
267
268    count.saturating_sub(1)
269}
270
271// docref:start dbc-overview-read-csv-bytes
272fn read_csv_bytes(path: &Path, table_name: &str) -> Result<Option<Vec<u8>>, DbcError> {
273    let file_path = path.join(format!("{table_name}.csv"));
274    match file::read_bytes(&file_path) {
275        Ok(data) => Ok(Some(data)),
276        Err(error) if error.is_not_found() => Ok(None),
277        Err(error) => Err(DbcError::io(error)),
278    }
279}
280// docref:end dbc-overview-read-csv-bytes
281
282fn load_by_id<T>(path: &Path, table_name: &str) -> Result<IntMap<i32, T>, DbcError>
283where
284    T: serde::de::DeserializeOwned + HasId,
285{
286    let Some(data) = read_csv_bytes(path, table_name)? else {
287        return Ok(IntMap::default());
288    };
289
290    let row_count = count_csv_rows(&data);
291    let mut reader = csv::Reader::from_reader(data.as_slice());
292
293    let mut map = IntMap::default();
294
295    map.reserve(row_count);
296
297    for result in reader.deserialize() {
298        let row: T = result.map_err(|error| DbcError::csv_parse(table_name.to_string(), error))?;
299
300        map.insert(row.id(), row);
301    }
302
303    Ok(map)
304}
305
306fn load_by_fk<T>(path: &Path, table_name: &str) -> Result<IntMap<i32, Vec<T>>, DbcError>
307where
308    T: serde::de::DeserializeOwned + HasFk,
309{
310    let Some(data) = read_csv_bytes(path, table_name)? else {
311        return Ok(IntMap::default());
312    };
313
314    let row_count = count_csv_rows(&data);
315
316    let mut counts: IntMap<i32, usize> = IntMap::default();
317
318    counts.reserve(row_count);
319    {
320        let mut reader = csv::Reader::from_reader(data.as_slice());
321
322        for result in reader.deserialize() {
323            let row: T =
324                result.map_err(|error| DbcError::csv_parse(table_name.to_string(), error))?;
325
326            *counts.entry(row.fk()).or_insert(0) += 1;
327        }
328    }
329
330    let mut map: IntMap<i32, Vec<T>> = IntMap::default();
331
332    map.reserve(counts.len());
333
334    for (&fk, &count) in &counts {
335        map.insert(fk, Vec::with_capacity(count));
336    }
337
338    let mut reader = csv::Reader::from_reader(data.as_slice());
339
340    for result in reader.deserialize() {
341        let row: T = result.map_err(|error| DbcError::csv_parse(table_name.to_string(), error))?;
342
343        if let Some(vec) = map.get_mut(&row.fk()) {
344            vec.push(row);
345        }
346    }
347
348    Ok(map)
349}
350
351fn load_one_by_fk<T>(path: &Path, table_name: &str) -> Result<IntMap<i32, T>, DbcError>
352where
353    T: serde::de::DeserializeOwned + HasFk,
354{
355    let Some(data) = read_csv_bytes(path, table_name)? else {
356        return Ok(IntMap::default());
357    };
358
359    let row_count = count_csv_rows(&data);
360    let mut reader = csv::Reader::from_reader(data.as_slice());
361
362    let mut map = IntMap::default();
363
364    map.reserve(row_count);
365
366    for result in reader.deserialize() {
367        let row: T = result.map_err(|error| DbcError::csv_parse(table_name.to_string(), error))?;
368
369        map.entry(row.fk()).or_insert(row);
370    }
371
372    Ok(map)
373}
374
375fn group_by<T, F>(source: &IntMap<i32, T>, key_fn: F) -> IntMap<i32, Vec<T>>
376where
377    T: Clone,
378    F: Fn(&T) -> i32,
379{
380    let mut map: IntMap<i32, Vec<T>> = IntMap::default();
381
382    // #t(block: rust_clone_in_loop) cloning borrowed values into new grouping is inherent to regrouping
383    for value in source.values() {
384        map.entry(key_fn(value)).or_default().push(value.clone());
385    }
386
387    map
388}
389
390fn group_by_some<T, F>(source: &IntMap<i32, T>, key_fn: F) -> IntMap<i32, Vec<T>>
391where
392    T: Clone,
393    F: Fn(&T) -> Option<i32>,
394{
395    let mut map: IntMap<i32, Vec<T>> = IntMap::default();
396
397    // #t(block: rust_clone_in_loop) cloning borrowed values into new grouping is inherent to regrouping
398    for value in source.values() {
399        if let Some(key) = key_fn(value) {
400            map.entry(key).or_default().push(value.clone());
401        }
402    }
403
404    map
405}
406
407fn group_one_by<T, F>(source: &IntMap<i32, T>, key_fn: F) -> IntMap<i32, T>
408where
409    T: Clone,
410    F: Fn(&T) -> i32,
411{
412    let mut map: IntMap<i32, T> = IntMap::default();
413
414    // #t(block: rust_clone_in_loop) cloning borrowed values into new grouping is inherent to regrouping
415    for value in source.values() {
416        map.entry(key_fn(value)).or_insert_with(|| value.clone());
417    }
418
419    map
420}
421
422pub(crate) trait HasId {
423    fn id(&self) -> i32;
424}
425
426pub(crate) trait HasFk {
427    fn fk(&self) -> i32;
428}
429
430#[cfg(test)]
431mod tests {
432    use googletest::prelude::*;
433    use rstest::rstest;
434
435    use super::*;
436
437    fn int_map(pairs: &[(i32, i32)]) -> IntMap<i32, i32> {
438        let mut map: IntMap<i32, i32> = IntMap::default();
439
440        for &(k, v) in pairs {
441            map.insert(k, v);
442        }
443
444        map
445    }
446
447    #[gtest]
448    #[rstest]
449    #[case::header_plus_2(b"h\na\nb\n", 2)]
450    #[case::header_only(b"h\n", 0)]
451    #[case::empty(b"", 0)]
452    #[case::no_trailing_newline(b"h\na", 0)]
453    fn count_csv_rows_cases(#[case] data: &[u8], #[case] expected: usize) -> Result<()> {
454        verify_that!(count_csv_rows(data), eq(expected))
455    }
456
457    #[gtest]
458    fn group_by_collects_same_key() -> Result<()> {
459        let source = int_map(&[(1, 10), (2, 20), (3, 25)]);
460        let grouped = group_by(&source, |v| v / 10);
461
462        let key_two = grouped.get(&2).cloned().unwrap_or_default();
463
464        verify_that!(key_two.len(), eq(2))?;
465        verify_that!(key_two.contains(&20), eq(true))?;
466        verify_that!(key_two.contains(&25), eq(true))?;
467
468        let key_one = grouped.get(&1).cloned().unwrap_or_default();
469
470        verify_that!(key_one, eq(&vec![10]))
471    }
472
473    #[gtest]
474    fn group_by_some_drops_rejected() -> Result<()> {
475        let source = int_map(&[(1, 10), (2, 20)]);
476        let grouped = group_by_some(&source, |v| (*v != 20).then_some(v / 10));
477
478        verify_that!(grouped.get(&2), none())?;
479
480        verify_that!(grouped.get(&1).cloned().unwrap_or_default(), eq(&vec![10]))
481    }
482
483    #[gtest]
484    fn group_one_by_single_key() -> Result<()> {
485        let source = int_map(&[(1, 10), (2, 20), (3, 30)]);
486        let grouped = group_one_by(&source, |v| *v);
487
488        verify_that!(grouped.get(&10), some(eq(&10)))?;
489        verify_that!(grouped.get(&20), some(eq(&20)))?;
490
491        verify_that!(grouped.get(&30), some(eq(&30)))
492    }
493
494    #[gtest]
495    fn group_one_by_duplicate_key_keeps_one() -> Result<()> {
496        let source = int_map(&[(1, 100), (2, 105)]);
497        let grouped = group_one_by(&source, |v| v / 10);
498
499        verify_that!(grouped.len(), eq(1))?;
500
501        verify_that!(grouped.get(&10), some(anything()))
502    }
503}