1use 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
67macro_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 #[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 pub fn load_all(data_dir: &Path) -> Result<Self, DbcError> {
89 let $dir = data_dir.join("data").join("tables");
90 $( 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
271fn 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}
280fn 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 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 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 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}